Jenkins Course
Securing Credentials
Credentials are the most sensitive data in your Jenkins instance. One leaked API key can expose your cloud infrastructure. One exposed deploy key can compromise your production servers. This lesson is about locking down secrets — how Jenkins protects them, where teams go wrong, and how to audit what you have.
This lesson covers
How Jenkins encrypts credentials at rest → The master key and its protection → Credential scopes and best practices → Auditing the credential store → Rotating credentials safely → The security mistakes that leak secrets
You've already seen how to store and use credentials in pipelines (Lesson 18) and how to manage access to the credential store via RBAC (Lesson 29). This lesson goes under the hood — how Jenkins actually encrypts credentials on disk, why losing JENKINS_HOME is different from just losing the data, and the operational practices that keep secrets safe over time.
The Analogy
Jenkins' credential store is like a safety deposit box at a bank. The bank (Jenkins) holds the box. Your key (the master encryption key) unlocks it. The contents (secrets) are safe as long as the key is protected. If someone steals the physical box but doesn't have the key, they get nothing. But if they steal both the box and the key — which is what happens when someone copies the entire JENKINS_HOME directory — they have everything. This is why protecting JENKINS_HOME is as important as protecting the credentials themselves.
How Jenkins Encrypts Credentials at Rest
Jenkins uses AES-128 encryption to protect credentials stored in JENKINS_HOME. The encryption process involves two files that work together:
hudson.util.Secret
The encryption engine
Jenkins' internal class that handles all encryption and decryption. When you store a credential, this class encrypts it. When a pipeline step requests it, this class decrypts it. The encryption key is never exposed to the pipeline — only the decrypted value is injected.
master.key
JENKINS_HOME/secrets/
The root encryption key for this Jenkins instance. Every credential is ultimately protected by this key. If this file is deleted or corrupted, all stored credentials become unreadable — Jenkins cannot decrypt them. If this file is copied alongside the credentials, anyone can decrypt them.
credentials.xml
JENKINS_HOME/
The file where all credential definitions are stored — their IDs, descriptions, types, and encrypted values. The encrypted values are safe without the master key. With the master key, they are readable. Back up both together or neither.
The JENKINS_HOME risk
A full backup of JENKINS_HOME contains both the encrypted credentials and the master key needed to decrypt them. This means a JENKINS_HOME backup is as sensitive as the plaintext secrets themselves. Store backups in a location with the same access controls as your production secret management system. Never commit a JENKINS_HOME backup to Git.
Credential Hygiene — The Rules That Matter
Every credential must have a clear ID and description
Six months from now, nobody will know what cred-42 is, who created it, or what it's for. A credential named docker-registry-prod-deployer with description "Push access to acmecorp.registry.io — owned by platform team" is actionable. A credential named key1 is a maintenance nightmare.
Rotate credentials on a schedule
API keys and passwords that never rotate are a compounding risk. A key that leaked six months ago and was never rotated is still being used. Set a calendar reminder — rotate all Jenkins credentials annually at minimum, or quarterly for production access credentials.
One credential per purpose — no sharing
If the payments pipeline and the frontend pipeline both use the same Docker registry credential, rotating it requires updating two pipelines. Worse — if one pipeline is compromised and the credential is revoked, both pipelines break. Create separate credentials per team or pipeline where possible.
Scope credentials as tightly as possible
A production SSH key scoped to Global means every developer and every pipeline can request it. Scope it to the specific folder that needs it. Use the folder-scoped credential store so only the right team's pipelines can access the right secrets.
Delete credentials you no longer use
An unused credential in Jenkins is still a valid key to something. If that system is later compromised and the key was never rotated, the attacker has working access. Quarterly — review the credential store and delete anything that's not referenced by an active pipeline.
Auditing the Credential Store
The scenario:
You're a platform engineer preparing for a security audit. The auditors want a list of all stored credentials — their IDs, types, scopes, and descriptions. They also want to know which pipelines reference which credentials so unused credentials can be identified. You need this report from the terminal without opening the browser.
New terms in this code:
- CredentialsProvider — a Jenkins class that provides access to all credentials across all stores and scopes.
CredentialsProvider.lookupCredentials()fetches credentials of a given type from a given context. - SystemCredentialsProvider — the root credential store — the global credential store you manage through the UI. Contains all credentials stored at the system level.
- Credentials interface — the base type for all credential objects. All specific types (usernamePassword, secretText, sshKey) implement this interface.
- credential.id — the credential's unique identifier — what you reference in
credentials('id')in your Jenkinsfile. - credential.description — the human-readable description. If this is empty or unhelpful, that's a finding.
// Run in the Script Console at /script to audit all stored credentials
// This script lists every credential's ID, type, scope, and description
// It does NOT expose any secret values — only metadata
import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.common.*
import com.cloudbees.plugins.credentials.domains.*
def stores = [
[name: 'System (Global)', store: SystemCredentialsProvider.getInstance().getStore()],
]
// Also check folder-level credential stores if any folders exist
Jenkins.instance.getAllItems(com.cloudbees.hudson.plugins.folder.Folder).each { folder ->
def folderStore = folder.getProperties()
.find { it instanceof FolderCredentialsProvider.FolderCredentialsProperty }
if (folderStore) {
stores << [name: "Folder: ${folder.name}", store: folderStore.getStore()]
}
}
println "=== CREDENTIAL AUDIT REPORT ==="
println "Generated: ${new Date()}"
println "=" * 60
stores.each { storeInfo ->
println "\n--- ${storeInfo.name} ---"
println String.format("%-35s %-20s %-15s %s", "ID", "Type", "Scope", "Description")
println "-" * 90
storeInfo.store.domains.each { domain ->
storeInfo.store.getCredentials(domain).each { cred ->
// Get the simple type name (e.g. "UsernamePasswordCredentialsImpl")
def type = cred.class.simpleName
.replace('CredentialsImpl', '')
.replace('Credentials', '')
def scope = cred.scope?.toString() ?: 'GLOBAL'
def desc = cred.description ?: '⚠ NO DESCRIPTION'
println String.format("%-35s %-20s %-15s %s",
cred.id, type, scope, desc)
}
}
}
// Summary
def allCreds = CredentialsProvider.lookupCredentials(
Credentials.class,
Jenkins.instance,
null,
(List) null
)
println "\n=== SUMMARY ==="
println "Total credentials: ${allCreds.size()}"
println "Credentials with no description: ${allCreds.count { !it.description }}"
Where to practice: Paste this script directly into the Script Console at http://localhost:8080/script. The output lists credential metadata only — no secret values are ever exposed or printed. After running it, identify any credentials with "⚠ NO DESCRIPTION" and update them immediately. Full Credentials API reference at github.com/jenkinsci/credentials-plugin.
=== CREDENTIAL AUDIT REPORT === Generated: Thu Mar 14 09:22:14 UTC 2024 ============================================================ --- System (Global) --- ID Type Scope Description ------------------------------------------------------------------------------------------ docker-registry-credentials UsernamePassword GLOBAL Docker registry push access — acmecorp.registry.io github-deploy-key BasicSSHUserPrivKey GLOBAL GitHub SSH deploy key — read/write to all repos slack-webhook-url StringCredentials GLOBAL Slack #deployments webhook — owned by platform team prod-server-ssh-key BasicSSHUserPrivKey GLOBAL ⚠ NO DESCRIPTION staging-deploy-key StringCredentials GLOBAL Staging environment deploy token — rotated 2024-01 aws-ecr-credentials UsernamePassword GLOBAL ⚠ NO DESCRIPTION old-jenkins-api-token StringCredentials GLOBAL ⚠ NO DESCRIPTION === SUMMARY === Total credentials: 7 Credentials with no description: 3
What just happened?
- Three credentials have no description —
prod-server-ssh-key,aws-ecr-credentials, andold-jenkins-api-token. The first two are clearly active but undocumented — immediate finding. The third has "old" in the name, which suggests it might be unused. These are the credentials to investigate first. - No secret values were exposed — the script reads only metadata.
cred.description,cred.id,cred.scope— none of these fields contain the actual secret. The credential's.secretor.passwordfield was never accessed. This is safe to run and safe to share with auditors. - Type names are simplified —
UsernamePassword,BasicSSHUserPrivKey,StringCredentials. The script strips the verbose Java class suffix so the output is readable. old-jenkins-api-token— the name alone flags this as a candidate for deletion. Someone created it, used it for something, and either forgot about it or it was replaced. It should be investigated and deleted if unused — it may be a valid API token to a running system.
Rotating a Credential Safely
Rotating a credential — replacing an old secret with a new one — is one of the most important security operations you'll perform on Jenkins. Done carelessly, it breaks pipelines mid-build. Done correctly, it's invisible to running jobs.
Generate the new secret on the remote system first
Create the new API key, SSH key, or password on the service that will use it. Don't revoke the old one yet — you need both active during the transition.
Update the credential in Jenkins
Manage Jenkins → Credentials → System → Global credentials → click the credential → Update. Replace the value. Click Save. The credential ID stays the same — all pipelines that reference it will automatically use the new value on their next run.
Trigger a test build to verify the new credential works
Run a non-critical pipeline that uses this credential. Confirm it passes. Check the console output for authentication errors.
Revoke the old secret on the remote system
Only after the test build passes, revoke the old API key or password. The old credential is now dead. Any pipeline still using a cached copy of the old value will fail — but no pipeline should be, because the credential store is the single source of truth.
The Five Credential Security Mistakes
Hardcoding secrets in Jenkinsfiles
A secret committed to Git is in the history permanently. Even if you remove it in the next commit, it lives in git log forever. Assume any secret ever committed to Git is compromised and rotate it immediately.
Printing credentials in pipeline output
Jenkins masks credentials in sh command output automatically. But if you do echo "password is ${MY_CRED}" in a Groovy echo step, the value may not be masked in all Jenkins versions. Never echo credentials explicitly — log that an operation succeeded, not what value was used.
Giving everyone Global scope access to production secrets
A production SSH key with Global scope is accessible to every pipeline on Jenkins — including pipelines written by contractors, interns, or compromised accounts. Use folder-scoped credentials and RBAC item roles together to ensure only the right pipelines can request the right secrets.
Storing credentials in environment variables at the system level
Setting secrets as system-wide environment variables in Manage Jenkins → Configure System means every pipeline on every agent has access to them as plaintext. This bypasses the credential store's masking and access controls entirely.
Never rotating credentials
A three-year-old API key that has never been rotated has had three years of potential exposure. Former employees, compromised systems, leaked logs — any of these could have exposed it without you knowing. Regular rotation limits the blast radius of any single exposure.
Teacher's Note
Run the credential audit script every quarter. Any credential with no description or with "old" in the name gets investigated first. Delete what's unused, document what remains, rotate what's overdue.
Practice Questions
1. What is the name of the file stored in JENKINS_HOME/secrets/ that is used to decrypt all stored Jenkins credentials?
2. Which file in JENKINS_HOME stores all credential definitions including their encrypted values?
3. Which Jenkins class do you use in a Groovy script to look up all credentials across all stores and scopes?
Quiz
1. Why must a JENKINS_HOME backup be treated with the same security controls as plaintext secrets?
2. What is the correct order of steps when rotating a credential?
3. What is the correct way to restrict a production SSH key so only the payments team's pipelines can use it?
Up Next · Lesson 31
Backup and Restore
What to back up, how to automate it, and exactly how to restore Jenkins from scratch in under 30 minutes — before you need to, not after.