Store secrets in the Jenkins credentials store, reference them by ID, bind them to variables only inside the step that needs them, and use single quoted strings so the shell reads the value from its environment. Log masking helps with accidents, but Jenkins itself says it does not stop a determined user.
Where does Jenkins keep credentials?
Jenkins stores credentials encrypted on the controller, using the controller's ID as the key. A Pipeline never holds the secret in the Jenkinsfile. It holds only the credential ID, and Jenkins resolves the value at run time.
The docs list six credential types: secret text, username and password, secret file, SSH username with private key, certificate (a PKCS#12 file with an optional password), and Docker host certificate authentication. Pick the narrowest type. A deploy that needs one API token should use secret text, not a username and password pair that also unlocks a console.
Scope matters too. Global scope applies to a Pipeline item and everything below it. System scope is for the controller itself, such as email authentication or agent connections, and applies to a single object. Keep controller secrets in system scope so a pipeline cannot reach them.
If you leave the ID blank when creating a credential, Jenkins assigns a GUID that can never be changed. A readable ID such as deploy-staging-token makes reviews and audits much easier.
How do you bind a secret in a Jenkinsfile?
Declarative pipelines have the credentials() helper in an environment block. It supports secret text, username and password, and secret file credentials. A username and password credential produces three variables: the main one (username:password), plus _USR and _PSW variants. A secret file variable holds the path to a temporary file, not the contents.
Other types, such as SSH keys and certificates, use the withCredentials step. The docs recommend the Snippet Generator, which only offers credentials the project can access. Variables bound this way exist only inside the withCredentials block.
Scope the binding as tightly as you can. A variable declared at the top level of pipeline is visible to every stage. One declared inside a stage is visible only there. For a deploy secret, bind it in the deploy stage and nowhere else.
What goes wrong with Groovy string interpolation?
This is the mistake we see most. Groovy double quoted strings expand ${TOKEN} before the command reaches the agent. The secret is then copied into the process arguments, where a tool like ps can show it. Shell metacharacters in the value can also run at once.
Interpolation can also damage a secret that has special characters, such as % in a bat command. The docs point out that a mangled value no longer matches the masked secret, so it can show up unmasked in the log.
The fix is simple. Use single quoted strings, so the shell expands the variable from its own environment. Compare a constructed example: a step that runs curl with an Authorization header built inside a double quoted string leaks the token into the argument list. The same step in single quotes, with the shell variable inside, does not.
Is log masking enough?
No. When a credential variable is printed, Jenkins shows four asterisks. The docs say this is meant to reduce the risk of accidental disclosure. They also say it does not stop a malicious user from getting the value by other means, and that a Pipeline using trusted credentials can disclose them. Someone who can edit a Jenkinsfile can base64 encode a secret and print it, and the mask will not match.
So the real control is who can run which jobs. Do not let untrusted jobs or branches use trusted credentials. For multibranch and organization folder jobs, the Jenkins security page warns that credentials may be reachable by unprivileged users in ways you might not expect.
What to do
- Move every secret out of Jenkinsfiles, job configs and build parameters into the credentials store.
- Give each credential a readable ID and the narrowest type.
- Keep controller level secrets in system scope.
- Bind credentials per stage or per
withCredentialsblock, never globally. - Replace double quoted shell strings that contain secrets with single quoted ones.
- Run builds on agents, not the built-in node, and review who can edit Jenkinsfiles.
- Scan repositories and build logs for leaked tokens, and rotate anything that was ever printed.
Safeguard's secrets scanning can look for committed tokens in your repositories, which covers step 7 for source. See the secrets scanning demo to try it.
Quick check
Does masking make it safe to echo a secret?
No. Masking only prevents accidental exposure. The Jenkins docs say it does not stop a malicious user from obtaining the value by other means.
Why use single quotes around shell commands that use a credential?
Double quoted Groovy strings expand the secret before the command runs, which can expose it in process arguments. Single quotes let the shell read it from the environment.
Sources
- Using credentials, Jenkins documentation
- Using a Jenkinsfile, Jenkins documentation
- Securing Jenkins, Jenkins documentation
Last reviewed: 18 October 2026.