skip to content

In a Jenkins Pipeline, what does the withCredentials step do, and how should a bound secret be referenced inside a sh step?

level: juniorimportance: must knowfreq 72%

answer

  1. pipeline holds the ID, not the value
  2. bound only inside the block
  3. one binding descriptor per credential kind
  4. single quotes, not Groovy interpolation
  5. masking catches mistakes, not design

basics

~20 s

withCredentials fetches a stored credential by its ID and exposes it as an environment variable only inside its block, masking the value in the console log. Inside sh, reference it with single quotes so Groovy never interpolates the secret.

solid answer

~50 s

Jenkins keeps secrets in a credentials store; a pipeline references one by its **ID**, never by value. `withCredentials([string(credentialsId: 'api-token', variable: 'TOKEN')]) { ... }` decrypts that entry, sets `TOKEN` as an environment variable for the duration of the block, and unsets it afterwards. Different binding types exist per credential kind: `string` for secret text, `usernamePassword` (two variables) or `usernameColonPassword`, `sshUserPrivateKey` and `file` (both write a temp file and give you its path), and `certificate`. The critical detail is quoting: `sh "curl -H 'X: ${TOKEN}'"` interpolates in Groovy on the controller, baking the literal secret into the script sent to the agent, where it shows up in the process list and defeats review; Jenkins even warns about it. Use `sh 'curl -H "X: $TOKEN"'` so the shell expands the variable in its own process. Log masking is a backstop, not the control.

code

groovy · 15 lines
groovy
pipeline {
  agent any
  stages {
    stage('Publish') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'registry',
                                          usernameVariable: 'REG_USER',
                                          passwordVariable: 'REG_PASS')]) {
          sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.example.com'
          sh 'docker push registry.example.com/app:$BUILD_NUMBER'
        }
      }
    }
  }
}

go deeper

for a junior

Know that the pipeline references a credential by ID, that withCredentials exposes it as an environment variable only inside its block, and that you reference it in sh with single quotes.

for a middle

Be able to name the binding descriptor for each credential kind, explain that file-based bindings write a temp file on the agent that is deleted at block exit, and explain exactly why Groovy interpolation leaks the value.

for a senior

Show that you narrow the block to the steps that need the secret, keep it off command lines and out of archived files, and treat console masking as a mistake-catcher rather than a control.

for a principal

Be ready to argue where the secret should come from at all — a long-lived entry in the Jenkins store versus a short-lived credential minted per build — and what that choice costs in blast radius when the controller is compromised.

## Where the secret actually lives Jenkins does not put secrets in the Jenkinsfile. They live in a **credentials store** managed by the Credentials plugin, encrypted on disk on the controller with the instance's key material under `$JENKINS_HOME/secrets`. Each entry has four properties that matter to a pipeline author: a **kind** (secret text, username/password, SSH username with private key, secret file, certificate), an **ID** you choose (`docker-registry`, `prod-deploy-key`), a **scope**, and an optional **domain** that restricts which URLs it may be used against. A pipeline only ever names the ID. That indirection is the whole design: the Jenkinsfile is reviewable, forkable and greppable, and it contains nothing worth stealing. ## The binding step `withCredentials` comes from the Credentials Binding plugin. It takes a list of binding descriptors and a block: ```groovy withCredentials([usernamePassword(credentialsId: 'registry', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS')]) { sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.example.com' } ``` The descriptor's name matches the credential kind: - `string(credentialsId:, variable:)` — secret text - `usernamePassword(credentialsId:, usernameVariable:, passwordVariable:)` — two variables - `usernameColonPassword(credentialsId:, variable:)` — one variable holding `user:pass`, handy for `curl -u` - `sshUserPrivateKey(credentialsId:, keyFileVariable:, usernameVariable:, passphraseVariable:)` — writes the key to a temporary file and hands you the path - `file(credentialsId:, variable:)` — secret file, again as a path - `certificate(credentialsId:, keystoreVariable:, passwordVariable:)` Bindings that materialise a file write it to a temporary location on the **agent** and delete it when the block exits. Variables are set only for the block; steps inside — and child processes they launch — inherit them, and nothing outside sees them. ## The quoting rule, which is the part interviews probe Groovy interpolates double-quoted strings **on the controller, before the command is sent anywhere**: ```groovy sh "curl -H 'Authorization: Bearer ${TOKEN}'" // WRONG sh 'curl -H "Authorization: Bearer $TOKEN"' // RIGHT ``` In the first form the secret becomes part of the script text written to the agent's temporary shell script, so it appears in `ps` output for anyone on that machine, in any trace of the script, and in error messages that echo the failed command. Jenkins emits an explicit warning that a secret was passed to `sh` using Groovy string interpolation. In the second form the shell — a process that already has the variable in its environment — does the substitution, and nothing containing the secret is ever written down. The same rule applies to `bat`, `powershell`, and to any step whose argument you build with `"..."`. ## The Declarative shortcut Declarative pipelines have a shorthand in an `environment` block: ```groovy environment { REGISTRY = credentials('registry') } ``` For a secret text credential `REGISTRY` holds the value; for a secret file it holds a path; for a **username/password** credential Jenkins sets three variables — `REGISTRY` as `user:pass`, plus `REGISTRY_USR` and `REGISTRY_PSW`. The `_USR`/`_PSW` suffixes surprise people who have only read about `withCredentials`. The shorthand binds for the whole scope of the `environment` block (the stage or the pipeline), which is broader exposure than a `withCredentials` block wrapped tightly around the two steps that need it. ## Masking is a backstop Jenkins replaces occurrences of the exact bound value in the console log with asterisks. That catches accidental `echo`s of the plain value. It does not catch a transformed value (base64, URL-encoded, split across lines), it does not protect artifacts or files you archive, and it does nothing about a process listing. Treat masking as a safety net for mistakes, and the quoting rule plus a tight block as the actual control. ## Failure modes to recognise - `Could not find credentials entry with ID '...'` — wrong ID, or the credential is in a store the job's context cannot see. - The variable is empty in a step *outside* the block — bindings do not survive the closure. - A `sh` step works interactively but not in the pipeline because the author used `${VAR}` in Groovy and the value contained shell metacharacters.

  • Why does Jenkins warn when a secret is passed to sh using Groovy string interpolation?
    Because interpolation happens on the controller before the command reaches the agent, so the literal secret is written into the temporary script file that runs there. Anyone with shell access or a process listing on that agent can read it, and it can surface in error output that echoes the command. Single-quoting defers expansion to the shell, which already holds the value in its environment.
  • What variables does environment { CRED = credentials('id') } create for a username/password credential in a Declarative Pipeline?
    Three: `CRED` holding `username:password`, plus `CRED_USR` and `CRED_PSW` for the two halves separately. For a secret text credential you just get the value in `CRED`, and for a secret file you get a path. The binding lasts for the whole scope of the `environment` block, which is wider exposure than a narrowly wrapped `withCredentials`.
  • How would you give a build an SSH deploy key without writing the key anywhere permanent?
    Bind it with `sshUserPrivateKey`, which decrypts the key into a temporary file on the agent and gives you the path in a variable such as `KEY`; point the tool at it (`GIT_SSH_COMMAND="ssh -i $KEY"`) inside the block. Jenkins deletes the file when the block exits, so nothing persists in the workspace between builds.

saying these in an interview costs you the question

  • Storing the token in a Jenkinsfile variable or job parameter instead
  • Assuming masking makes any usage of the secret safe
  • Using Groovy ${TOKEN} interpolation inside a sh string
  • Expecting the bound variable to persist after the block ends
  • Thinking withCredentials sends the secret from the agent to the controller

context