Skip to main content
Jenkins · lesson 2 of 5
Jenkins beginner Lesson 2 of 5

Jenkins: Credentials & Agents

Use Jenkins credentials without leaking secrets. Control execution using labels/agents and archive artifacts between stages.

By SkillByExample · Updated

This tutorial extends Jenkins pipeline knowledge with two enterprise essentials:

  • credentials (secure secret management)
  • agents/nodes (running the right stage in the right place)

1) Jenkins credentials: avoid hardcoding

Jenkins stores secrets in its credentials system. You reference them by credentialsId.

Common credential types:

  • Secret text (token)
  • Username/password
  • SSH keys
  • Files (e.g., kubeconfig)

Example: secret text token

withCredentials([
  string(credentialsId: 'API_TOKEN', variable: 'API_TOKEN')
]) {
  sh '''
    echo "Calling external API"
    curl -sS -H "Authorization: Bearer $API_TOKEN" https://api.example.com
  '''
}

Rules of thumb:

  • do not print the token
  • avoid set -x
  • keep logs clean

2) Agents: choose where stages run

Declarative: label-based

pipeline {
  agent none

  stages {
    stage('Build') {
      agent { label 'linux-x64' }
      steps {
        sh 'npm ci'
        sh 'npm run build'
      }
    }

    stage('Test') {
      agent { label 'linux-x64' }
      steps {
        sh 'npm test'
      }
    }
  }
}

Declarative: Docker agent

pipeline {
  agent {
    docker {
      image 'node:20-alpine'
      args '-u node'
    }
  }

  stages {
    stage('Build') {
      steps {
        sh 'npm ci'
        sh 'npm run build'
      }
    }
  }
}

3) Artifacts: keep outputs for later

Archive artifacts so you can download them from the build page.

stage('Build') {
  steps {
    sh 'npm ci'
    sh 'npm run build'
    archiveArtifacts artifacts: 'dist/**', fingerprint: true
  }
}

4) Practical pipeline example (theory + code)

A typical CI flow:

  1. checkout
  2. install deps
  3. build
  4. test
  5. archive artifacts
pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps { checkout scm }
    }

    stage('Install') {
      steps { sh 'npm ci' }
    }

    stage('Build') {
      steps { sh 'npm run build' }
    }

    stage('Test') {
      steps { sh 'npm test' }
    }

    stage('Archive') {
      steps { archiveArtifacts artifacts: 'dist/**', fingerprint: true }
    }
  }
}

Practice

