Securing AWS Infrastructure Deployment with GitHub Actions, Terraform, and OpenID Connect

The evolution of infrastructure as code has shifted the operational paradigm from manual configuration to automated, version-controlled deployments. Central to this shift is the integration of GitHub Actions for continuous integration and continuous deployment (CI/CD) with Terraform for infrastructure provisioning and AWS for cloud resource management. While the functional mechanics of deploying Terraform code via GitHub Actions are straightforward, the critical challenge lies in authentication and security. Historically, teams relied on long-lived AWS access keys stored as GitHub Secrets to authenticate workflows. This approach introduces significant security risks, including credential leakage and excessive privilege exposure. The modern standard replaces static secrets with OpenID Connect (OIDC), enabling short-lived, just-in-time credentials that eliminate the need for long-lived secrets and provide granular, identity-based access control. This article details the technical architecture, configuration, and best practices for securely deploying Terraform to AWS using GitHub Actions authenticated via OIDC, including state management strategies and workflow structuring.

The Security Imperative: Moving Beyond Long-Lived Credentials

In traditional CI/CD pipelines for infrastructure, the most common method for authenticating GitHub Actions to AWS was to store an AWS Access Key ID and Secret Access Key as repository secrets. These credentials are long-lived, meaning they remain valid until manually rotated or revoked. Furthermore, they often possess broad permissions to ensure the pipeline can perform any necessary operation. If these credentials are compromised—through a leak in a log file, a malicious actor gaining access to the repository, or a misconfigured secret—they grant persistent access to the AWS account. The damage potential is substantial, ranging from unauthorized resource creation to data exfiltration.

OpenID Connect (OIDC) resolves this vulnerability by acting as an identity layer on top of the OAuth 2.0 protocol. In the context of GitHub Actions and AWS, OIDC allows GitHub to verify the identity of the workflow execution and issue a short-lived JSON Web Token (JWT). AWS IAM can then validate this token and issue temporary security credentials to the GitHub Action. These credentials expire shortly after the job completes, drastically reducing the window of opportunity for misuse. This approach ensures that credentials are never stored in the repository, are not subject to accidental leaks, and are scoped strictly to the specific GitHub repository and branch triggering the workflow.

Configuring AWS Identity and Access Management for OIDC

To enable OIDC authentication, the AWS side of the equation requires two primary components: an IAM Identity Provider and an IAM Role with a trust policy that accepts tokens from GitHub.

Establishing the GitHub Identity Provider

The first step involves creating an OpenID Connect provider within AWS IAM. This provider informs AWS that GitHub is a trusted source for identity tokens.

  • Navigate to the IAM console and select "Identity providers".
  • Create a new provider and select "OpenID Connect".
  • Configure the provider with the following specific values:
    • Provider URL: https://token.actions.githubusercontent.com
    • Audience: sts.amazonaws.com

The provider URL points to GitHub's OIDC token endpoint, while the audience ensures that the tokens are intended for AWS Security Token Service (STS).

Creating the IAM Role with Trust Policy

Once the identity provider is established, an IAM role must be created to grant the GitHub Actions the necessary permissions to manage AWS resources. This role uses "Web Identity" federation, which relies on the previously created GitHub identity provider.

  • Navigate to "Roles" in the IAM console and create a new role.
  • Select "Web identity" as the trusted entity type.
  • Choose the newly created GitHub identity provider.
  • Define the trust relationship using a policy document that restricts access to specific GitHub repositories and branches.

The trust policy is critical for security. It uses conditions to ensure that only workflows from authorized repositories can assume the role. For example, to allow a specific repository (tiborhercz/terraform-openid-connect-example) to assume the role from any branch, the policy condition utilizes the StringLike operator on the subject (sub) claim of the JWT:

json "StringLike": { "token.actions.githubusercontent.com:sub": "repo:tiborhercz/terraform-openid-connect-example:*" }

This configuration ensures that even if the OIDC token is intercepted, it cannot be used from a different repository or branch. The role should then be attached with the necessary IAM policies (e.g., AmazonS3FullAccess, AmazonDynamoDBFullAccess) required for the Terraform configuration, adhering to the principle of least privilege.

Terraform State Management and Bootstrap Infrastructure

Terraform requires a state file to track the mapping between resources and the real-world infrastructure. Storing this state locally is impractical for team environments and CI/CD pipelines. The industry standard is to use a remote backend, typically an Amazon S3 bucket with state locking provided by an Amazon DynamoDB table.

Bootstrap Process

Before the OIDC workflow can be fully operational, initial AWS credentials (long-lived secrets) are required to "bootstrap" the state infrastructure. This is a one-time process. The bootstrap infrastructure includes:

  • An S3 bucket to store the Terraform state file.
    • Versioning must be enabled to allow recovery from accidental deletions or corruption.
    • Encryption at rest using AES256 must be enforced to protect sensitive data (such as database passwords) that may be contained within the state file.
    • Public access must be blocked entirely to ensure privacy.
  • A DynamoDB table to manage state locking.
    • On-demand billing is recommended to minimize costs, as lock operations are infrequent.

Once the state backend is provisioned, the long-lived credentials used for bootstrapping can be removed from the GitHub repository, as subsequent workflows will use OIDC.

Configuring the Terraform Provider for OIDC

Terraform itself must be configured to use the OIDC token issued by GitHub Actions. This is achieved by configuring the AWS provider to use web identity federation. Instead of relying on environment variables for static keys, the provider reads a token file and assumes a specific role.

A common approach involves defining variables for the AWS account ID, region, and the GitHub role name. The credentials.aws file, which can be generated or configured within the workflow, specifies the backend and provider settings:

