Oct 27, 20265 min read

How to Deploy from GitHub Actions to AWS Without Long-Lived Keys

Replacing AWS access keys in GitHub secrets with OIDC federation and a scoped IAM role, compared with letting Encore deploy through a role in your account, and the same pattern for Google Cloud.

Many deploy workflows still start with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in GitHub secrets. Those keys work from anywhere until they are rotated, which in practice is rarely.

GitHub Actions can instead request a signed OpenID Connect token for each job, and AWS can exchange it for short-lived credentials on a role you scope to one repository and branch. This guide walks through that setup first, then covers deploying through Encore and the same pattern on Google Cloud.

What you need to decide:

  • Whether CI should hold cloud credentials at all.
  • Which repositories, branches or environments may deploy.
  • Which permissions the deploy role needs.
  • Who owns the trust policy over time.

Keyless deploy options at a glance

CapabilityEncoreGitHub OIDC to AWSGitHub OIDC to Google Cloud
Credentials in GitHubNoneNone, only a role ARNNone, only provider and account names
Trust set upConnect an IAM role once with an external IDOIDC provider and role trust policyWorkload identity pool and provider
Who deploysEncore, on pushYour workflowYour workflow
PermissionsRole for provisioning, IAM role per servicePolicy you writeIAM bindings you write

GitHub OIDC to AWS

The setup has three parts: an IAM OIDC identity provider for token.actions.githubusercontent.com, a role whose trust policy only accepts tokens whose sub claim names your repository and branch, and a workflow step that assumes the role. First, create the provider and the role. IAM fetches the provider's certificate thumbprint itself when you leave --thumbprint-list out:

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com cat > trust.json <<JSON { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main" } } }] } JSON aws iam create-role --role-name github-deploy --assume-role-policy-document file://trust.json aws iam put-role-policy --role-name github-deploy --policy-name deploy \ --policy-document '{"Version":"2012-10-17","Statement":[ {"Effect":"Allow","Action":"ecr:GetAuthorizationToken","Resource":"*"}, {"Effect":"Allow","Action":["ecr:BatchCheckLayerAvailability","ecr:InitiateLayerUpload","ecr:UploadLayerPart","ecr:CompleteLayerUpload","ecr:PutImage"],"Resource":"arn:aws:ecr:eu-central-1:'"${ACCOUNT_ID}"':repository/my-app"}, {"Effect":"Allow","Action":["ecs:UpdateService","ecs:DescribeServices"],"Resource":"arn:aws:ecs:eu-central-1:'"${ACCOUNT_ID}"':service/my-cluster/my-service"}]}'

In the workflow, permissions: id-token: write lets the job request an ID token, and configure-aws-credentials exchanges it for short-lived credentials on the role named in role-to-assume:

name: deploy on: push: branches: [main] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::123456789012:role/github-deploy aws-region: eu-central-1 - run: aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment

Good to know

Never use a wildcard like repo:my-org/* in the sub condition, since any branch or pull request workflow in matching repositories could then deploy. Narrow Resource in the deploy policy to your repository and service ARNs once they exist. GitHub's OIDC documentation lists the claims available for conditions.

Encore: no cloud credentials in CI

Encore

Encore combines your backend code with a deploy pipeline that runs outside your GitHub workflows, so pushes deploy to your own AWS account and no AWS credentials are stored in GitHub. You create an IAM role in your account once, require the external ID Encore gives you, and connect it, and Encore uses that role to provision and update infrastructure.

After linking the repository through the GitHub integration, a push to the configured branch deploys:

git push origin main

Encore provisions the infrastructure the code declares on AWS and provisions an IAM role for the service, so the application does not use deploy credentials at runtime either. Access to the app is managed with roles in Encore, and each pull request can get a preview environment.

For agents opening pull requests, cloud access sits with Encore's IAM role, not the agent, and a preview environment is where the agent's change runs before merge.

Good to know

The default connected role gives Encore broad permissions to provision and manage infrastructure in the account, so the docs for connecting your cloud suggest a dedicated AWS account for isolation. Your GitHub Actions can still run tests and linters without any cloud access.

Try Encore →

GitHub OIDC to Google Cloud

On Google Cloud, Workload Identity Federation accepts the same token. Create a pool and provider limited to your repository, then let it impersonate a deploy service account:

gcloud iam workload-identity-pools create github --location=global gcloud iam workload-identity-pools providers create-oidc my-repo \ --location=global --workload-identity-pool=github \ --issuer-uri=https://token.actions.githubusercontent.com \ --attribute-mapping=google.subject=assertion.sub,attribute.repository=assertion.repository \ --attribute-condition="assertion.repository=='my-org/my-repo' && assertion.ref=='refs/heads/main'" gcloud iam service-accounts create github-deploy gcloud iam service-accounts add-iam-policy-binding github-deploy@${PROJECT_ID}.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github/attribute.repository/my-org/my-repo" gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --role=roles/run.developer --member="serviceAccount:github-deploy@${PROJECT_ID}.iam.gserviceaccount.com"

The workflow then uses google-github-actions/auth with workload_identity_provider and service_account instead of a JSON key.

Good to know

Deploying to Cloud Run also needs roles/iam.serviceAccountUser on the runtime service account, and pushing images needs Artifact Registry write access. Grant both to the deploy account rather than to the whole pool.

How to choose

  • GitHub OIDC to AWS if your workflows deploy themselves and you want to replace stored keys with a scoped role.
  • Encore if you want pushes to deploy to your own AWS account through one connected role, with deploys running outside your GitHub workflows.
  • GitHub OIDC to Google Cloud for the same setup on GCP.

Once OIDC works, delete the old access keys from GitHub secrets and from IAM.

Frequently asked questions

How do I deploy to AWS from GitHub Actions without access keys?

Create an IAM OIDC identity provider for token.actions.githubusercontent.com, create a role whose trust policy allows sts:AssumeRoleWithWebIdentity for your repository and branch, grant the role only the deploy permissions it needs, and use aws-actions/configure-aws-credentials with role-to-assume and id-token: write permission in the workflow. Alternatively, Encore deploys from pushes through an IAM role you connect once, so no AWS credentials live in GitHub at all.

Why are long-lived AWS keys in GitHub secrets a risk?

They work from anywhere until someone rotates them, so a leaked workflow log, a compromised dependency or a forked workflow can use them. OIDC tokens are issued per job, expire within minutes and only work for the repository and branch the trust policy names.

How do I restrict the OIDC role to one branch?

Add a condition on token.actions.githubusercontent.com:sub, for example repo:my-org/my-repo:ref:refs/heads/main, and require aud to be sts.amazonaws.com. For deploys gated by GitHub environments, use repo:my-org/my-repo:environment:production instead.

Does this work for Google Cloud too?

Yes. Google's Workload Identity Federation accepts the same GitHub OIDC tokens, and google-github-actions/auth exchanges them for short-lived credentials for a service account.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders