Secretless workflows with AWS and GitHub

Shahar Mintz
DevOps Security Secretless AWS GitHub GitHub Actions GitHub Codespaces

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:

  1. Security Risks: Long-lived credentials are attractive targets for attackers
  2. Maintenance Overhead: Regular rotation of credentials requires ongoing attention
  3. Access Control Challenges: Granular permissions become difficult to manage
  4. 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:

  1. GitHub’s OIDC provider issues a token that proves the identity of your workflow
  2. AWS validates this token using a trust relationship
  3. Temporary credentials are issued based on the configured IAM role
  4. 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:

  1. An OIDC provider in AWS for GitHub
  2. Separate IAM roles for GitHub Actions and Codespaces
  3. 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:

  1. 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"
          }
        }
      }
    ]
  })
}
  1. 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:

  1. 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
  }
]
  1. 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:

  1. 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"
          }
        }
      }
    }
  }
}
  1. 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
  1. Update Repository Settings: In your GitHub repository settings, under “Codespaces”, add these environment variables:
  • AWS_ROLE_ARN: The ARN of your OIDC role
  • AWS_REGION: Your default AWS region
  1. 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
  1. 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
  1. 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
  2. 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
  3. 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
  4. Audit and Compliance:

    • Track team membership changes in GitHub
    • Monitor role assumptions in CloudTrail
    • Set up alerts for unauthorized access attempts
  5. 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:

  1. Zero Stored Secrets: No credentials to manage or rotate
  2. Short-lived Credentials: Temporary access tokens reduce risk
  3. Fine-grained Control: Permissions can be limited by repository, branch, or environment
  4. Audit Trail: All access is logged in AWS CloudTrail
  5. Compliance Friendly: Meets security requirements for credential management

Best Practices

When implementing this solution:

  1. Limit Role Permissions: Follow the principle of least privilege
  2. Use Branch Protection: Restrict who can modify the OIDC configuration
  3. Monitor Usage: Regular review of CloudTrail logs
  4. Version Control: Keep your Terraform configuration in version control
  5. 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!


Additional Resources