skip to content

Credentials & Security

The credentials store and its types, binding secrets into a pipeline with withCredentials, and locking the instance down with matrix or role-based authorization and CSRF protection. Interviewers ask because Jenkins holds deploy keys for everything, which makes it a high-value target and a common source of secrets leaked into build logs.

part ofJenkinsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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

open as a page

In Jenkins, what do the credential scopes System, Global and User control, and why can a job report "Could not find credentials entry with ID" for an ID an administrator can plainly see?

level: middleimportance: should knowfreq 45%

basics

~20 s

Scope decides who may use a Jenkins credential: System means the controller itself only (agent launches, cloud configuration) and is invisible to jobs; Global means the storing object and its children; User means one person's private store. A System-scoped entry produces the "not found" error in a build.

open as a page

On a Jenkins controller where every developer has permission to configure any job, why is that effectively the same as handing out every credential, and how do you restrict it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Anyone who can change what a job runs can bind any credential that job's context can see and send the value anywhere, so job-configure permission equals credential access. Restrict it with matrix or role-based authorization, per-team folders holding their own credentials, and pipelines defined in reviewed source control.

open as a page

A Jenkins build printed a usable API token into its console log even though the value was bound with withCredentials and Jenkins masks credentials. How does that happen, and what do you do about it?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Jenkins masks only exact literal occurrences of the bound value in the console log. Any transformation — base64, URL-encoding, JSON escaping, splitting across lines — passes through unmasked, and masking never covers archived files or process listings. Rotate the credential first, then fix the pipeline.

open as a page

A script that POSTs to a Jenkins controller's REST API is rejected with HTTP 403 and "No valid crumb was included in the request". What is Jenkins' crumb, and how should the script satisfy it?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

The crumb is Jenkins' CSRF token: state-changing requests must carry a value fetched from the controller's crumb issuer, sent in a request header on the same session. Scripts should either fetch the crumb per session or authenticate with a user API token, which is exempt in current Jenkins versions.

open as a page