These solutions ran on a jenkins/jenkins:lts container with the Pipeline plugins. Its built-in node carries the label linux-x64, and two credentials exist: secret text API_TOKEN (value s3cr3t-token-123) and username/password REGISTRY_LOGIN (user deploy-bot). Each task is a Pipeline job with the Jenkinsfile pasted as its script, run with jenkins-cli.jar build -s -v, which waits for the build and streams its log.

  1. See where log masking stops, on your own Jenkins and a throwaway credential. Inside the secret-text example, replace the curl script with one that pipes the token through base64 and through rev (which reverses a string).

    Solution
    pipeline {
      agent any
    
      stages {
        stage('Call API') {
          steps {
            withCredentials([
              string(credentialsId: 'API_TOKEN', variable: 'API_TOKEN')
            ]) {
              sh '''
                echo "$API_TOKEN" | base64
                echo "$API_TOKEN" | rev
              '''
            }
          }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build masking-limits -s -v
    Started masking-limits #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/masking-limits
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Call API)
    [Pipeline] withCredentials
    Masking supported pattern matches of $API_TOKEN
    [Pipeline] {
    [Pipeline] sh
    + echo ****
    + base64
    ****wo=
    + echo ****
    + rev
    321-nekot-t3rc3s
    [Pipeline] }
    [Pipeline] // withCredentials
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    Finished: SUCCESS
    Completed masking-limits #1 : SUCCESS

    The + trace lines appear without any set -x, because Jenkins already runs sh scripts with tracing on. Masking is a find-and-replace on the log. It caught the exact secret and even its base64 form, but the reversed token went into the log in the clear. Masking is a convenience, not a security boundary. Anyone who can edit a Jenkinsfile can get any credential that job can bind. So limit who can change pipelines, scope credentials to the folders that need them, and rotate any secret that reaches a log.

  2. Misspell the credential ID as 'API_TOKN' in the secret-text example, and replace the script with sh 'echo "calling API"'. Does the sh step run with an empty token?

    Solution
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build wrong-id -s -v
    Started wrong-id #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/wrong-id
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Call API)
    [Pipeline] withCredentials
    [Pipeline] // withCredentials
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    ERROR: Could not find credentials entry with ID 'API_TOKN'
    Finished: FAILURE
    Completed wrong-id #1 : FAILURE

    No. withCredentials fails before entering its block, so “calling API” never prints. A wrong ID is a loud failure that names the ID. If you see an empty secret instead, suspect the variable name in your script, not the ID.

  3. Bind the username/password credential REGISTRY_LOGIN. Use usernamePassword(credentialsId: 'REGISTRY_LOGIN', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS') in place of string(...), then print the username, the password’s length, and the password.

    Solution
    pipeline {
      agent any
    
      stages {
        stage('Login') {
          steps {
            withCredentials([
              usernamePassword(credentialsId: 'REGISTRY_LOGIN',
                               usernameVariable: 'REG_USER',
                               passwordVariable: 'REG_PASS')
            ]) {
              sh 'echo "logging in as $REG_USER with a ${#REG_PASS}-character password"'
              sh 'echo "password is $REG_PASS"'
            }
          }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build registry-login -s -v
    Started registry-login #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/registry-login
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Login)
    [Pipeline] withCredentials
    Masking supported pattern matches of $REG_PASS
    [Pipeline] {
    [Pipeline] sh
    + echo logging in as deploy-bot with a 16-character password
    logging in as deploy-bot with a 16-character password
    [Pipeline] sh
    + echo password is ****
    password is ****
    [Pipeline] }
    [Pipeline] // withCredentials
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    Finished: SUCCESS
    Completed registry-login #1 : SUCCESS

    One credential becomes two variables, and only the password is masked. The “Masking … of $REG_PASS” line lists exactly which variables Jenkins hides, and the username deploy-bot prints in the clear.

  4. In the label-based example, replace the npm steps in Build with sh 'echo "building on $NODE_NAME (labels: $NODE_LABELS)"'. Then delete the agent line from the Test stage, rename the stage to Report, and make its step sh 'echo "reporting"'.

    Solution
    pipeline {
      agent none
    
      stages {
        stage('Build') {
          agent { label 'linux-x64' }
          steps {
            sh 'echo "building on $NODE_NAME (labels: $NODE_LABELS)"'
          }
        }
    
        stage('Report') {
          steps {
            sh 'echo "reporting"'
          }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build agent-none -s -v
    Started agent-none #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] stage
    [Pipeline] { (Build)
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/agent-none
    [Pipeline] {
    [Pipeline] sh
    + echo building on built-in (labels: linux-x64 built-in)
    building on built-in (labels: linux-x64 built-in)
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] stage
    [Pipeline] { (Report)
    [Pipeline] sh
    [Pipeline] error
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] End of Pipeline
    ERROR: Attempted to execute a step that requires a node context while ‘agent none’ was specified. Be sure to specify your own ‘node { ... }’ blocks when using ‘agent none’.
    Finished: FAILURE
    Completed agent-none #1 : FAILURE

    With agent none, a stage only gets a machine if it asks for one. In Build, [Pipeline] node appears inside the stage. That is where the label was matched to the built-in node. Report never allocated a node, so its sh had no workspace to run in.

Next steps

Next tutorials:

  • Monitoring & Observability fundamentals
  • Terraform for reproducible environments
  • Ansible for idempotent configuration

Frequently Asked Questions

Why are my secrets empty in Jenkins?
Often because the credential ID is wrong, or you’re using the wrong credentials type (string/file/username/password). Also check that the job has access to the credential.
When should I use agents vs running everything on one node?
Use agents to run stages on the correct environment (OS/tools) and to scale parallel builds. Use one node when simplicity matters and resources are constrained.