AWS Secrets in CI/CD Pipelines: Secrets Manager vs Parameter Store vs OIDC

Every CI/CD pipeline needs credentials: AWS authentication, database passwords for integration tests, API keys for third-party services. The wrong approach (hardcoding in config files, storing in git, printing to logs) turns your pipeline into a fast path to account compromise. AWS gives you three tools to handle this correctly: Secrets Manager, Systems Manager Parameter Store, and OIDC. Knowing which to use, and when, is the core of this guide.

The short version: use OIDC for AWS authentication from GitHub Actions, Secrets Manager or Parameter Store for build-time secrets in CodeBuild, and ECS task definition references for runtime secrets. Never pass runtime secrets through the pipeline.

This page covers each approach with working examples, explains the build-time versus runtime distinction clearly, and points out the patterns that look reasonable but quietly cause breaches.

Simple explanation

A “secret” in CI/CD is any value that grants access to something: a database password, an API key, an AWS credential. Pipelines commonly need secrets because they run real operations — integration tests that hit a database, deployments that push container images, notifications that call third-party APIs.

The insecure answer is to put the secret directly in your code or config file. Once it is in git, it is effectively public. Even if the repo is private, anyone with access can read it, and git history is permanent. The secure answer is to store the secret in a managed service outside the repo, give the pipeline permission to read it, and inject it only when needed.

OIDC is a slightly different concept. Instead of storing a secret that proves who you are, OIDC lets GitHub Actions prove its identity to AWS using a short-lived token. Think of it like showing a photo ID at the door rather than handing over a permanent key. AWS issues temporary credentials that expire when the job ends. There is no credential to store, rotate, or accidentally commit. For AWS authentication from GitHub Actions, OIDC is always the right choice.

Quick answer

If you are looking for a fast decision, this table covers the most common scenarios:

ScenarioWhat to use
GitHub Actions needs AWS accessOIDC (no stored keys)
CodeBuild needs a DB password for integration testsSecrets Manager or Parameter Store, declared in buildspec
App needs DB credentials at runtime in ECSsecrets.valueFrom in the task definition (not through the pipeline)
Non-sensitive config values (hostnames, region names)Parameter Store String or plain environment variable
Secrets that need automatic rotationSecrets Manager
API keys for third-party services that do not support OIDCParameter Store SecureString or Secrets Manager

Still unsure which secret store to choose? Jump to the Secrets Manager vs Parameter Store comparison table below.

How secrets in CI/CD should work

The correct pattern for every secret in every pipeline follows the same five steps:

  1. Store the value in a managed service. Secrets Manager, Parameter Store, or use OIDC so there is no stored value at all.
  2. Grant least-privilege read access to the build role. The pipeline’s IAM role gets permission to read exactly the secrets it needs, scoped to specific ARNs. See least-privilege IAM for how to scope policies correctly.
  3. Inject only when needed. Declare secrets in buildspec.yml for build-time use. For runtime secrets, reference them in the ECS task definition and let ECS inject them at startup.
  4. Avoid logging or exporting secrets. Remove any env, printenv, or set commands from your buildspec before merging.
  5. Audit access and rotate when required. Every secret access is logged to CloudTrail. Set up alerts for unexpected access and rotate on schedule or immediately after any exposure.

When to use each option

Use AWS Secrets Manager when

  • The secret needs automatic rotation. Secrets Manager has native rotation support for RDS, Redshift, DocumentDB, and custom Lambda-based rotation.
  • You need fine-grained resource-based access policies per secret.
  • You need cross-account secret sharing.
  • The secret is a database password for integration tests in CodeBuild.
  • Compliance requirements demand audit trails per-secret rather than per-parameter.

Cost: $0.40 per secret per month, plus $0.05 per 10,000 API calls.

Use Systems Manager Parameter Store when

  • The secret does not need rotation: third-party API keys, Datadog agent tokens, webhook URLs.
  • You want free storage (standard tier, non-SecureString).
  • The value is non-sensitive config that you want to manage centrally: hostnames, region names, feature flags.
  • You are already using SecureString parameters encrypted with KMS and do not need Secrets Manager’s extra features.

Cost: free for standard parameters. Advanced parameters cost $0.05 per parameter per month.

Use OIDC when

  • GitHub Actions, GitLab CI, or any OIDC-capable CI system needs to authenticate to AWS.
  • You want to eliminate long-lived AWS access keys from your CI configuration entirely.
  • You want credentials that expire automatically when the job ends.

