Credentials binding done right

withCredentials, masking, and scope.

Intermediate14 min · lesson 11 of 15

Pipelines need secrets — a registry password, a deploy token, an SSH key — and how you handle them is the security heart of this course. The rule is absolute: secrets never appear in the Jenkinsfile, in the repo, or in environment{}. They live in Jenkins’ credentials store (encrypted in JENKINS_HOME), scoped to who may use them, and are injected into a build only for the moment they are needed.

HOW A SECRET REACHES A BUILD SAFELY
1Encrypted store
credential kept in JENKINS_HOME, referenced by id
2withCredentials block
resolves the id, binds value to an env var
3Step uses the var
secret available only for these steps
4Log masking
exact stored string replaced with ****
5Block ends
variable unset; secret gone from the pipeline
The Jenkinsfile only names an id; the value lives encrypted and is exposed for the narrowest possible window.
Jenkinsfile
stage('Push') {
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-creds', // an id, never the value
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS')]) {
sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.acme.internal'
}
}
}

withCredentials scopes the exposure

withCredentials binds a stored credential to environment variables only inside its block, so the secret exists for those few steps and nowhere else in the pipeline. Jenkins also masks the value in the build log — if the secret string appears in output, it is replaced with ****. Reference the credential by its id, pass it to commands via the variable, and let the block’s scope limit how far it can leak.

For declarative pipelines there is also the credentials() helper in an environment block, which is convenient for a single credential across a stage. Either way the principle holds: the Jenkinsfile names an id; the value is resolved from the encrypted store at run time.

Jenkinsfile
environment {
// resolves the stored secret text into GH_TOKEN for this scope only
GH_TOKEN = credentials('github-token')
}
Masking is best-effort — never echo secrets
Log masking only catches the exact stored string. If your script transforms a secret (base64-encodes it, prints part of it, writes it to a file that gets archived), the masking misses it and the credential leaks in plain sight. Never echo or debug-print a secret, keep it in withCredentials scope, and scope credentials tightly (per-folder, per-agent) so a compromised job cannot reach production secrets.