Jenkins OIDC AWS removes the access key that has been sitting in the Jenkins credential store since the first deploy job. The controller becomes an OIDC issuer for the duration of a build. The job requests a token, STS returns credentials for one role, and the static AWS credential is deleted from the controller and from every agent. Rotating that key inside the credential plugin keeps a long-lived secret on the machine that already has your build history.
This is the Jenkins issuer. CircleCI OIDC AWS uses CircleCI's issuer and a project subject. A GitLab trust policy will not validate a Jenkins token. Rutagon stands up the provider plugin, the IAM role bound to the jobs that deploy, and the deletion of credentials.xml entries that still hold AWS_ACCESS_KEY_ID. The cutover packet is filed in the GRC you already run.
Jenkins as the issuer, not as a place to hide keys
The OpenID Connect Provider plugin publishes the Jenkins root URL as an issuer. That URL has to be stable HTTPS. An issuer that changes when someone renames the controller invalidates the IAM provider and the next build fails closed, which is better than succeeding with a key, as long as you planned the URL before you created the provider.
A pipeline step asks Jenkins for a token with audience sts.amazonaws.com. We set the sub claim template to the job's full name so IAM can tell a production deploy job from a sandbox job. Before we lock the trust policy, we decode one token from a non-production job and read the iss, aud, and sub claims. The trust policy is written against those claims, not against a guess.
script {
env.JENKINS_OIDC_TOKEN = oidcToken(audience: 'sts.amazonaws.com')
}
The assume call is ordinary STS. Session duration matches the deploy stage, and the role session name includes the build number so CloudTrail distinguishes runs.
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::111122223333:role/jenkins-prod-deploy \
--role-session-name "jenkins-${BUILD_NUMBER}" \
--web-identity-token "$JENKINS_OIDC_TOKEN"
AWS documents the call in AssumeRoleWithWebIdentity. The token is the web identity. There is no access key on the command line and none in the agent environment after the step exits.
Jenkins OIDC AWS scope on the controller and the agents
The IAM trust allows sts:AssumeRoleWithWebIdentity only for the Jenkins issuer, the audience we configured, and a sub limited to the production folder. A freestyle job created in the root of the controller does not match job:prod-deploy/* and cannot assume the role. That constraint is the point of reading sub before you write the condition.
Agents are where static keys hide. The credential store on the controller is the obvious place. Agent images, injected environment files, and a "global" AWS credential bound to every node are the rest. We inventory credentials of type AWS access key, note which jobs reference them, move those jobs to the OIDC step, and delete the credential. An agent AMI that still bakes AWS_ACCESS_KEY_ID into a file is rebuilt. A credential that remains "in case the plugin fails" is the standing secret.
The controller's own IAM role, if it runs on EC2, is not the deploy role. Builds that assume a role with the controller instance profile give every job the controller's privileges and skip the sub check. We keep instance metadata off the deploy path. The job token is the only AWS identity the pipeline uses. AWS IMDSv2 enforcement still belongs on that controller if it is an EC2 instance. It is a different control from removing the CI key.
Folder separation is the administrative control that matches the IAM condition. Production jobs live in a folder whose Job DSL or Configuration as Code only the platform team can edit. A developer who can add a pipeline step in that folder can request the production token. OIDC does not fix an over-broad Jenkins authorization strategy. We pair the role trust with a folder ACL so the people who can mint the token are the people who can already ship.
Cutover order and what we delete
We create the IAM OIDC provider with the Jenkins issuer URL and a role trusted by the non-production subject. A sandbox job assumes it and calls sts get-caller-identity. We then add the production subject condition, run one production build, and read CloudTrail for that assume. After the event is present, we delete the static AWS credentials from Jenkins and deactivate the IAM access key.
Jobs that used the "withAWS" wrapper around a stored key get the wrapper pointed at a role assumption fed by the OIDC token, or they drop the wrapper and export the temporary credentials for the stage only. Temporary credentials are not written into a build log. We mask the token and the resulting session secret in the pipeline. A token printed in a console log is a short-lived secret with a public copy. Treat the log as sensitive until the session expires, and do not echo the token.
Shared libraries that construct an AWS client from global credentials are updated in the same change. Leaving the library on the old credential id means the next job someone copies still requests the key. We grep the job DSL and the shared library for the credential id and for AWS_ACCESS_KEY_ID before we call the migration done.
Multi-controller estates get one IAM provider per issuer URL. A staging controller is not trusted by the production role. Combining them because "it is all Jenkins" recreates a wide subject. GitLab OIDC to AWS stays on its own provider if both systems deploy. Two issuers, two roles.
Evidence and the tradeoff
The packet we file in the GRC you already run contains the issuer URL, the sub condition, a CloudTrail event id from the production assume, and an export of the Jenkins credential store showing no AWS access keys. Agent image digests that no longer contain keys are listed when the agents were a hiding place.
The tradeoff is operational. Jenkins must stay reachable at the issuer URL you registered, with a certificate AWS can validate when it fetches the keys. A broken TLS setup stops deploys. That outage is visible. A key copied out of the credential store is not. We take the availability dependency.
We also name the limit: OIDC on Jenkins authenticates the job to AWS. It does not review who can edit the Jenkinsfile. That review is access governance on the CI system. Credential elimination and folder ACL work land together, or the token is only as disciplined as the last person who could change the pipeline.
Frequently Asked Questions
Where does the Jenkins OIDC token come from?
The OpenID Connect Provider plugin on the controller issues it. The pipeline calls oidcToken with audience sts.amazonaws.com. We set sub to the job full name and confirm the claims on a sandbox job before the production trust policy is tightened.
Why not use the controller's instance profile for deploys?
An instance profile gives every build the controller's privileges and never checks the job name. The deploy identity is the assumed role, and only jobs whose sub matches the production folder can assume it. The controller role stays limited to running Jenkins.
What has to be deleted for the key to be gone?
The AWS credential in the Jenkins credential store, any copy bound to agents, any key baked into an agent image, and the matching access key in IAM. A pipeline that can also assume a role while the key remains stored has not finished the cutover.
Can one role trust every job on the controller?
We do not ship that trust. The condition limits sub to the production folder. Sandbox jobs and freestyle jobs outside that folder fail the assume. A wider subject is a standing privilege with a shorter secret.
What do you record for the auditor?
The issuer, the subject condition, a CloudTrail event id, and proof the credential store no longer holds AWS keys. That packet is filed in the GRC you already run. Rutagon builds the cutover. We do not claim your compliance attestation.
Talk to us about your credential elimination pilot. Reach Rutagon at rutagon.com/contact, 907-841-8407, or contact@rutagon.com.