What OIDC is not OIDC is not a secret store. It is an authentication mechanism. It does not replace Secrets Manager for application credentials; it replaces static AWS access keys for CI pipeline authentication. You still need Secrets Manager or Parameter Store for database passwords, API keys, and third-party tokens.

Do not pass runtime secrets through the pipeline when

  • The secret is needed by the deployed application, not by the build itself.
  • The value is a production database password, production API key, or any credential the running container needs.
  • You are tempted to set an environment variable in your pipeline and pass it into the Docker build or bake it into the image.

Runtime secrets belong in the ECS task definition or the application’s startup logic, not in the pipeline. See the runtime injection section below.

Secrets Manager vs Parameter Store vs OIDC

Secrets ManagerParameter StoreOIDC
Stores a secretYesYesNo (eliminates the need for stored AWS credentials)
Best use caseDB passwords, rotating credentialsAPI keys, config valuesAWS auth from GitHub Actions / GitLab CI
Automatic rotationNative supportNot built-inN/A
Good for AWS auth in CINot applicableNot applicableYes, this is its purpose
Good for app/runtime secretsYesYes (SecureString)No
Cost$0.40/secret/monthFree (standard tier)Free
Beginner recommendationStart here for DB credentialsStart here for API keys and configAlways use for GitHub Actions AWS auth

Build-time secrets vs runtime secrets

This is one of the most important distinctions on this page, and one of the most commonly misunderstood.

Build-time secrets are values the pipeline needs while it runs: a database password for integration tests, a Datadog API key to post build metrics, a token to pull a private package. These exist during the build phase and should never be baked into the container image or artifact. CodeBuild’s native Secrets Manager and Parameter Store integration handles these cleanly.

Runtime secrets are values the deployed application needs while running in production: a production database password, a Stripe API key, a Redis connection string. These should never touch the CI/CD pipeline. The pipeline’s job is to build and deploy the container. The container should fetch its own secrets when it starts.

Think of it this way The pipeline is a delivery van. It drops off the container image at the destination. The container is the building. The building’s keys (runtime secrets) should not ride in the van — they should already be waiting at the destination. If the van is hijacked, the building stays locked.

A common mistake is fetching a runtime secret during the build, writing it to a config file inside the image, and then deploying that image. Now the secret is baked into every layer of the container image, stored in ECR, and accessible to anyone who can pull the image. Use ECS task definition secret references instead. See the runtime injection section.

Unsafe patterns to avoid

These patterns appear in tutorials and legacy codebases. Recognising them is half the fix.

Hardcoding in buildspec.yml This is the worst option. The value is committed to git, visible to everyone with repo access, and appears in plain text in CloudWatch Logs.

# Never do this
phases:
  build:
    commands:
      - export DB_PASSWORD=mypassword123
      - pytest tests/integration/

Plaintext CodeBuild environment variable Visible in the AWS console to anyone with CodeBuild read access. Also visible in build logs if any command prints environment variables.

aws codebuild update-project \
  --name my-project \
  --environment '{"environmentVariables": [{"name": "DB_PASSWORD", "value": "mypassword123", "type": "PLAINTEXT"}]}'

Logging environment variables in buildspec These commands dump every environment variable to CloudWatch Logs, including any secrets that were injected correctly. They are usually added for debugging and forgotten. Search buildspec files for them before merging.

phases:
  build:
    commands:
      - env
      - printenv
      - set

Storing long-lived AWS access keys in GitHub Secrets Long-lived access keys are dangerous: they do not expire, they can be copied, and if they appear in a log file they are permanently compromised. Use OIDC instead. See access keys explained for the full picture.

# Before OIDC — risky, credentials never expire on their own
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Using Secrets Manager with CodeBuild

CodeBuild can fetch secrets from Secrets Manager and inject them as environment variables at build start. The secret value is never stored in the CodeBuild project; only the ARN reference is. Declare the mapping in buildspec.yml:

version: 0.2

env:
  secrets-manager:
    # Format: VARIABLE_NAME: secret-arn:json-key
    DB_PASSWORD: arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db:password
    DB_USERNAME: arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db:username
    STRIPE_API_KEY: arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/stripe:api_key

phases:
  build:
    commands:
      # $DB_PASSWORD, $DB_USERNAME, and $STRIPE_API_KEY are available here
      # Values are masked in CloudWatch Logs automatically
      - pytest tests/integration/ --db-url "postgresql://$DB_USERNAME:$DB_PASSWORD@$DB_HOST/testdb"

The CodeBuild service role needs secretsmanager:GetSecretValue on the secret ARNs. Secrets Manager appends a random six-character suffix to every secret name (for example, prod/myapp/db-AbCdEf), so you cannot match the exact ARN without knowing that suffix. The recommended approach is a tightly scoped prefix pattern:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": [
        "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db-*",
        "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/stripe-*"
      ]
    }
  ]
}