ini [backend] region=eu-west-1 role_arn=arn:aws:iam::000000000000:role/GitHubActionsRole web_identity_token_file=/tmp/web_identity_token_file

It is crucial to include two blank lines at the end of this file, as some parsers require this formatting for correct interpretation. Additionally, best practices involve pinning the Terraform version and provider versions in versions.tf to ensure consistency across environments:

hcl terraform { required_version = ">= 1.2.0, < 1.3.0" required_providers { aws = { source = "hashicorp/aws" version = "4.22.0" } } }

Designing the GitHub Actions Workflow

The GitHub Actions workflow orchestrates the entire deployment process. It must be configured to request the OIDC token, assume the AWS role, and execute the Terraform lifecycle commands.

Workflow Permissions and Triggers

The workflow file (e.g., .github/workflows/terraform.yml) must explicitly request the necessary permissions. Without these, GitHub will not generate the OIDC token required for authentication.

  • id-token: write: Required to request the JWT from GitHub.
  • contents: read: Required for the actions/checkout step to read the repository code.

Triggers should be defined to run on pushes to the main branch and on pull requests. This allows for validation on PRs and deployment on merges.

The Plan Phase

The plan phase validates the infrastructure changes without applying them. This is crucial for peer review and safety. The workflow steps typically include:

  1. Checkout: Pull the code from the repository.
  2. Setup Terraform: Install the specific version of Terraform. Using hashicorp/setup-terraform@v3 is the current standard.
  3. Configure AWS Credentials: Use the aws-actions/configure-aws-credentials@v4 action to assume the IAM role using OIDC.
  4. Terraform Init: Initialize the backend and providers.
  5. Terraform Format Check: Ensure code style consistency using terraform fmt -check.
  6. Terraform Validate: Check for syntax errors and internal consistency.
  7. Terraform Plan: Generate the execution plan.
  8. Upload Artifact: Save the plan file (tfplan) as an artifact for the apply phase.

```yaml
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam:::role/
aws-region: ${{ env.AWS_REGION }}

  • name: Terraform Init
    run: terraform init -input=false

  • name: Terraform Format Check
    run: terraform fmt -check

  • name: Terraform Validate
    run: terraform validate

  • name: Terraform Plan
    run: terraform plan -input=false -out=tfplan

  • name: Upload plan artifact
    uses: actions/upload-artifact@v4
    with:
    name: tfplan
    path: tfplan
    ```

The Apply Phase with Environment Protection

The apply phase actually modifies the infrastructure. To prevent accidental deployments, this phase should be separated into a distinct job and protected by GitHub Environments.

  • Environment Protection: Define the job to run in a specific environment (e.g., production). GitHub allows you to configure protection rules for environments, such as requiring manual approval from designated reviewers before the job proceeds.
  • Branch Restriction: Ensure the apply step only runs on pushes to the main branch, not on pull requests.
  • Download Artifact: Retrieve the plan file generated in the previous job.
  • Terraform Apply: Execute the plan using the downloaded file.

```yaml
apply:
name: Terraform Apply (requires approval)
needs: plan
runs-on: ubuntu-latest
environment:
name: production
if: github.eventname != 'pullrequest'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Terraform
  uses: hashicorp/setup-terraform@v3

- name: Configure AWS credentials (OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<ROLE_FOR_GITHUB>
    aws-region: ${{ env.AWS_REGION }}

- name: Terraform Init
  run: terraform init -input=false

- name: Download plan artifact
  uses: actions/download-artifact@v4
  with:
    name: tfplan
    path: .

- name: Terraform Apply
  run: terraform apply -input=false tfplan

```

Operational Best Practices and Considerations

Version Pinning and Consistency

Using specific versions of Terraform and AWS providers is critical for stability. In the hashicorp/setup-terraform action, specifying the version ensures that the workflow uses the same engine as the developer's local environment. Similarly, pinning the provider version in versions.tf prevents unexpected changes in behavior or resource attributes when AWS updates its provider plugin.

Separation of Plan and Apply

Splitting the plan and apply into separate jobs with artifact passing provides a clear audit trail. The plan artifact can be reviewed by engineers to verify the intended changes before approval. This manual gate, enforced by GitHub Environment protection rules, is a critical safeguard against erroneous deployments.

Minimizing Permissions

The IAM role assumed by the GitHub Action should only have the permissions necessary for the specific resources managed by the Terraform configuration. Avoid using overly broad policies like AdministratorAccess. Instead, create custom policies that grant read/write access only to the specific S3 buckets, DynamoDB tables, EC2 instances, or other resources defined in the Terraform code.

Conclusion

The integration of GitHub Actions, Terraform, and AWS represents a mature and secure approach to infrastructure automation. By transitioning from long-lived static credentials to OpenID Connect, organizations significantly reduce their security surface area. The combination of short-lived tokens, granular IAM trust policies, and robust state management via S3 and DynamoDB ensures that infrastructure changes are traceable, recoverable, and protected from unauthorized access. Furthermore, structuring the workflow with separate plan and apply jobs, enforced by GitHub Environment protection rules, introduces a necessary human-in-the-loop control for critical deployments. This architecture not only enhances security but also improves operational reliability and confidence in the infrastructure delivery pipeline.

Sources

  1. Automating AWS Resource Deployment with GitHub Actions and Terraform
  2. How to Deploy Terraform to AWS with GitHub Actions Authenticated with OpenID Connect
  3. Provisioning AWS Infrastructure Using Terraform and GitHub Actions
  4. How to Use GitHub OIDC and Terraform

Related Posts