> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CICD-SEC-2 — Static cloud credential where OIDC federation is available

> A pipeline authenticating to AWS, GCP, or Azure with a long-lived key, when the provider would issue a short-lived one instead.

| Field      | Value                                                                                                                                                                    |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Rule ID    | `cicd-sec-2-cloud-credentials`                                                                                                                                           |
| Category   | `CICD-SEC-2`                                                                                                                                                             |
| Severity   | **MEDIUM**                                                                                                                                                               |
| Confidence | HIGH (MEDIUM for script-command matches)                                                                                                                                 |
| Platforms  | GitHub Actions **and** GitLab CI (one rule ID)                                                                                                                           |
| OWASP      | [CICD-SEC-2: Inadequate Identity and Access Management](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-02-Inadequate-Identity-And-Access-Management) |
| Auto-fix   | partial (comment only)                                                                                                                                                   |

## What the check does

Flags a pipeline that authenticates to AWS, GCP, or Azure with a **long-lived
static credential**. Three signals:

1. **A cloud login action configured with static inputs** —
   `aws-actions/configure-aws-credentials` with `aws-access-key-id:`,
   `google-github-actions/auth` with `credentials_json:`, `azure/login` with
   `creds:` or `client-secret:`.
2. **A static-credential environment variable or CI variable** — a closed set of
   names (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `GCP_SA_KEY`,
   `GOOGLE_CREDENTIALS`, `AZURE_CREDENTIALS`, `AZURE_CLIENT_SECRET`,
   `ARM_CLIENT_SECRET`, and a few siblings) — or a literal `AKIA…` access-key id
   in a value.
3. **A cloud CLI invoked in its static-credential form** —
   `aws configure set aws_access_key_id`, `gcloud auth activate-service-account`,
   `az login --service-principal … -p …`.

Signals 1 and 2 are name-exact and carry **HIGH** confidence. Signal 3 matches a
command line and carries **MEDIUM**: a script can name a command it does not run.

## Why it matters

This is **not** the "you leaked a key" rule — that is
[CICD-SEC-6 hardcoded credentials](/rules/cicd-sec-6), and it fires on the
literal value. This rule fires on a credential stored **correctly** in secrets,
because that is still a credential:

* it outlives the job that used it,
* every workflow in the repository can read it,
* and rotating it is a manual task somebody has to remember.

All three major clouds will instead trade the runner's OIDC token for a
short-lived credential scoped to the repository and the ref. That **removes** the
stored credential rather than protecting it — a different answer to the same
finding.

```yaml theme={null}
# flagged — a long-lived key, correctly stored, still long-lived
- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

## Safe alternative

Grant the job an OIDC token and let the provider mint short-lived credentials.

```yaml theme={null}
permissions:
  id-token: write        # ← the job may now mint an OIDC token
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/deploy   # ← no stored key
      aws-region: us-east-1
```

| Provider | Federated form                                                                                                                 |
| -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| AWS      | `aws-actions/configure-aws-credentials` with `role-to-assume:` against a role trusting GitHub's OIDC provider                  |
| GCP      | `google-github-actions/auth` with `workload_identity_provider:` (Workload Identity Federation)                                 |
| Azure    | `azure/login` with `client-id:` + `tenant-id:` + `subscription-id:` against an app registration holding a federated credential |

Then **delete the stored credential** — leaving it in place keeps the blast
radius the migration was meant to remove.

## GitLab CI

The same rule ID fires on `.gitlab-ci.yml`, driven by `variables:` blocks and
`script:` lines rather than actions. The token comes from an `id_tokens:` block
instead of a `permissions:` grant:

```yaml theme={null}
deploy:
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  script:
    - aws sts assume-role-with-web-identity --web-identity-token "$GITLAB_OIDC_TOKEN" ...
```

## What does not fire

* A federated login step (`role-to-assume`, `workload_identity_provider`,
  `client-id` + `tenant-id`), on either platform.
* `AWS_SESSION_TOKEN` — that is the short-lived half of the pair, and flagging it
  would advise migrating away from the thing you migrated to.
* `AWS_REGION` and other non-credential configuration.
* Commands that merely *use* an existing identity (`aws s3 sync`,
  `gcloud storage cp`).

A job that federates to one cloud and keeps a stored key for another is still
flagged for the second — the credential still on the shelf is the one worth
naming.

## Auto-fix

**Partial, and comment-only.** The replacement needs a role ARN, a
workload-identity provider, or a federated app registration — none of which
exist in the file and none of which can be guessed. A placeholder would parse
and then fail at deploy time, which is worse than the credential it replaced. So
the fix annotates the exact line with the provider's replacement and leaves the
pipeline working:

```yaml theme={null}
- # pipefort: static cloud credential — Static AWS credential used where OIDC federation is available
  # Grant the job `permissions: id-token: write` and use `aws-actions/configure-aws-credentials`
  # with `role-to-assume:` ... Then delete the stored key.
  uses: aws-actions/configure-aws-credentials@v4
```

Running the fix twice does not stack a second comment.

## Related rules

* [CICD-SEC-2 — Long-lived personal access token](/rules/cicd-sec-2)
* [CICD-SEC-2 — Package published with a long-lived token instead of OIDC](/rules/cicd-sec-2-use-trusted-publishing)
* [CICD-SEC-6 — Hardcoded credentials](/rules/cicd-sec-6)
* [SLSA-BUILD-L2 — Provenance/signing step missing `id-token: write`](/rules/slsa-build-l2-oidc-token-scope)
