Git, CI/CD & GitOpsXLII · CI SecretsSecrets
The short-lived credential ideal — OIDC, dynamic secrets, and the end of long-lived keys
What you'll learn
- State the lifetime principle: a credential lifetime must be no longer than the workload lifetime
- Describe how OIDC federation issues per-job, short-lived tokens scoped by repository, branch, and workflow path
- Describe how Vault-issued dynamic secrets replace long-lived database and cloud credentials with leases that expire
- Recognise when long-lived credentials remain necessary and how to minimise their blast radius in those cases
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A long-lived CI credential is a structural smell. The smell is not that long-lived credentials exist; the smell is that long-lived credentials are the default, the easy option, the pattern every team reaches for first. The default is wrong because the default has a lifetime that exceeds the workload’s lifetime by orders of magnitude. A 90-day IAM key on a runner that hosts 5-minute jobs is a key whose lifetime is 25,000 times longer than the workload’s. The structural fix is short-lived credentials issued per job, scoped to the job, and dead when the job ends.
The lifetime principle
The principle that ties every secret model in this course together: a credential’s lifetime must be no longer than the workload’s lifetime.
flowchart LR
WORK["Workload lifetime\n(minutes)"] --> CRED["Credential lifetime\n(must be ≤ workload)"]
CRED --> LONG["Long-lived\n(days, months, years)"]
CRED --> SHORT["Short-lived\n(minutes, hours)"]
CRED --> DYNAMIC["Dynamic\n(per workload)"]
Three families of credentials, ranked by lifetime:
- Long-lived. IAM user access keys, service account JSON files, personal access tokens. The credential outlives the workload that uses it; the credential outlives the runner that hosts the workload; the credential outlives the engineer who issued it. The lifetime is the team’s rotation cadence, not the workload’s.
- Short-lived. STS session tokens, JWT tokens with minutes-to-hours expiry, time-bound service account tokens. The credential’s lifetime is bounded; the credential is renewed per workload.
- Dynamic. OIDC-issued STS sessions, Vault-issued database credentials, Vault-issued cloud credentials. The credential’s lifetime is the workload’s lifetime; the credential is issued at job start and dies at job end.
The structural improvement is moving from long-lived to dynamic. Short-lived is the intermediate step; dynamic is the destination.
OIDC federation
OIDC federation is the structural upgrade for cloud credentials. The forge (GitHub Actions, GitLab CI) issues a signed OIDC ID token per job. The token is bound to the workflow run, the repository, the branch, and the workflow file path. The cloud provider’s trust policy validates the token and issues a short-lived STS session token (typically valid for 1 hour).
flowchart LR
JOB["GitHub Actions job"] --> T["Forge issues\nOIDC ID token\n(signed, bound to repo/branch/workflow)"]
T --> TRUST["Cloud trust policy\nvalidates claims"]
TRUST --> STS["Cloud issues\nSTS session token\n(~1 hour)"]
STS --> JOB
JOB --> END["Job ends"]
END --> DIE["STS token dies"]
The structural property: the runner does not hold a credential. The runner holds a request for a credential. The runner’s compromise does not yield a credential because there is no credential to yield.
A GitHub Actions workflow that uses OIDC for AWS:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::"$AWS_ACCOUNT_ID":role/github-actions-deploy
aws-region: us-east-1
- name: Deploy
run: aws s3 sync ./build s3://"$BUCKET_NAME"
The cloud-side trust policy gates the role to a specific repository and branch:
{
"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",
"token.actions.githubusercontent.com:sub": "repo:acme/infra:ref:refs/heads/main"
}
}
}
]
}
A workflow on a different repo or branch cannot assume the role. The STS token is valid for one hour; the job uses it; the job ends; the token dies.
Vault dynamic secrets
HashiCorp Vault issues dynamic credentials for databases, cloud providers, and PKI. The credential is a Vault lease: a token bound to a TTL, renewable on demand, revocable at expiry. A Vault-issued database credential is a username and password pair that Vault generates per CI job, returns to the runner, and revokes when the lease expires.
flowchart LR
JOB["CI job"] --> V["Vault: issue\ndynamic DB credential\n(TTL: 1 hour)"]
V --> CREDS["Username + password\n(returned to job)"]
CREDS --> DB["Job connects\nto database"]
DB --> JOB
JOB --> END["Job ends"]
END --> LEASE["Lease expires\nVault revokes credential"]
The structural property: the credential is generated per job, scoped to the database role, and dies when the lease expires. The rotation cadence is the lease TTL; the lease TTL is set by the Vault policy; the Vault policy is auditable and version-controlled.
A CI job that uses Vault dynamic secrets:
CREDS=$(vault read -format=json database/creds/deploy-role)
USERNAME=$(echo "$CREDS" | jq -r '.data.username')
PASSWORD=$(echo "$CREDS" | jq -r '.data.password')
mysql -u "$USERNAME" -p"$PASSWORD" -h "$DB_HOST" -e "SELECT 1"
The username and password exist only for the duration of the lease. The job uses them; the job ends; the lease expires; the database credential is revoked.
When long-lived credentials remain necessary
OIDC and Vault are not universal. Several cases still require long-lived credentials:
- Cloud providers without OIDC support. A cloud provider that does not have a forge-side OIDC integration cannot use the federation pattern. The fallback is per-deploy scoped credentials with strict rotation.
- Self-hosted runners that cannot reach the OIDC endpoint. A runner in an air-gapped network cannot reach the cloud’s OIDC validation endpoint; the fallback is a per-deploy scoped credential rotated frequently.
- Third-party services that only accept API keys. A SaaS provider that does not support OIDC and does not support Vault dynamic secrets accepts only static API keys. The fallback is rotation; the cadence is the security ceiling.
In every case, the fallback’s blast radius is minimised:
- Per-deploy scoped credentials. The credential grants only the permissions the deploy needs; the credential is rotated after every deploy.
- Strict rotation. The cadence is shorter than the team would otherwise choose; the consequence of a leak is bounded by the cadence.
- Environment scope. The CI secret lives in the environment, not in the repository; the reader set is the deploy job.
The fallback is not equivalent to OIDC; the fallback is a smaller window on the same problem.
The migration path
Migrating from long-lived to dynamic is a project, not a flip. The path has four steps:
- Inventory. List every long-lived credential in every CI secret store; identify the source system for each (IAM user, vault static role, registry token); identify the consumer for each (which workflow reads which secret).
- Pilot. Pick one credential - a low-blast-radius staging credential, a single-workflow integration token - and migrate it to OIDC or Vault dynamic. Verify the pattern works end to end; verify the rollback path; document the operational differences.
- Adopt. Migrate the production credentials incrementally, one workflow at a time. Each migration removes a long-lived credential from the runner; each migration shrinks the runner’s blast radius.
- Audit. Quarterly review of remaining long-lived credentials; each remaining credential is either on the migration roadmap or justified by a documented blocker.
The migration is incremental because the migration is risky. A workflow that has run on a static key for three years has unknown dependencies on the static key; the pilot reveals the dependencies; the rollout applies the lessons.
Production discipline
- Long-lived credentials are the fallback, not the default. Every CI credential has a dynamic option; the dynamic option is the first choice; the long-lived option is the documented exception.
- OIDC for cloud credentials. AWS, GCP, Azure all support OIDC federation from GitHub Actions and GitLab CI. The cloud credential is an STS session issued per job; the static key does not exist.
- Vault dynamic secrets for database, cloud, and PKI credentials. The credential is a Vault lease with a TTL; the lease expires; the credential dies.
- Environment scope for the credentials that remain long-lived. Production credentials in the production environment; rotation cadence shorter than feels necessary.
- Migration roadmap with quarterly check-ins. The inventory is current; the pilot has happened; the rollout is in progress; the audit verifies the trajectory.
Cross-course references
- Git, CI/CD & GitOps — Part XLI-03 (Production credentials on runners) covers the structural argument for OIDC in detail.
- Git, CI/CD & GitOps — Part XXXIV-06 (The least- privilege credential) covers the Vault dynamic secrets pattern.
- AWS for Production Sysadmins — Part XXXI (IAM) covers the IAM role configuration that backs OIDC federation.
- Terraform for Production Sysadmins — Part XXII (StateSecrets) covers the analogous pattern for Terraform state credentials.
Quiz
Knowledge check · 4 questions
Q1. Which credential pattern satisfies the lifetime principle most completely?
Q2. A long-lived IAM access key rotated monthly satisfies the lifetime principle because the rotation cadence is shorter than the workload lifetime for most jobs.
Q3. State the lifetime principle and explain how OIDC federation satisfies it.
Q4. Diagnose the lifetime violation and prescribe the OIDC migration that closes the gap.
Team T runs a Terraform deploy job on a self-hosted runner pool. The runner hosts an IAM user access key in ~/.aws/credentials with AdministratorAccess on the production account. The key was issued 11 months ago and has never been rotated. The runner hosts 200 jobs per day; each job is a 5-minute Terraform plan or apply. A fork PR opens; a malicious step reads the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables and POSTs them to an attacker-controlled webhook. The attacker now holds an 11-month-old AdministratorAccess key on the production account.
Passing score: 75%. Answers are checked in this browser.