CI/CD Deployment to AWS Using Temporary Credentials via IAM Roles Anywhere

Introduction

As organizations increasingly adopt secure and scalable CI/CD pipelines, managing long-lived AWS credentials becomes a growing security concern. A powerful alternative is using temporary credentials via IAM Roles Anywhere, a service that allows external workloads to assume AWS IAM roles securely using X.509 certificates.

Why Use Temporary Credentials for CI/CD?

Traditionally, CI/CD tools like Gitlab, Bitbucket pipelines, or Azure DevOps used long-lived IAM user access keys to authenticate with AWS. But this method has security risks:

  • Access keys don’t auto-expire
  • Key rotation is manual
  • Higher risk of credentials leaks

IAM Roles Anywhere solves this by issuing temporary credentials to external environments, based on trusted X.509 certificates. It combines the security of AWS IAM roles with the flexibility to authenticate outside AWS, like in on-premises CI/CD systems or self-hosted runners.

What is IAM Roles Anywhere?

IAM Roles Anywhere allows workloads outside AWS to assume IAM roles by presenting signed X.509 certificates from a trusted Certificate Authority (CA).

Key components:

  • Trust Anchor: Represents your CA (e.g., ACM PCA or a self-managed CA)
  • Profile: Specifies which IAM roles can be assumed and what actions are allowed
  • Session: Temporary credentials (via AWS Security Token Service - STS)

Setting up CI/CD Deployment using IAM Roles Anywhere

Let’s walk through setting up a pipeline to deploy to AWS using temporary credentials.

:white_check_mark: Prerequisites

  • IAM Roles Anywhere is enabled in your AWS account
  • A trusted root CA or use AWS ACM PCA
  • Signed certificate + private key
  • CI/CD runner or server (Linux VM or Docker)

1. :locked: Generate and Sign Certificate

# generate key for CA certificate
openssl genrsa -out ca.key 2048

# generate CA certificate
openssl req -new -x509 -days 1826 -key ca.key -subj /CN=ca.example.com \
    -addext 'keyUsage=critical,keyCertSign,cRLSign,digitalSignature' \
    -addext 'basicConstraints=critical,CA:TRUE' -out ca.crt 

#generate key for leaf certificate
openssl genrsa -out private.key 2048

#request leaf certificate
cat > extensions.cnf <<EOF
[v3_ca]
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
EOF

openssl req -new -key private.key -subj /CN=cicd-agent.example.com -out iamra-cert.csr

#sign leaf certificate with CA
openssl x509 -req -days 3650 -in iamra-cert.csr -CA ca.crt -CAkey ca.key -set_serial 01 -extfile extensions.cnf -extensions v3_ca -out certificate.crt ```

2. :cloud: Setup IAM Roles Anywhere in AWS

# ----------------------------------------
# AWS Provider for Stage Account
# ----------------------------------------
provider "aws" {
  region  = var.REGION
  alias   = "deployment-eu"
}


# -----------------------------
# 1. Create a Trust Anchor
# -----------------------------
resource "aws_rolesanywhere_trust_anchor" "external_pipeline_trust" {
  name = "external-pipeline-trust"

  source {
    source_type = "CERTIFICATE_BUNDLE"
    source_data {
      x509_certificate_data = file("ca.crt") # Your trusted CA bundle (PEM)
    }
  }
  enabled = true
  provider = aws.deployment-eu
}

# ----------------------------------------
# 2. Create an IAM Role for External CICD
# ----------------------------------------
resource "aws_iam_role" "pipeline_deployer" {
  name = "external-pipeline-deployer-${var.REGION}"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = "rolesanywhere.amazonaws.com"
      },
      Action = [
        "sts:AssumeRole",
        "sts:TagSession",
        "sts:SetSourceIdentity"
      ],
      Condition = {
        StringEquals = {
          "aws:PrincipalTag/x509Subject/CN" = "example-internal-ca"
        },
        ArnEquals = {
          "aws:SourceArn" = aws_rolesanywhere_trust_anchor.external_pipeline_trust.arn
        }
      }
    }]
  })

  provider = aws.deployment-eu
}

# ----------------------------------------
# 3. Attach Deployment Permissions (Broad Example)
# ----------------------------------------
resource "aws_iam_role_policy" "pipeline_access_policy" {
  name   = "deployment-access"
  role   = aws_iam_role.pipeline_deployer.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect   = "Allow",
        Action   = ["*"],
        Resource = ["*"]
      }
    ]
  })

  provider = aws.deployment-eu
}

# ----------------------------------------
# 4. Create a RolesAnywhere Profile
# ----------------------------------------
resource "aws_rolesanywhere_profile" "pipeline_profile" {
  name                        = "pipeline-profile"
  role_arns                   = [aws_iam_role.pipeline_deployer.arn]
  require_instance_properties = false
  enabled                     = true
  duration_seconds            = 3600

  provider = aws.deployment-eu
}

3. :locked_with_key: Upload Certificate and Private Key to Azure DevOps Secure Files

To ensure your private key and certificate are kept safe and not exposed in your code repository, upload them to Azure DevOps Secure Files.

4. :rocket: Use in CI/CD Pipeline File

jobs:
  - deployment: Deploy
    strategy:
      runOnce:
        deploy:
          steps:
            - task: DownloadSecureFile@1
              name: Certificate
              displayName: 'Download certificate'
              inputs:
                secureFile: 'certificate.crt'
            - task: DownloadSecureFile@1
              name: Privatekey
              displayName: 'Download private key'
              inputs:
                secureFile: 'private.key'
            - bash: |              
                wget https://rolesanywhere.amazonaws.com/releases/1.0.3/X86_64/Linux/aws_signing_helper
                chmod +x aws_signing_helper
              displayName: Install AWS Signer
            - bash: |
                aws configure set credential_process "$(pwd)/aws_signing_helper credential-process --certificate $(Certificate.secureFilePath) --private-key $(Privatekey.secureFilePath)  --trust-anchor-arn $TRUST_ANCHOR_ARN --profile-arn $PROFILE_ARN --role-arn $ROLE_ARN" --profile "$PROFILE_ENV"
                echo "##vso[task.setvariable variable=AWS_SDK_LOAD_CONFIG]1"
              displayName: Obtain AWS Credentials
            - script: |
                source .env
                aws s3 sync rewired_build s3://$REACT_APP_SELLER_S3_BUCKET --acl public-read --profile "$PROFILE_ENV"
                aws cloudfront create-invalidation --distribution-id $REACT_APP_SELLER_DISTRIBUTION_ID --paths "/*" --profile "$PROFILE_ENV"
              displayName: 'Deployment'

:chequered_flag: Conclusion

IAM Roles Anywhere makes it possible to run secure, temporary AWS deployments from any CI/CD pipeline without the need for long-lived credentials. It bridges the gap between external workloads and AWS IAM, enabling safer automation.

By following the approach outlined above, we can modernize your CI/CD security while maintaining flexibility and speed.

Reference

1 Like