Replace GCP service account keys before the JSON file becomes the way AWS calls Google. A downloaded key is a long-lived credential: a private key, a key id, and a service account email, copied into a secret store, a CI variable, or a laptop. Anyone who holds the file can call the APIs that service account can call, until someone finds every copy and deletes the key in the project. Federation removes that file. The AWS role the workload already has is what Google accepts, and Google returns a short-lived token.
Rutagon builds that cutover for teams whose systems run on AWS and who still have one or two Google projects in the path: a BigQuery read, a Maps or Workspace API, an artifact bucket. This is the one place a second cloud shows up in the design. The caller we operate is the AWS workload. Google documents the exchange in Workload Identity Federation with AWS. Google's own key management guidance tells you to prefer a short-lived credential over a downloaded key. The work is making the running job obey that, then deleting the key.
How we replace GCP service account keys
The inventory comes first, because a key you have not listed will survive the cutover. For each service account we list the user-managed keys: key id and creation time. System-managed keys that Google uses internally are not the files people download, and we do not treat them as the leak. The user-managed key is the one whose JSON left the project. Beside each key id we record where the JSON was copied, from the runbook and from the secret stores we can read: an AWS Secrets Manager secret, a CI variable, a parameter in the task definition. A key with no known copy is still deleted once nothing authenticates with it. A key with three copies needs three removals, or the delete in Google only stops the copies you remembered.
The federation path Google documents for an AWS caller works like this. A workload identity pool in the Google project trusts the AWS account. A pool provider describes that AWS account. The AWS workload already has a role: an ECS task role, a Lambda execution role, or an EC2 instance role. At call time the Google auth library builds a signed AWS request that proves which role is calling. Google's token service checks it and returns a federated token. That token is short-lived. The JSON key is not on the disk and not in the environment.
Some APIs and some existing IAM bindings expect that specific service account, not a federated principal. In that case the federated token impersonates the service account. The service account grants roles/iam.workloadIdentityUser to the AWS principal, and the library calls generateAccessToken on that account. The service account remains the identity in Google audit logs. The key file is still absent. We use impersonation when the current IAM on buckets and datasets already names that service account, so the cutover does not rewrite every binding in the same change. Where an API accepts the federated principal directly, we grant the role to the principal and skip impersonation. Both are in Google's workload identity documentation. The choice is which identity the existing bindings already name.
A second path applies when the job already runs inside Google Cloud with an attached service account. Then there is nothing to federate from AWS. The platform provides the identity, and downloading a key is how someone worked around the attachment. We remove the key and point the job at the attached account. We do not treat a Kubernetes cluster on Google as a platform we are here to rebuild. If the job is on AWS, the pool-and-provider path above is the one we ship.
What the workload config looks like
The job gets a credential configuration, not a key. The configuration names the pool provider as the audience, says the subject token comes from the AWS environment, and, when we impersonate, names the service account URL. Application Default Credentials reads that file from a path. The process never sees a private_key field.
type: external_account
audience: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/AWS_PROVIDER
subject_token_source: aws_environment
service_account_impersonation: SA_EMAIL
token_lifetime: short-lived access token from Google STS
key_file: absent
aws_caller: ecs task role or lambda role, no static access key PROJECT_NUMBER is the number, not the project id. That mistake fails the exchange with an audience error and sends people back to the JSON key because the key "still works." We keep the number in the same Terraform that creates the pool. The AWS role needs no Google JSON. It needs to be the role the provider trusts, and nothing broader. A trust of the entire AWS account, with no condition on the role name, lets every role in the account impersonate the service account. We condition the provider attribute on the role we named in the inventory.
The call path is the test. We run one read the job already performs, with the key removed from that environment only. Success is a Google access token in memory and a cloud audit entry for the service account or the federated principal. Failure is a reason to fix the audience, the role condition, or the impersonation binding. It is not a reason to put the JSON back "until next sprint" without a dated exception. An exception lists the key id, the environment, and the date the key will be deleted. The date is in the same tracker as the rest of the credential work described in eliminating service account passwords.
After the production environment succeeds, we delete the user-managed key in the project. Deletion is what stops the other copies. Removing the secret in AWS while the key remains in Google only stops the copy you found. We then search the CI logs and the secret inventory for that key id once more. A pipeline that still injects the file fails closed, which is the point of deleting the key before you declare the migration done.
Stopping the next download
An organization policy constraint, constraints/iam.disableServiceAccountKeyCreation, blocks new user-managed keys once the live jobs are off them. We turn that on at the project after the last key is gone, or at the folder if every project under it is ready. Turning it on while a job still mounts a key does not migrate the job. It only makes the next outage harder to patch by minting another key. Order is: inventory, federation, prove the call, delete the key, then the constraint.
Keys created before the constraint remain until deleted. The constraint is not a cleanup. The cleanup is the key list going to zero user-managed keys for the accounts in scope. We write that list into the evidence folder: service account email, key count, date. The GRC platform can point at the file. The file is regenerated from keys list, not from a screenshot of the console on the day someone remembered.
Cross-cloud identity federation into AWS covers the neighboring problem, where a non-AWS caller needs an AWS role. This article is the opposite direction of the token: an AWS role needs a Google API, and the credential that used to be a downloaded key becomes a Google STS exchange. Workload identity federation is the broader cutover pattern we use when the long-lived secret is an AWS access key or a CI token. The Google key is the same class of secret. It has a file, a long lifetime, and a habit of being copied.
What we will not do is leave the JSON in Secrets Manager and rotate it every 90 days. Rotation still distributes a private key. It adds a calendar. The federated token does not need a rotation lambda, because Google mints it at call time and it expires on Google's clock. The AWS role's permissions are the ongoing review, and that review belongs with the rest of the role's access, not with a key-id spreadsheet.
Rutagon's deliverable is the key inventory, the pool and provider scoped to the AWS role, the credential configuration on the workload, the proof call, the key deletion, and the organization policy that keeps the next person from downloading a replacement. The service account can stay. The file goes.
Frequently Asked Questions
Can we keep the key in a secret manager if we rotate it?
Rotation replaces one long-lived private key with another and pushes the new file to every consumer. The federated exchange does not leave a private key for us to store. We delete the user-managed key after the workload's proof call succeeds, and we block creation of the next one.
What if the API still has to run as the existing service account?
The AWS role obtains a federated token, then impersonates that service account through roles/iam.workloadIdentityUser. Audit logs still show the service account. There is still no JSON key on the workload. Direct grants to the federated principal are the other option when the API and the bindings can name that principal.
Does this require moving the job off AWS?
No. The caller stays on its AWS role, and Google's workload identity pool trusts that role and issues the short-lived token. A job that already runs in Google Cloud with an attached service account should use the attachment, and downloading a key for that job is the habit we remove.
What stops a new key next month?
The organization policy constraint that disables service account key creation, applied after the live keys are deleted. The constraint does not delete existing keys. The evidence we keep is a key list at zero for the accounts in scope, plus the date.
Where does the AWS role get reviewed after the key is gone?
The role is an ordinary AWS role with a trust and a set of callers. It no longer carries a Google private key in a secret. Review the role with the rest of your AWS access reviews. The Google side is the pool condition that names that role, which we keep in Terraform beside the pool.
Ask for a credential diagnostic that lists downloaded Google service account keys and the AWS roles that should replace them. Contact Rutagon at rutagon.com/contact, 907-841-8407, or contact@rutagon.com.