Lesson 007 · Jenkins Learning Path

Manage Jenkins Credentials Without Leaking Secrets

· Published · 5 min read

Labelled Jenkins CI/CD path separating untrusted pull request validation from trusted immutable artifact approval deployment monitoring and rollback

A credential in Jenkins is available to any build logic allowed to bind it. Because a Jenkinsfile and the scripts it calls are executable code, secret safety depends on authorization, folder design, Pipeline trust and agent isolation, not masking alone.

Choose the correct credential type

Credential typeUseTypical binding
Secret textAPI token or webhook tokenstring
Username with passwordRegistry or service loginusernamePassword
SSH username with private keyGit checkout, agent launch or deploymentsshUserPrivateKey
Secret fileCertificate, configuration or service-account materialfile
CertificatePKCS#12 client identityPlugin-specific
GitHub AppGitHub API and repository discoveryGitHub Branch Source configuration

Use a workload identity or short-lived token when the target platform supports it. A long-lived static key creates rotation and theft risk even when stored correctly.

Understand credential scope

  • System scope is for Jenkins internal functions such as agent connections; jobs should not consume it.
  • Global scope is visible to jobs in that Jenkins scope and is often broader than necessary.
  • Folder scope limits credentials to jobs under a team, product or environment folder and is normally the best default.
  • Use separate credentials for build, staging and production. Read permission on source does not imply production deployment permission.

Bind a secret text safely

stage('Call protected API') {
  steps {
    withCredentials([string(credentialsId: 'staging-api-token', variable: 'API_TOKEN')]) {
      sh '''set +x
        curl --fail --silent --show-error \
          -H "Authorization: Bearer $API_TOKEN" \
          https://api.example.com/health
      '''
    }
  }
}

The single-quoted Groovy string lets the shell expand $API_TOKEN at runtime. A double-quoted Groovy string may expose the value to process arguments or Jenkins before masking. set +x prevents shell tracing, but command output can still disclose data.

Bind username/password and SSH key

withCredentials([usernamePassword(
  credentialsId: 'registry-publisher',
  usernameVariable: 'REGISTRY_USER',
  passwordVariable: 'REGISTRY_PASSWORD'
)]) {
  sh '''set +x
    printf '%s' "$REGISTRY_PASSWORD" | \
      docker login registry.example.com --username "$REGISTRY_USER" --password-stdin
    trap 'docker logout registry.example.com >/dev/null 2>&1 || true' EXIT
    docker push registry.example.com/example/app@"$IMAGE_DIGEST"
  '''
}

withCredentials([sshUserPrivateKey(
  credentialsId: 'production-deploy-key',
  keyFileVariable: 'SSH_KEY',
  usernameVariable: 'SSH_USER'
)]) {
  sh '''set -euo pipefail
    ssh -i "$SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \
      "[email protected]" /usr/local/sbin/deploy-status
  '''
}

Keep secret files outside browsable workspace paths

Binding a secret file after entering dir('subdir') can place the temporary secret beneath that subdirectory's workspace. Bind first, then change directories, or allocate a separate temporary workspace.

withCredentials([file(credentialsId: 'service-config', variable: 'CONFIG_FILE')]) {
  dir('service') {
    sh '''set -euo pipefail
      ./ci/test-config.sh "$CONFIG_FILE"
    '''
  }
}

Do not archive the workspace, use env or printenv, create a support bundle, or run untrusted code while a secret is bound.

Protect pull requests and shared agents

Build contextAllowed credentialsAgent
Untrusted fork pull requestNone, or isolated read-only public dependency accessEphemeral untrusted validation pool
Trusted internal branchBuild and test credentials onlyBuild pool
Protected main branchArtifact publishing credential after testsTrusted publisher
Approved production promotionProduction deploy identity onlyRestricted deployment agent

Masking replaces matching strings in logs. It cannot stop a malicious step from encoding a secret, sending it over the network, reading another process, or leaving it in a file.

Rotation runbook

  1. Identify credential ID, owner, consumers, scope, target permissions and expiry.
  2. Create the replacement with equal or narrower privilege.
  3. Update a non-production folder and run checkout/build/deploy validation.
  4. Change remaining consumers without changing the stable credential ID where practical.
  5. Revoke the replaced credential and test that it no longer works.
  6. Record the rotation and next expiry; rotate immediately after suspected exposure.

Audit without printing values

  • Find Jenkinsfiles referring to credential IDs and assign an owner to each ID.
  • Review folder inheritance and who can configure jobs or approve trusted libraries.
  • Search console logs and artifacts for credential formats using an authorized secret scanner.
  • Check agents for abandoned temporary files and terminate persistent processes after builds.
  • Alert on authentication from unexpected agents, networks or repositories.

Unsafe patterns

Unsafe: hardcoding a token in a Jenkinsfile, job parameter, URL or command exposes it in source, history or logs. Unsafe: putting a production credential in the global domain makes it selectable by unrelated jobs. Unsafe: storing a private key in a workspace file and archiving **/* publishes it. Keep these patterns visible in review checklists and reject them.

Incident response for a leaked Jenkins credential

  1. Revoke or disable the credential at its authority first.
  2. Stop affected jobs and isolate agents that may retain the value.
  3. Preserve authorized evidence: job, build, commit, user, time, agent and destination.
  4. Remove the secret from current configuration and rotate related secrets.
  5. Review access logs for misuse and rebuild affected agents.
  6. Do not assume deleting the Jenkins console log removes copies from browsers, log forwarding or backups.

Rotate a credential without exposing its value

Design credentials around workload and environment, not around a convenient global list. A production deployment identity should not be visible to pull-request jobs or ordinary build agents. When the target supports short-lived workload identity, prefer it over a static secret. For stored credentials, document owner, target, scope, rotation interval and revocation procedure. Bind only across the exact step requiring the value and avoid command interpolation by Groovy, which can place secrets in process arguments before the shell applies quoting. Masking does not prevent a trusted Pipeline from encoding or transmitting a secret.

withCredentials([string(credentialsId: 'staging-api', variable: 'API_TOKEN')]) {
  sh '''
    set +x
    curl --fail --silent --show-error \
      -H "Authorization: Bearer $API_TOKEN" \
      https://api.staging.example.test/health
  '''
}
# Never echo, archive or fingerprint the temporary secret material

Evidence and failure exercise

LayerEvidenceFailure response
BindingCredential ID and bounded stageSecret is available only while required
AgentTrusted label and process ownershipOther workloads cannot inspect process/environment
OutputLog scan and artifact inventoryNo plaintext, transformed value or secret file retained
RotationOld and new credential test plus revocationNo shared key remains indefinitely

Create two lab tokens. Deploy with the first, replace the Jenkins credential under controlled change, test the second, revoke the first and prove it fails. Search console text, workspace, archived artifacts and temporary directories for a synthetic marker rather than a real secret.

Independent operating checks

  1. Test a pull request and confirm the protected credential is not bound.
  2. Use folder scope and prove a sibling folder cannot select the ID.
  3. Abort the job during the binding and inspect temporary-file cleanup.
  4. Record rotation without copying the value into the ticket.

Official references

Advertisement