Skip to content

Removing Long-Lived Cloud Credentials from GitHub Actions

Lesson 5 of 10Intermediate → Advanced5 min readGitHub Actions & CI/CD · Actions SecurityVerified: GitHub secrets API, AWS IAM, Entra ID, Google Cloud IAM, August 2026

Configuring OIDC is the easy part. Removing the credentials it replaces is the part that gets deferred — and a repository with both a working OIDC role and an unused access key still in secrets has gained nothing in security terms. The old credential is what an attacker would use.

This page is the migration, including the step people skip: proving the old path is dead before deleting it, and deleting it afterwards.

You cannot retire what you have not found. Credentials hide in more places than repository secrets:

Terminal window
gh secret list --repo OWNER/REPO
gh secret list --org ORG
gh variable list --repo OWNER/REPO
{/* Environment secrets are per environment and are missed by the commands above */}
gh api repos/OWNER/REPO/environments --jq '.environments[].name' | while read -r env; do
echo "== $env"
gh api "repos/OWNER/REPO/environments/$env/secrets" --jq '.secrets[].name'
done

Names that indicate a long-lived cloud credential:

NameWhat it is
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYIAM user key — no expiry
AZURE_CREDENTIALSJSON blob containing a client secret
AZURE_CLIENT_SECRETEntra ID client secret
GCP_SA_KEY / GOOGLE_CREDENTIALSService account key JSON — no expiry
KUBECONFIG / KUBE_CONFIG_DATACluster credentials, often with a long-lived token
DOCKER_PASSWORDRegistry credential; GHCR needs none

Also check for credentials outside GitHub entirely — a CI variable in another system, a key baked into a container image, a .env in a deployment repository. The GitHub secret is often a copy.

  1. Configure OIDC alongside the existing credential. Create the identity provider, role or federated credential, and conditions — following AWS, Azure or Google Cloud. Nothing is removed yet.

  2. Add a read-only verification job that authenticates via OIDC and reads something harmless — aws sts get-caller-identity, az account show, gcloud auth list. Confirm it works from the exact trigger your real deployment uses, including the environment.

  3. Switch the real workflow to OIDC. Add id-token: write, replace the credential step, and leave the old secret in place.

  4. Run it for real. Deploy through the new path and confirm the deployment behaves identically.

  5. Prove the old credential is unused. Check the cloud’s last-used data after a period covering your full cycle — including weekly or monthly scheduled workflows, which are exactly what a two-day observation window misses.

  6. Disable, do not delete. Deactivate the access key, disable the client secret, or disable the service account key. If something breaks, re-enabling takes seconds.

  7. Wait, then delete. After a period with nothing broken, delete the cloud credential and remove the GitHub secret.

Step 5 is the one that determines whether this migration is safe, and step 6 is the one that makes it reversible. Skipping to deletion is how a migration becomes an outage.

AWS — the access key’s last-used date and the service it was used against:

Terminal window
aws iam list-access-keys --user-name ci-deploy
aws iam get-access-key-last-used --access-key-id AKIA_PLACEHOLDER

Azure — sign-in activity for the service principal is in the Entra ID sign-in logs, filtered to service principal sign-ins. Also list credentials on the app registration to confirm what still exists:

Terminal window
az ad app credential list --id APP_ID --query "[].{name:displayName, end:endDateTime}" -o table

Google Cloud — key usage is reported per service account key, and IAM’s activity analyser reports recent authentication.

In all three, “no usage since we cut over” is the signal to proceed. “No usage ever” on a key that exists usually means you have found a second, forgotten credential — which is its own finding.

Terminal window
{/* AWS: deactivate, keep the key so it can be re-enabled */}
aws iam update-access-key --user-name ci-deploy --access-key-id AKIA_PLACEHOLDER --status Inactive
Terminal window
{/* Google Cloud: disable the key */}
gcloud iam service-accounts keys disable KEY_ID --iam-account=deployer@PROJECT_ID.iam.gserviceaccount.com

For Azure, remove the client secret from the app registration — there is no disable, so record what you removed before doing it.

Then delete the GitHub secret:

Terminal window
gh secret delete AWS_SECRET_ACCESS_KEY --repo OWNER/REPO

Some things genuinely still need a stored secret:

  • Vendors that only issue API keys.
  • Azure Static Web Apps deployment tokens.
  • Docker Hub, and other registries without federation.
  • Signing keys held outside a managed service.

For each, reduce the blast radius rather than pretending it is fine:

  • Store it as an environment secret behind protection rules, not as a repository secret. It is then unreadable by any job that has not passed the environment’s gate.
  • Scope it as narrowly as the vendor allows — one repository, one permission, read-only where possible.
  • Set a rotation reminder and actually rotate.
  • Prefer a GitHub App token over a personal access token where the counterparty is GitHub — actions/create-github-app-token@v3 mints one per run, scoped and short-lived.

Migrating and then having someone add AWS_SECRET_ACCESS_KEY back next quarter is a common outcome. Two controls help:

Audit on a schedule. A workflow that lists secrets across the organisation and fails on names matching known long-lived credential patterns turns regression into a build failure.

Remove the IAM user entirely. If the user that owned the access key no longer exists, a new key cannot be created for it by habit. This is more durable than any policy document.

  1. Inventory every secret across your repositories, including environment secrets, and classify each as long-lived or not.

  2. Pick the highest-risk one — usually a cloud key with broad permissions — and check its last-used date.

  3. Configure OIDC alongside it and add a read-only verification job. Confirm it works from the real trigger.

  4. Cut over the deployment. Observe for a period covering your scheduled workflows.

  5. Deactivate — not delete — the old credential. Wait. Then delete it and remove the GitHub secret.

  6. Delete the IAM user or service account that owned it, so the habit cannot be repeated.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Get the production security checklists and Actions hardening templates from the Professional Toolkit.