Secretless workflows with AWS and GitHub
In today’s cloud-native development landscape, managing secrets securely is a critical challenge. While storing credentials as GitHub secrets or environment variables is common, it introduces security risks and maintenance overhead. What if we could eliminate the need for stored credentials entirely? Enter OpenID Connect (OIDC) - a game-changing approach to implementing truly secretless workflows.
The Problem with Traditional Approaches
Traditionally, when setting up CI/CD pipelines or development environments that interact with AWS, we’ve relied on long-lived credentials:
- AWS access keys stored as GitHub secrets
- Environment variables in Codespaces
- Manually configured AWS profiles
These approaches come with several drawbacks:
- Security Risks: Long-lived credentials are attractive targets for attackers
- Maintenance Overhead: Regular rotation of credentials requires ongoing attention
- Access Control Challenges: Granular permissions become difficult to manage
- Compliance Concerns: Stored credentials may violate security requirements
Enter OIDC: A Better Way
OpenID Connect (OIDC) allows GitHub Actions and Codespaces to obtain short-lived AWS credentials on-demand, without storing any secrets. Here’s how it works:
- GitHub’s OIDC provider issues a token that proves the identity of your workflow
- AWS validates this token using a trust relationship
- Temporary credentials are issued based on the configured IAM role
- Your workflow uses these credentials to access AWS resources
The best part? This entire process is automatic and requires zero stored credentials!
Implementation with Terraform
Let’s walk through implementing this secretless setup using Terraform. Our configuration will create:
- An OIDC provider in AWS for GitHub
- Separate IAM roles for GitHub Actions and Codespaces
- Custom IAM policies with fine-grained permissions
The Core Infrastructure
First, we create the OIDC provider and IAM roles:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-oidc-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub": "repo:${var.github_org}/${var.github_repo}:*"
}
}
}]
})
}
Configuring GitHub Actions
In your GitHub Actions workflow:
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-oidc-role
aws-region: us-east-1
Setting Up Codespaces with User-Based Access Control
When working with Codespaces, it’s crucial to consider who the user is and what permissions they should have. We can implement fine-grained access control based on GitHub usernames:
- First, create a restricted role in Terraform that checks the user identity:
variable "allowed_users" {
description = "List of allowed GitHub usernames"
type = list(string)
default = []
}
resource "aws_iam_role" "github_codespaces_restricted" {
name = "github-codespaces-restricted-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub": [for user in var.allowed_users : "user:${user}"]
}
StringEquals = {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
})
}
- Configure your devcontainer.json:
{
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {}
},
"customizations": {
"codespaces": {
"repositories": {
"*": {
"permissions": {
"id-token": "write"
}
}
}
}
}
}
Team-Based Access Control with GitHub and AWS
One of the most powerful features of this setup is the ability to map GitHub teams to specific AWS roles. This allows for granular access control based on both team membership and repository context.
Here’s how to implement team-based access control:
- Define Team-Role Mappings in Terraform:
team_role_mappings = [
{
team_slug = "devops"
repository = "*" # Access to all repositories
role_name = "github-devops-admin"
aws_managed_policies = ["arn:aws:iam::aws:policy/AdministratorAccess"]
max_session_duration = 3600
},
{
team_slug = "developers"
repository = "api-service"
role_name = "github-api-developer"
aws_managed_policies = [
"arn:aws:iam::aws:policy/AWSLambda_FullAccess",
"arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
]
max_session_duration = 3600
}
]
- Create Dynamic IAM Roles:
resource "aws_iam_role" "github_team_roles" {
for_each = {
for mapping in var.team_role_mappings : "${mapping.team_slug}-${mapping.repository}" => mapping
}
name = each.value.role_name
assume_role_policy = jsonencode({
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub": [
"repo:${var.github_org}/${each.value.repository}:*",
"team:${var.github_org}/${each.value.team_slug}"
]
}
}
}
]
})
}
Authenticating with AWS CLI in Codespaces
Setting up AWS CLI authentication in Codespaces requires a few key steps:
- Configure the Devcontainer:
First, ensure your
.devcontainer/devcontainer.json
includes the AWS CLI and necessary permissions:
{
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {}
},
"customizations": {
"codespaces": {
"repositories": {
"*": {
"permissions": {
"id-token": "write",
"contents": "read"
}
}
}
}
}
}
- Create a Configuration Script:
Create a script in your repository (e.g.,
.devcontainer/configure-aws-credentials.sh
):
#!/bin/bash
# Get GitHub OIDC token
GITHUB_TOKEN=$(curl -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/json" "https://api.github.com/codespaces/token" | jq -r .value)
# Configure AWS CLI to use OIDC
aws configure set credential_process "aws-credential-helper.sh"
# Create the credential helper script
cat > ~/.local/bin/aws-credential-helper.sh << 'EOF'
#!/bin/bash
# Get temporary credentials using the GitHub token
CREDENTIALS=$(curl -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/json" \
"https://token.actions.githubusercontent.com/.well-known/openid-configuration" | \
aws sts assume-role-with-web-identity \
--role-arn $AWS_ROLE_ARN \
--role-session-name "GitHubCodespace" \
--web-identity-token - )
# Output credentials in the format AWS CLI expects
echo "{
\"Version\": 1,
\"AccessKeyId\": \"$(echo $CREDENTIALS | jq -r '.Credentials.AccessKeyId')\",
\"SecretAccessKey\": \"$(echo $CREDENTIALS | jq -r '.Credentials.SecretAccessKey')\",
\"SessionToken\": \"$(echo $CREDENTIALS | jq -r '.Credentials.SessionToken')\",
\"Expiration\": \"$(echo $CREDENTIALS | jq -r '.Credentials.Expiration')\"
}"
EOF
chmod +x ~/.local/bin/aws-credential-helper.sh
- Update Repository Settings: In your GitHub repository settings, under “Codespaces”, add these environment variables:
AWS_ROLE_ARN
: The ARN of your OIDC roleAWS_REGION
: Your default AWS region
- Usage in Codespaces: Once configured, you can use AWS CLI commands normally:
# List S3 buckets
aws s3 ls
# Describe EC2 instances
aws ec2 describe-instances
# Check assumed identity
aws sts get-caller-identity
- Troubleshooting Common Issues:
- Permission Denied: Ensure your role’s trust policy includes the correct GitHub team/user
aws sts get-caller-identity
# If this fails, check your role trust policy
- Token Expiration: Credentials automatically refresh, but you might need to re-run the configuration if you see:
An error occurred (ExpiredToken) when calling the Operation
- Role Not Found: Verify your AWS_ROLE_ARN environment variable:
echo $AWS_ROLE_ARN
# Should show: arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME
-
Team Hierarchy:
- Map senior teams to roles with broader access
- Assign project-specific roles to feature teams
- Create cross-cutting roles for shared services teams
-
Repository-Based Access:
- Grant repository-specific permissions using
repository
parameter - Use wildcards (
*
) for teams that need org-wide access - Create separate roles for production vs. development repositories
- Grant repository-specific permissions using
-
Role Duration and Permissions:
- Set appropriate
max_session_duration
based on work patterns - Use AWS managed policies for common permission sets
- Create custom policies for specific team needs
- Set appropriate
-
Audit and Compliance:
- Track team membership changes in GitHub
- Monitor role assumptions in CloudTrail
- Set up alerts for unauthorized access attempts
-
Best Practices:
# Example of granular team-repository mapping team_role_mappings = [ { team_slug = "platform-team" repository = "infrastructure" role_name = "platform-infra-admin" aws_managed_policies = ["arn:aws:iam::aws:policy/AdministratorAccess"] max_session_duration = 3600 }, { team_slug = "frontend-developers" repository = "web-app" role_name = "frontend-developer" aws_managed_policies = ["arn:aws:iam::aws:policy/AWSCloudFrontFullAccess"] max_session_duration = 7200 } ]
Security Benefits
This approach offers several key security advantages:
- Zero Stored Secrets: No credentials to manage or rotate
- Short-lived Credentials: Temporary access tokens reduce risk
- Fine-grained Control: Permissions can be limited by repository, branch, or environment
- Audit Trail: All access is logged in AWS CloudTrail
- Compliance Friendly: Meets security requirements for credential management
Best Practices
When implementing this solution:
- Limit Role Permissions: Follow the principle of least privilege
- Use Branch Protection: Restrict who can modify the OIDC configuration
- Monitor Usage: Regular review of CloudTrail logs
- Version Control: Keep your Terraform configuration in version control
- Documentation: Maintain clear documentation for team members
Conclusion
Implementing secretless workflows with GitHub and AWS using OIDC is a powerful approach that enhances security while reducing operational overhead. By eliminating the need for stored credentials, you can focus on building and deploying applications without worrying about secret management.
The complete Terraform configuration and additional resources are available in the accompanying GitHub repository. Give it a try in your next project and experience the benefits of truly secretless development workflows!