Skip to content

Instantly share code, notes, and snippets.

@mohashari
Created July 3, 2026 12:54
Show Gist options
  • Select an option

  • Save mohashari/146e50a5d8989c5f8eeee8c7c80449dd to your computer and use it in GitHub Desktop.

Select an option

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
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"
}
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
}
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"
]
}
{
"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"
}
}
}
]
}
{
"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": "*"
}
]
}
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
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