Why the -* suffix? The prefix pattern (e.g., prod/myapp/db-*) accounts for Secrets Manager’s auto-appended random suffix. This is more permissive than listing the exact ARN, but scoped tightly to your namespace, not a wildcard across the whole account. If you know the exact suffix, list the full ARN for tighter control.

Automatic log masking CodeBuild automatically masks secret values in CloudWatch Logs. If a build command tries to print a secret, CloudWatch replaces the value with ****. This is a useful safety net, but it is not a substitute for removing env and printenv from your buildspec.

Using Parameter Store with CodeBuild

Parameter Store is a good fit for API keys that do not need rotation and for non-sensitive configuration values you want to manage centrally. SecureString parameters are encrypted with KMS and suitable for sensitive values. String parameters are unencrypted and appropriate for hostnames, region names, and feature flags.

A useful way to think about it: Parameter Store is a centralised config file that your whole AWS account can read from, with optional encryption. Secrets Manager is a vault with an audit log, rotation machinery, and a per-secret price tag.

version: 0.2

env:
  parameter-store:
    # Non-sensitive config stored as String type
    DB_HOST: /prod/myapp/db-host
    REDIS_HOST: /prod/myapp/redis-host
    # Sensitive config stored as SecureString type
    DATADOG_API_KEY: /prod/myapp/datadog-api-key

phases:
  build:
    commands:
      - echo "Connecting to database at $DB_HOST"
      - ./run-tests.sh
# Store a plain config value
aws ssm put-parameter \
  --name "/prod/myapp/db-host" \
  --value "myapp-prod.cluster-abc123.us-east-1.rds.amazonaws.com" \
  --type String

# Store a sensitive value encrypted with KMS
aws ssm put-parameter \
  --name "/prod/myapp/datadog-api-key" \
  --value "dd-api-key-value-here" \
  --type SecureString \
  --key-id alias/myapp-parameter-key

The CodeBuild service role needs ssm:GetParameters on the parameter ARNs. For SecureString parameters, it also needs kms:Decrypt on the KMS key. If you use customer-managed keys, make sure the CodeBuild role is listed in the key policy.

Using OIDC for GitHub Actions

For AWS authentication from GitHub Actions, OIDC is the right answer, not a stored access key. GitHub Actions requests a short-lived JWT from GitHub’s OIDC provider and presents it to AWS. AWS verifies it and issues temporary STS credentials that expire when the job ends.

There is no credential to store, rotate, or accidentally commit. If a job is compromised, the temporary credentials expire within minutes to an hour. A long-lived key would never expire on its own.

Step 1: Create the OIDC provider in AWS (one-time setup):

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

Step 2: Create an IAM role with a trust policy scoped to your repository:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

Always scope the trust policy The sub condition locks the role to a specific organisation, repository, and branch. Without it, any GitHub Actions workflow, including forks, could assume your role. See role assumption in AWS for more on trust policy conditions.

Step 3: Use the role in your workflow:

# .github/workflows/deploy.yml
permissions:
  id-token: write   # Required to request the OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          # No access keys required — OIDC handles authentication

For a complete OIDC setup guide including scoping to specific branches and handling multiple environments, see GitHub Actions for AWS deployments.

Runtime secret injection for ECS

Runtime secrets (database passwords, API keys, connection strings the application needs while running) should never touch the pipeline. The pipeline’s job ends when the container image is deployed. The container fetches its own secrets when it starts.

For ECS containers, define secrets in the task definition using the secrets field with valueFrom. ECS fetches the values from Secrets Manager or Parameter Store when the container starts and injects them as environment variables. The pipeline never sees these values:

{
  "containerDefinitions": [
    {
      "name": "my-app",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db:password::"
        },
        {
          "name": "DB_USERNAME",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db:username::"
        },
        {
          "name": "DB_HOST",
          "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/myapp/db-host"
        }
      ],
      "environment": [
        {
          "name": "APP_ENV",
          "value": "production"
        }
      ]
    }
  ]
}

secrets vs environment in ECS — easy mistake The secrets field resolves values from Secrets Manager or Parameter Store at task startup. The environment field passes literal string values as-is. Placing a Parameter Store path like /prod/myapp/db-host in environment.value sets the variable to that literal string; ECS will not resolve it. Always use secrets.valueFrom for anything that needs to be fetched at runtime.

The ECS task execution role (not the task role) needs secretsmanager:GetSecretValue and ssm:GetParameters to fetch these values at container startup. If secrets are encrypted with a customer-managed KMS key, the execution role also needs kms:Decrypt.

Rotation does not update running tasks Because ECS injects secrets at task startup, rotating a secret in Secrets Manager does not update running containers. The running container still holds the old value. To pick up the rotated secret, stop and restart the task, typically by triggering a new deployment. Plan for this when designing your rotation strategy.

Audit logging and incident response

Every call to secretsmanager:GetSecretValue and ssm:GetParameters is logged to CloudTrail. You can query these logs to see which IAM principal accessed which secret, when, and from where. If a secret is compromised, CloudTrail gives you the timeline: who accessed it, from which IP, from which service.

# Find all Secrets Manager access events in the last 24 hours
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
  --start-time $(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ') \
  --query 'Events[*].{Time:EventTime,User:Username}' \
  --output table

Set up CloudWatch Alarms or EventBridge rules to alert when production secrets are accessed from unexpected principals. For example: an SNS notification if the production database password is accessed by any role other than the CodeBuild service role or ECS task execution role. This is what turns a potential breach into a 15-minute incident instead of a 3-month one. See securing production systems for broader incident response patterns.

Common mistakes

  1. Storing long-lived AWS keys as GitHub Secrets or CodeBuild plaintext variables. Long-lived access keys in CI are the most common cause of AWS credential exposure. For GitHub Actions, use OIDC. For CodeBuild, the service role handles AWS authentication; no static keys needed.

  2. Using a wildcard resource in the Secrets Manager IAM policy. “Resource”: ”*” on secretsmanager:GetSecretValue gives the pipeline access to every secret in the account. Always scope to specific ARNs or a tightly-namespaced prefix pattern. See least-privilege IAM.

  3. Logging environment variables in buildspec.yml. env, printenv, or set in any buildspec phase writes all environment variables, including injected secrets, to CloudWatch Logs. Remove these before merging.

  4. Passing runtime secrets through the pipeline. If the application needs a secret at runtime, it should fetch it at startup, not receive it from the pipeline. Secrets baked into the container image or passed as build arguments end up in image layers and are visible to anyone who can pull the image.

  5. Placing a Parameter Store path in an ECS environment.value field. ECS does not resolve parameter paths in the environment field. Use secrets.valueFrom with a full ARN or parameter name for ECS to fetch and inject the value.

  6. Forgetting that ECS does not auto-apply rotated secrets. After rotating a secret, running tasks still hold the old value. Trigger a new deployment or task restart to pick up the rotated value.

  7. Not planning secret rotation after a pipeline breach. If a pipeline is compromised, rotate every secret it could access immediately, not when it is convenient. Know how to rotate each secret before you need to do it under pressure.

Frequently asked questions

Should I use Secrets Manager or Parameter Store for CI/CD secrets?

Use Secrets Manager for credentials that need automatic rotation: database passwords, OAuth tokens, API keys for critical services. It costs $0.40/secret/month but handles rotation natively. Use Parameter Store for values that change rarely, like third-party API keys or config values. Standard-tier parameters are free. If you are unsure, start with Parameter Store SecureString. Add Secrets Manager when you need rotation or cross-account sharing.

Do I still need GitHub Secrets if I use OIDC for AWS authentication?

Not for AWS credentials. OIDC replaces AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY entirely: no stored key, nothing to rotate. You may still need GitHub Secrets for third-party service tokens (Datadog, NPM publish tokens, Slack webhooks) that do not support OIDC. Those can live in GitHub Secrets or be fetched from Secrets Manager during the build.

Should runtime secrets ever go through the CI/CD pipeline?

No. Runtime secrets, the values the deployed application needs while running, should be injected at container startup, not passed through the pipeline. For ECS, use the secrets field in the task definition with valueFrom pointing to Secrets Manager or Parameter Store. The pipeline builds and deploys the container; the container fetches its own secrets when it starts.

Does ECS automatically pick up rotated secrets?

No. ECS injects secrets when a task starts. If a secret is rotated in Secrets Manager, running containers still use the old value. To pick up the new value, stop and restart the task or trigger a new deployment. Rotation alone does not propagate to running workloads; plan for task restarts as part of your rotation process.

What IAM permissions does CodeBuild need to read secrets?

For Secrets Manager: secretsmanager:GetSecretValue on the specific secret ARNs. For Parameter Store: ssm:GetParameters on the parameter ARNs. For SecureString parameters, also kms:Decrypt on the KMS key used to encrypt them. Always scope permissions to specific ARNs; never use a wildcard resource for secrets access.

Last verified: 25 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.