Created
July 3, 2026 12:54
-
-
Save mohashari/146e50a5d8989c5f8eeee8c7c80449dd to your computer and use it in GitHub Desktop.
Preventing Credential Leakage: Automated Just-In-Time AWS IAM Session Token Generation in GitHub Actions using OIDC and Vault — code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| resource "vault_jwt_auth_backend" "github" { | |
| description = "JWT authentication backend for GitHub Actions" | |
| path = "github-actions" | |
| oidc_discovery_url = "https://token.actions.githubusercontent.com" | |
| bound_issuer = "https://token.actions.githubusercontent.com" | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| resource "vault_jwt_auth_backend_role" "production_deploy" { | |
| backend = vault_jwt_auth_backend.github.path | |
| role_name = "production-deploy-role" | |
| token_policies = ["production-deploy-policy"] | |
| bound_claims = { | |
| repository = "my-organization/production-app" | |
| ref = "refs/heads/main" | |
| } | |
| user_claim = "actor" | |
| role_type = "jwt" | |
| token_ttl = 900 # Token expires in 15 minutes | |
| token_max_ttl = 1800 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| resource "vault_aws_secret_backend" "aws" { | |
| path = "aws-production" | |
| region = "us-east-1" | |
| use_iam_server_credentials = true | |
| default_lease_ttl_seconds = 900 # 15 minutes | |
| max_lease_ttl_seconds = 3600 # 1 hour | |
| } | |
| resource "vault_aws_secret_backend_role" "deploy_role" { | |
| backend = vault_aws_secret_backend.aws.path | |
| name = "app-deploy-role" | |
| credential_type = "assumed_role" | |
| role_arns = [ | |
| "arn:aws:iam::123456789012:role/GithubActionsDeploymentRole" | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Effect": "Allow", | |
| "Principal": { | |
| "AWS": "arn:aws:iam::123456789012:role/VaultServerIAMRole" | |
| }, | |
| "Action": "sts:AssumeRole", | |
| "Condition": { | |
| "StringEquals": { | |
| "sts:ExternalId": "vault-production-cluster-token" | |
| } | |
| } | |
| } | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Sid": "DeploymentBoundary", | |
| "Effect": "Allow", | |
| "Action": [ | |
| "ec2:Describe*", | |
| "ecs:UpdateService", | |
| "ecs:DescribeServices", | |
| "s3:PutObject", | |
| "s3:GetObject", | |
| "s3:ListBucket" | |
| ], | |
| "Resource": "*" | |
| }, | |
| { | |
| "Sid": "DenyIAMChanges", | |
| "Effect": "Deny", | |
| "Action": [ | |
| "iam:*", | |
| "organizations:*" | |
| ], | |
| "Resource": "*" | |
| } | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Production Deployment | |
| on: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| id-token: write | |
| contents: read | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v4 | |
| - name: Retrieve OIDC Token and Query Vault | |
| id: vault-auth | |
| run: | | |
| # 1. Retrieve the OIDC token from the local runner environment | |
| JWT_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://vault.corp.internal" | jq -r '.value') | |
| if [ -z "$JWT_TOKEN" ] || [ "$JWT_TOKEN" = "null" ]; then | |
| echo "::error::Failed to fetch OIDC JWT from GitHub." | |
| exit 1 | |
| fi | |
| # 2. Authenticate with Vault using the OIDC JWT | |
| VAULT_RESP=$(curl -sS --request POST \ | |
| --data "{\"jwt\":\"$JWT_TOKEN\",\"role\":\"production-deploy-role\"}" \ | |
| "https://vault.corp.internal/v1/auth/github-actions/login") | |
| VAULT_TOKEN=$(echo "$VAULT_RESP" | jq -r '.auth.client_token') | |
| if [ -z "$VAULT_TOKEN" ] || [ "$VAULT_TOKEN" = "null" ]; then | |
| echo "::error::Vault OIDC authentication failed." | |
| exit 1 | |
| fi | |
| # 3. Retrieve short-lived AWS Credentials from the Secrets Engine | |
| AWS_CREDS=$(curl -sS -H "X-Vault-Token: $VAULT_TOKEN" \ | |
| "https://vault.corp.internal/v1/aws-production/creds/app-deploy-role") | |
| # 4. Extract credentials and mark them as masked secrets in GitHub runner | |
| AWS_ACCESS_KEY_ID=$(echo "$AWS_CREDS" | jq -r '.data.access_key') | |
| AWS_SECRET_ACCESS_KEY=$(echo "$AWS_CREDS" | jq -r '.data.secret_key') | |
| AWS_SESSION_TOKEN=$(echo "$AWS_CREDS" | jq -r '.data.security_token') | |
| echo "::add-mask::$AWS_ACCESS_KEY_ID" | |
| echo "::add-mask::$AWS_SECRET_ACCESS_KEY" | |
| echo "::add-mask::$AWS_SESSION_TOKEN" | |
| echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" >> $GITHUB_ENV | |
| echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" >> $GITHUB_ENV | |
| echo "AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN" >> $GITHUB_ENV | |
| - name: Deploy to Amazon ECS | |
| run: | | |
| aws ecs update-service \ | |
| --cluster production-cluster \ | |
| --service web-app \ | |
| --force-new-deployment \ | |
| --region us-east-1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import boto3 | |
| import sys | |
| from datetime import datetime, timezone | |
| def audit_session_expiry(): | |
| try: | |
| sts_client = boto3.client('sts') | |
| # Call GetCallerIdentity to ensure active session is valid | |
| identity = sts_client.get_caller_identity() | |
| print(f"Authenticated as: {identity['Arn']}") | |
| # Query IAM to check the session creation parameters | |
| # Dynamic assumed roles carry a session name we can parse | |
| role_arn = identity['Arn'] | |
| session_name = role_arn.split('/')[-1] | |
| # Verify the session is short-lived by parsing the STS token structure | |
| # (This check verifies credentials do not belong to static IAM Users) | |
| if ":assumed-role/" not in role_arn: | |
| print("[CRITICAL] Non-assumed role detected in CI pipeline! Static keys are in use.") | |
| sys.exit(1) | |
| print("[SUCCESS] Just-in-Time OIDC verification passed.") | |
| except Exception as e: | |
| print(f"[ERROR] Failed to validate credentials: {str(e)}") | |
| sys.exit(2) | |
| if __name__ == "__main__": | |
| audit_session_expiry() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment