Keyless deploys: GitHub Actions to AWS with OIDC and Terraform
Why every CloudGuard platform deploys without a single stored cloud key, and the exact setup we use.
Most leaked cloud credentials are not stolen from production. They are copied out of a CI system, where an access key was pasted into a secret years ago and never rotated. The fix is simple and free: stop storing keys in CI at all.
Every platform we build, including the website you’re reading, deploys this way.
How it works
GitHub Actions can issue a short-lived OpenID Connect token for each workflow run. The token says, in signed form, which repository, branch and workflow is asking. AWS can be told to trust GitHub as an identity provider and to exchange that token for temporary credentials, but only when the claims match exactly what you allow.
No secret is stored. Credentials last about an hour and are scoped to one role.
The Terraform
First, trust GitHub’s token issuer once per AWS account:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
}
Then create a role that only one repository and one branch can assume:
data "aws_iam_policy_document" "trust" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:your-org/your-repo:ref:refs/heads/main"]
}
}
}
The sub condition is the important line. Without it, any repository on GitHub could assume your role.
Finally, give the role only what the deploy needs. For a static site, that is writing to one bucket and invalidating one CloudFront distribution.
The workflow
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ca-central-1
The role ARN is not a secret, so it can live in a repository variable. There is nothing to rotate and nothing to leak.
Going further
- Separate plan and apply roles. On infrastructure repositories, pull requests get a read-only role that can run
terraform plan. Only a deliberate run onmaingets the role that can apply. - Use environments. Bind the production role to a GitHub environment in the
subclaim, so only jobs in that environment can use it. - Pin the account. In Terraform, set
allowed_account_idson the provider so a plan run against the wrong account fails immediately instead of creating resources in it. - The same idea works on Azure and GCP. Azure calls it workload identity federation, and we use it the same way.
If your pipelines still hold long-lived cloud keys, this is one of the cheapest security improvements you can make this quarter.