Jenkins to AWS authentication with OIDC federation is one of those infrastructure changes that feels optional until it isn’t — until a rotated key breaks three pipelines at 2 AM, or until a leaked credential shows up in a CloudTrail anomaly report. This post covers the full setup: from registering the identity provider in AWS to injecting temporary credentials into a declarative Jenkinsfile, with Terraform handling the IAM side.
The Problem with Static Keys in Jenkins

The conventional approach — creating an IAM user, generating an access key pair, and storing it in Jenkins Credentials — works, but it carries compounding risk. Every key is a long-lived secret that must be manually rotated, tracked across environments, and protected against accidental exposure in logs or build output. When a team has dozens of pipelines, each potentially holding its own credential, the blast radius of a single compromise becomes significant.
Beyond the operational burden, static keys violate the principle of least privilege over time. Permissions granted for one use case tend to persist long after the original need disappears. Auditing which job used which key and when is difficult because CloudTrail records the IAM user identity, not the Jenkins job that invoked it.
The deeper issue is structural: Jenkins credentials storage was not designed to be a secrets vault. It provides convenience, not security boundaries. Storing AWS keys there is a workaround that scales poorly and degrades your security posture as the team grows. Jenkins to AWS authentication with OIDC removes the stored secret entirely by replacing it with a cryptographically signed, short-lived token that AWS validates directly.
How OIDC Federation Eliminates the Stored Secret
OpenID Connect federation allows AWS IAM to trust an external identity provider — in this case, your Jenkins instance — and accept tokens it issues as proof of identity. When a pipeline runs, Jenkins mints a JWT (JSON Web Token) containing claims that describe the job: the issuer URL, the subject (typically the job path), and the intended audience. AWS STS validates that token against the registered identity provider and, if the trust policy conditions match, returns a set of temporary credentials scoped to the configured IAM role.
The key architectural shift is that no secret ever leaves Jenkins. The token is signed with Jenkins’ private key; AWS verifies the signature using the public JWKS endpoint Jenkins exposes. The resulting STS credentials are valid for a configurable window (between 15 minutes and 12 hours) and are never stored anywhere — they exist only for the duration of the pipeline execution.
This pattern mirrors what GitHub Actions uses for AWS deployments, and it is now fully supported in Jenkins via the AWS Credentials Plugin combined with the OIDC extension. The STS API call involved is AssumeRoleWithWebIdentity, and every invocation produces a CloudTrail event that includes the full subject claim — giving you precise, per-job audit visibility that static keys can never provide.
One constraint worth noting upfront: your Jenkins instance must be reachable over HTTPS with a valid TLS certificate. AWS IAM needs to fetch the JWKS endpoint to verify token signatures, and the thumbprint of the root CA in the certificate chain must be registered with the identity provider. We will cover that in the next section.
Configuring the Jenkins OIDC Provider in AWS IAM
Before AWS will trust any token from Jenkins, you need to register your Jenkins instance as an IAM Identity Provider. The provider record tells AWS where to find the public keys and which audience claim to accept.
Start by confirming that your Jenkins OIDC discovery endpoint is reachable. The Jenkins OIDC plugin exposes its configuration at the standard path:
curl https://jenkins.kuryzhev.cloud/.well-known/openid-configuration
The response should include an issuer field matching your Jenkins URL and a jwks_uri pointing to the public key set. If this endpoint returns a 404, the OIDC plugin is either not installed or not enabled — resolve that before proceeding.
The thumbprint AWS requires is the SHA-1 fingerprint of the root CA certificate in the chain, not the leaf certificate. This is a common misconfiguration. To extract it:
# Extract the root CA thumbprint from the Jenkins TLS chain
openssl s_client -connect jenkins.kuryzhev.cloud:443 -showcerts 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/{n++} n==2{print}' \
| openssl x509 -noout -fingerprint -sha1 \
| sed 's/SHA1 Fingerprint=//' \
| tr -d ':'
With the thumbprint in hand, the Terraform configuration below creates the identity provider and the scoped IAM role in a single file. Keep this in your infrastructure repository alongside the rest of your IAM definitions.
# oidc_jenkins_aws.tf
# Creates the OIDC provider and a scoped IAM role for Jenkins pipelines
locals {
jenkins_url = "https://jenkins.kuryzhev.cloud"
jenkins_audience = "sts.amazonaws.com"
role_name = "jenkins-oidc-deploy-role"
# SHA-1 thumbprint of the root CA for jenkins.kuryzhev.cloud
tls_thumbprint = "9e99a48a9960b14926bb7f3b02e22da2b0ab7280"
}
resource "aws_iam_openid_connect_provider" "jenkins" {
url = local.jenkins_url
client_id_list = [local.jenkins_audience]
thumbprint_list = [local.tls_thumbprint]
tags = {
ManagedBy = "terraform"
Purpose = "jenkins-oidc"
}
}
data "aws_iam_policy_document" "jenkins_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.jenkins.arn]
}
condition {
test = "StringEquals"
variable = "${local.jenkins_url}:aud"
values = [local.jenkins_audience]
}
# Restrict to jobs under the "deploy" folder only
condition {
test = "StringLike"
variable = "${local.jenkins_url}:sub"
values = ["job/deploy/*"]
}
}
}
resource "aws_iam_role" "jenkins_oidc" {
name = local.role_name
assume_role_policy = data.aws_iam_policy_document.jenkins_assume_role.json
max_session_duration = 3600
tags = {
ManagedBy = "terraform"
}
}
# Attach only the permissions this role actually needs
resource "aws_iam_role_policy_attachment" "jenkins_deploy" {
role = aws_iam_role.jenkins_oidc.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" # replace with custom policy
}
output "jenkins_role_arn" {
description = "Pass this ARN into the Jenkins AWS OIDC credential configuration"
value = aws_iam_role.jenkins_oidc.arn
}
A few decisions in this configuration deserve explanation. The max_session_duration is set to 3600 seconds (one hour). If your jobs routinely run longer than this, increase it — but be aware that a mid-job credential expiry will cause silent failures in AWS SDK calls rather than an immediate pipeline error. Always set this value to comfortably exceed your longest expected job runtime.
The StringLike condition on the sub claim is what scopes this role to a specific Jenkins folder. The subject claim format Jenkins emits follows the job path structure: job/deploy/job/my-pipeline. Using a wildcard here means any job nested under the deploy folder can assume this role, while jobs in other folders cannot. You can find the full reference for IAM condition operators in the AWS IAM documentation on condition operators.
Installing and Configuring the AWS Credentials Plugin
On the Jenkins side, two plugins are required: the AWS Credentials Plugin (which provides the OIDC credential type) and the OpenID Connect Provider Plugin (which exposes the JWKS endpoint and issues tokens). Install both through Manage Jenkins → Plugin Manager and restart.
Once installed, navigate to Manage Jenkins → Credentials → System → Global credentials → Add Credentials. Select AWS OIDC Credentials from the kind dropdown. The form requires:
- ID — a stable identifier you will reference in Jenkinsfiles, for example
aws-oidc-deploy - Role ARN — the output value from the Terraform apply above
- Audience — must be
sts.amazonaws.com, matching what you configured in the IAM provider - Duration — the requested session duration in seconds; keep it consistent with
max_session_durationon the IAM role
There is no secret field to fill in. The credential record stores only configuration metadata — the role ARN, audience, and duration. The actual token exchange happens at job runtime when the AWS SDK calls STS with the JWT Jenkins generates for that specific build.
If your Jenkins instance sits behind a proxy or has a private hostname, confirm that the AWS STS endpoint (sts.amazonaws.com or a regional variant) is reachable from the Jenkins controller. Regional STS endpoints are preferable for latency and availability; you can configure the preferred region in the plugin settings or via the AWS_DEFAULT_REGION environment variable in your pipeline.
Injecting Credentials into a Declarative Pipeline
With the credential configured, referencing it in a Jenkinsfile is straightforward. The withAWS step from the AWS Credentials Plugin handles the token request and injects the standard AWS environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) into the build environment. Any AWS CLI or SDK call inside that block will use the temporary credentials automatically.
// Jenkinsfile — declarative pipeline using OIDC-backed AWS credentials
pipeline {
agent any
environment {
AWS_REGION = 'eu-central-1'
DEPLOY_BUCKET = 'kuryzhev-deploy-artifacts'
}
stages {
stage('Validate OIDC Token Exchange') {
steps {
withAWS(role: 'arn:aws:iam::123456789012:role/jenkins-oidc-deploy-role',
roleAccount: '123456789012',
region: "${AWS_REGION}",
webIdentityTokenFile: '') {
sh '''
# Confirm the identity STS resolved — should show the assumed role ARN
aws sts get-caller-identity
# Confirm no long-lived key is in use — session token must be present
if [ -z "$AWS_SESSION_TOKEN" ]; then
echo "ERROR: No session token — not using temporary credentials"
exit 1
fi
'''
}
}
}
stage('Deploy') {
steps {
withAWS(role: 'arn:aws:iam::123456789012:role/jenkins-oidc-deploy-role',
roleAccount: '123456789012',
region: "${AWS_REGION}") {
sh '''
aws s3 sync ./dist s3://${DEPLOY_BUCKET}/release/ \
--delete \
--sse AES256
'''
}
}
}
}
post {
failure {
echo 'Pipeline failed — check CloudTrail for AssumeRoleWithWebIdentity events if AWS calls succeeded before the failure.'
}
}
}
A few points on this pipeline structure. The withAWS block wraps only the steps that require AWS access — this is intentional. Scoping the credential binding tightly means the temporary credentials are requested exactly when needed and are not available to unrelated steps. If you have multiple stages deploying to different AWS accounts or roles, you can nest separate withAWS blocks with different role ARNs, and each will perform an independent STS exchange.
The inline check for AWS_SESSION_TOKEN is a lightweight guard against misconfiguration. Temporary credentials from STS always include a session token; if that variable is empty, something fell back to a static key or instance profile unexpectedly. Making this check explicit in the pipeline catches configuration drift early.
For teams managing multiple environments, consider parameterising the role ARN rather than hardcoding it. A pattern we cover in the context of environment promotion on kuryzhev.cloud is storing role ARNs in a shared library or parameter store and resolving them by environment name at pipeline start — this keeps the Jenkinsfile environment-agnostic and the IAM configuration centralised.
Verify It Works
Run the pipeline and examine the output of aws sts get-caller-identity. A successful OIDC exchange produces output similar to this:
{
"UserId": "AROA5EXAMPLE:jenkins-job-deploy-my-service-42",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/jenkins-oidc-deploy-role/jenkins-job-deploy-my-service-42"
}
The session name embedded in the ARN is derived from the Jenkins build context — this is what makes CloudTrail useful here. Every AWS API call this job makes will appear in CloudTrail under that assumed-role ARN, with the session name identifying the specific build. Compare this to a static key, where all calls appear under a single IAM user with no job-level attribution.
To confirm that no static keys remain in Jenkins, navigate to Manage Jenkins → Credentials and audit the credential store. You should see the OIDC credential record (which contains no secret material) and nothing of kind AWS Credentials with an access key ID. If any remain from the previous setup, revoke them in IAM immediately and remove them from Jenkins.
In CloudTrail, filter for event name AssumeRoleWithWebIdentity in the region where your STS calls land. Each successful pipeline run should produce one event per withAWS block. The event detail includes the subjectFromWebIdentityToken field, which contains the Jenkins job path — this is your primary audit trail for who did what and when.
If the token exchange fails, the most common causes are: a thumbprint mismatch (verify with the openssl command above), an audience claim mismatch (the plugin config and IAM provider must agree exactly on sts.amazonaws.com), or a subject claim that does not match the StringLike condition in the trust policy. Enable Jenkins system logs at DEBUG level for the AWS Credentials Plugin to see the raw JWT claims being sent to STS.
Related Ideas Worth Pursuing
Per-environment role scoping. The pattern shown here uses a single role for the deploy folder. A more mature setup creates separate roles for staging and production, each with appropriately scoped permissions, and uses the sub claim condition to enforce which Jenkins folders can assume which role. This prevents a staging pipeline from accidentally deploying to production infrastructure, regardless of what the Jenkinsfile contains.
Service Control Policies as a backstop. Even with OIDC in place, an SCP that denies iam:CreateAccessKey and iam:CreateUser across non-identity accounts ensures that no pipeline can create new static credentials even if it has broad IAM permissions. This is defence-in-depth: OIDC removes the need for static keys, and the SCP makes it structurally impossible to reintroduce them accidentally.
Extending the pattern to other providers. The same OIDC federation mechanism works for GitLab CI, GitHub Actions, and Kubernetes service accounts (via IAM Roles for Service Accounts). Once you have the mental model — identity provider issues a signed JWT, AWS validates it against a registered provider, STS returns scoped temporary credentials — the implementation details for each platform are variations on the same theme. The AWS IAM OIDC provider documentation covers the federation model in depth and is worth reading alongside this setup.
Alerting on anomalous assume-role patterns. CloudTrail events for AssumeRoleWithWebIdentity can feed into EventBridge rules that alert on unexpected source accounts, unusual session durations, or calls from subject claims that do not match known job paths. This gives you a detection layer on top of the prevention layer OIDC provides — a combination that holds up well under audit scrutiny.
