Jenkins Course
Credentials Management
Secrets hardcoded in pipelines get committed to Git. Git commits live forever. This lesson is about doing it right — storing credentials securely in Jenkins and injecting them into pipelines without ever exposing them in a log or a repository.
This lesson covers
The Jenkins credential store → Credential types → Storing credentials in the UI → Injecting credentials into pipelines → Scopes → The most common mistakes teams make
Every team that hasn't set this up properly has the same story. A developer needed to connect Jenkins to a Docker registry. In a hurry, they typed the password directly into the Jenkinsfile. It got committed. Three years later, that password is in a public GitHub repo and someone's using the registry for free crypto mining. It takes five minutes to do this correctly. Those five minutes are worth it.
The Analogy
The Jenkins credential store is a safe inside the building. Your pipeline doesn't carry the key — it tells the safe "I need item 42" and the safe hands it over, masked, just for that build. The key never leaves the safe. The value never appears in any log. Anyone who can read the pipeline code only sees the ID — not the secret.
The Jenkins Credential Store
Jenkins stores all credentials in an encrypted store. The credentials are encrypted at rest on the Jenkins master using a key that lives in JENKINS_HOME/secrets/. Even if someone gets access to the raw files on disk, the credentials are not readable without the encryption key.
To add a credential, go to Manage Jenkins → Credentials → System → Global credentials (unrestricted) → Add Credentials. Here's what that screen looks like:
The Credential Types You'll Use Most
Username with password
usernamePassword()
Stores a username and password pair. Jenkins creates two environment variables: CREDS_USR (the username) and CREDS_PSW (the password). Used for Docker registries, Maven repositories, and any service requiring a login.
Secret text
string()
A single masked string — API keys, Slack webhook URLs, auth tokens. Creates one environment variable with the value masked in all logs. The most common type for modern API-based integrations.
SSH username with private key
sshUserPrivateKey()
Stores an SSH private key. Jenkins writes it to a temporary file on the agent during the build and injects the file path as an environment variable. Used for SSH-based Git authentication and server deployments.
Secret file
file()
Stores a file — a kubeconfig, a service account JSON key, a .pem certificate. Jenkins writes it to a temporary location during the build and gives you the path. Deleted when the build finishes.
Certificate (PKCS#12)
certificate()
Stores a PKCS#12 certificate with its private key. Used for mutual TLS authentication and code signing. Less common but essential in regulated industries.
Scopes — Who Can See What
When you create a credential, you choose a scope. Scope controls which jobs and systems can use it. Getting scope right is part of the principle of least privilege — a credential should only be accessible to the jobs that genuinely need it.
Global
Available everywhere — all pipelines, all nodes, all child items. Use for shared credentials like a Docker registry that every team's pipeline needs.
System
Only available to Jenkins itself — for connecting to agents via SSH, sending email, or other internal operations. Not accessible inside pipeline steps.
Folder-scoped
Available only to jobs inside a specific folder. Use for team-specific credentials — the payments team's deploy key should only be accessible to payments team jobs.
Three Ways to Inject Credentials Into a Pipeline
The scenario:
You're a DevOps engineer building a deployment pipeline for the analytics-service. The pipeline needs three different secrets: a Docker registry login, a Slack webhook URL for notifications, and an SSH key to deploy to the production server. Each is stored in the Jenkins credential store. Here are the three ways to inject them, depending on the use case.
New terms in this code:
- environment { credentials('id') } — injects a credential as a pipeline-wide environment variable. Available to all stages. Jenkins automatically creates suffixed variables (
_USR,_PSW) for username/password types. - withCredentials([ ]) — a step that injects credentials only for the duration of the block. More granular than environment injection — use when you only need the secret in one place. The credential is unavailable outside the block.
- usernamePassword() — the binding type inside
withCredentialsfor username/password credentials. You specify which variable names to use for username and password. - string(credentialsId:, variable:) — the binding type for secret text credentials. Injects the secret text as a single named variable.
- sshUserPrivateKey() — the binding type for SSH credentials. Jenkins writes the private key to a temp file and injects the file path as a variable.
pipeline {
agent { label 'linux' }
// Method 1: environment block injection
// Credentials are available to ALL stages as environment variables
// Best for credentials used in multiple stages
environment {
// For a username/password credential, Jenkins creates two variables:
// DOCKER_CREDS_USR = the username
// DOCKER_CREDS_PSW = the password (masked in logs)
DOCKER_CREDS = credentials('docker-registry-credentials')
// For a secret text credential, creates one variable with the secret value
SLACK_WEBHOOK = credentials('slack-webhook-url')
}
stages {
stage('Build and Push Image') {
steps {
// Use the variables injected by the environment block
// DOCKER_CREDS_USR and DOCKER_CREDS_PSW are available here
sh """
docker login \
-u ${DOCKER_CREDS_USR} \
-p ${DOCKER_CREDS_PSW} \
registry.acmecorp.com
docker build -t registry.acmecorp.com/analytics-service:${BUILD_NUMBER} .
docker push registry.acmecorp.com/analytics-service:${BUILD_NUMBER}
"""
}
}
stage('Deploy to Production') {
when { branch 'main' }
steps {
// Method 2: withCredentials block
// The SSH key is only available inside this block
// Best for credentials needed in just one place
withCredentials([
sshUserPrivateKey(
credentialsId: 'prod-server-ssh-key',
keyFileVariable: 'SSH_KEY_FILE', // path to the temp key file
usernameVariable: 'SSH_USER' // the SSH username
)
]) {
// SSH_KEY_FILE contains the path to the private key file Jenkins created
sh """
ssh -i ${SSH_KEY_FILE} \
-o StrictHostKeyChecking=no \
${SSH_USER}@prod-server-01.acmecorp.com \
'docker pull registry.acmecorp.com/analytics-service:${BUILD_NUMBER} && \
docker-compose up -d'
"""
}
// SSH_KEY_FILE and SSH_USER are no longer available here
// Jenkins deletes the temporary key file after the block closes
}
}
stage('Notify') {
steps {
// Method 3: withCredentials for a secret text credential
// Useful when you need fine-grained control over when the secret is used
withCredentials([
string(
credentialsId: 'slack-webhook-url',
variable: 'SLACK_URL'
)
]) {
sh """
curl -X POST \
-H 'Content-type: application/json' \
--data '{"text":"analytics-service ${BUILD_NUMBER} deployed to production"}' \
${SLACK_URL}
"""
}
}
}
}
post {
failure {
// SLACK_WEBHOOK is available here — it was injected at pipeline level
sh """
curl -X POST \
-H 'Content-type: application/json' \
--data '{"text":"analytics-service pipeline FAILED — build ${BUILD_NUMBER}"}' \
${SLACK_WEBHOOK}
"""
}
always { cleanWs() }
}
}
Where to practice: In your Jenkins instance, go to Manage Jenkins → Credentials → System → Global credentials → Add Credentials. Add a "Secret text" credential with ID slack-webhook-url and paste any test value. Then add the Jenkinsfile above and observe how Jenkins masks the value in the console output. Full credentials binding reference at jenkins.io — Credentials Binding Plugin.
Started by SCM change
[Pipeline] Start of Pipeline
[Pipeline] node (agent-linux-01)
[Pipeline] withCredentials
Masking supported pattern matches of $DOCKER_CREDS_PSW or $DOCKER_CREDS_USR
Masking supported pattern matches of $SLACK_WEBHOOK
[Pipeline] { (Build and Push Image) }
[Pipeline] sh
+ docker login -u deployer -p **** registry.acmecorp.com
Login Succeeded
+ docker build -t registry.acmecorp.com/analytics-service:91 .
Successfully built 4f2a1b3c
+ docker push registry.acmecorp.com/analytics-service:91
Pushed: registry.acmecorp.com/analytics-service:91
[Pipeline] { (Deploy to Production) }
[Pipeline] withCredentials
Masking supported pattern matches of $SSH_KEY_FILE or $SSH_USER
[Pipeline] sh
+ ssh -i /tmp/ssh-key-14729483 -o StrictHostKeyChecking=no deployer@prod-server-01.acmecorp.com ...
Pulling analytics-service:91...
Container restarted. Status: Up 3 seconds
[Pipeline] { (Notify) }
[Pipeline] withCredentials
Masking supported pattern matches of $SLACK_URL
[Pipeline] sh
+ curl -X POST -H 'Content-type: application/json' --data ... ****
ok
[Pipeline] End of Pipeline
Finished: SUCCESS
What just happened?
Masking supported pattern matches of $DOCKER_CREDS_PSW— Jenkins announces at the start of the build which variables it will mask. Every time the value ofDOCKER_CREDS_PSWappears anywhere in the console output, Jenkins replaces it with****. This is automatic — you don't need to do anything extra.-p ****— the Docker password was passed to the command, but Jenkins masked it before printing. The actual password was used correctly — it just never appeared in any log./tmp/ssh-key-14729483— Jenkins created a temporary file on the agent containing the SSH private key and injected its path asSSH_KEY_FILE. This file is created at the start of thewithCredentialsblock and deleted when the block closes. The key never lives on disk any longer than necessary.--data ... ****— the Slack webhook URL was masked in the curl command. Even though it was passed as a command argument, Jenkins caught it and replaced it with stars. This applies to all output — shell commands, echoes, error messages — not just specific locations you tell Jenkins about.- The Notify stage used
withCredentialseven though the same credential is in the environment block — this was intentional in the example to show both methods. In practice, if a credential is already in theenvironmentblock, you can just use the variable directly in any stage without a secondwithCredentialsblock.
The Four Mistakes That Leak Credentials
Printing credentials with echo
Even though Jenkins masks credentials in shell output, it does not mask them if you explicitly echo them with echo "My password is ${MY_CRED}" in a Groovy context. Never echo credential variables directly. Jenkins masks the value in sh output, but Groovy string interpolation in echo steps bypasses the masking in some versions.
Hardcoding secrets in the Jenkinsfile
The Jenkinsfile is committed to Git. Anything hardcoded in it is in your Git history permanently — even if you remove it later. Every secret must come from the credential store, never from the Jenkinsfile itself.
Using Global scope for everything
A production deploy key with Global scope is accessible to every job on Jenkins — including jobs run by contractors, interns, or compromised pipeline scripts. Use folder-scoped credentials for anything sensitive and limit Global scope to truly shared infrastructure credentials.
Storing credentials without a description
Six months from now, nobody will know what cred-42 is or who owns it. Every credential must have a clear ID, description, and owner. A credential store with 80 mystery entries is as dangerous as no credential store — someone will delete the wrong one.
Teacher's Note
Audit your credential store every quarter. Delete anything unused. Rotate anything that hasn't been rotated in a year. Treat it like a production database — with care and a maintenance schedule.
Practice Questions
1. Which pipeline step injects a Jenkins credential only for the duration of its block, making it unavailable outside?
2. If you inject a username/password credential with ID docker-registry-credentials using DOCKER_CREDS = credentials('docker-registry-credentials'), what is the name of the variable that holds the username?
3. Which withCredentials binding type writes an SSH private key to a temporary file on the agent and injects the file path as a variable?
Quiz
1. How does the Jenkins credential store protect secrets?
2. A payments team has an SSH key that should only be usable by their own pipeline jobs. Which credential scope should you use?
3. A developer wants to use an API key in their pipeline. What is the correct approach?
Up Next · Lesson 19
Pipeline Triggers
SCM polling, webhooks, schedules, and upstream triggers — how to make your pipeline fire automatically at exactly the right moment.