Chapter 29Lesson 02~210 minutes

Security Hardening, CSRF, Agent-to-Controller Controls, Script Security, CSP, TLS, and Reverse Proxies: Guided Hands-On Workflow and Core Operations

Audit and harden a disposable Jenkins controller without turning security controls off: isolate the built-in node, verify CSRF behavior, inspect script approvals, model a TLS reverse proxy correctly, introduce CSP safely, and prove agent isolation with before/after evidence.

Hardening labZero executorsCSRF testsNginxSecurity headersAgent isolation

Learning objectives

  • Audit a disposable controller with a repeatable hardening checklist.
  • Move builds off the built-in node and verify queue/agent evidence.
  • Prove current CSRF behavior without exposing credentials.
  • Inspect Script Security approvals and stage CSP enforcement safely.
  • Validate reverse-proxy/TLS headers and agent isolation with bounded tests.

1. Disposable lab and trust assumptions

Use a Jenkins 2.568.3 LTS controller bound to loopback or an isolated Docker network and one disposable agent labeled trusted-lab. The controller has no production credentials, no proprietary repositories and no public DNS. The lab’s weakness is intentional but bounded: the built-in node initially has one executor, the general UI CSP is not yet enforced, and the reverse-proxy configuration exists only as a local simulation. CSRF and authorization remain enabled from the start.

Guardrail. Never reproduce a deliberately weak baseline on an internet-reachable or shared production controller. The exercise demonstrates strengthening controls, not disabling them.

2. Capture a before-state packet

Before changing anything, record versions, controller URL, ports, executor counts, agent labels, Script Security version and approval state. Use the UI plus read-only commands.

date -Iseconds
java -version
ss -ltnp | grep -E ':(8080|50000|443|8443)\\b' || true
curl -sS -D before-login.headers -o /dev/null http://127.0.0.1:8080/login

# Plugin inventory can be exported from Plugin Manager / installed-plugins list.
# Confirm script-security is 1422.v06869826dd9b_ or newer.

Record the built-in node’s executor count separately from agent executors. A queue delay is not proof that the controller is isolated.

3. Set built-in executors to zero

With the disposable agent online, open Manage Jenkins → Nodes → Built-In Node → Configure, change Number of executors from 1 to 0, save, then run a synthetic job pinned to trusted-lab. This changes controller node configuration, not the job’s source revision.

Verify the run’s node name, executor and workspace. Then intentionally mark the agent temporarily offline and trigger the same job. Expected evidence: the job waits for trusted-lab; Jenkins does not silently fall back to the controller.

pipeline {
  agent { label 'trusted-lab' }
  stages {
    stage('boundary') {
      steps { sh 'printf "node=%s workspace=%s\\n" "$NODE_NAME" "$WORKSPACE"' }
    }
  }
}

4. Verify CSRF behavior, not just its checkbox

Create only a disposable lab user with the minimum permission needed for the chosen test job. Do not paste credentials into the Jenkinsfile. For a username/password session-style client, first fetch a crumb and retain the session cookie. For API-token authentication, verify that the exact POST succeeds without a crumb because current Jenkins exempts token-authenticated requests.

# Keep values out of shell history. Example only; use a disposable account.
read -r JENKINS_USER
read -s JENKINS_API_TOKEN; export JENKINS_API_TOKEN; printf '\n'
export JENKINS_URL='http://127.0.0.1:8080'

# Safe read first.
curl -fsS -u "$JENKINS_USER:$JENKINS_API_TOKEN" \
  "$JENKINS_URL/job/security-lab/api/json?tree=fullName,url"

# Exact disposable target; API-token POST is crumb-exempt in current Jenkins.
curl -fsS -X POST -u "$JENKINS_USER:$JENKINS_API_TOKEN" \
  "$JENKINS_URL/job/security-lab/build"
unset JENKINS_API_TOKEN

Capture status codes and queue/build IDs, not the Authorization header. A 403 from a password/session client is a cue to inspect crumb/session handling—not to disable CSRF.

5. Inspect Script Security approvals

Open Manage Jenkins → In-process Script Approval. Record pending and approved signatures/scripts with their owners/reasons. Do not bulk-approve pending entries. If the controller is on Script Security 1415 or earlier, stop and update the plugin in a tested maintenance workflow before relying on sandbox boundaries.

Approval Question before keeping it Safer direction
Single signature Which job/library requires it? Can less capability solve the same need? Prefer a narrow, reviewed signature or a maintained plugin step.
Whole script Who can modify the script source? Pin/review source; avoid treating mutable repository code as trusted.
Classpath entry Is the exact JAR immutable and approved? Use reviewed dependencies; do not load attacker-controlled URLs.

6. Model a correct local TLS reverse proxy

The mandatory path is a faithful local configuration simulation, so learners do not need public DNS or a certificate authority. Store a self-signed lab certificate outside source control and model Nginx listening on 127.0.0.1:8443, forwarding to a controller reachable only on an isolated network/loopback interface.

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}
server {
  listen 127.0.0.1:8443 ssl;
  server_name jenkins.lab.test;
  ssl_certificate     /run/secrets/jenkins-lab.crt;
  ssl_certificate_key /run/secrets/jenkins-lab.key;

  location / {
    proxy_pass http://jenkins:8080;
    proxy_http_version 1.1;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }
}

If your Jenkins lives under a context path such as /jenkins, configure the same context path in Jenkins and the proxy. Do not attempt broad response-rewrite tricks to map unrelated paths.

Header verification

# Self-signed certificate: -k is acceptable ONLY for this isolated local lab check.
curl -k -sS -D proxy.headers -o /dev/null https://127.0.0.1:8443/login
# In production, trust the real CA instead of using -k.
grep -Ei '^(HTTP/|location:|content-security-policy:|set-cookie:)' proxy.headers

7. Stage CSP rather than breaking the UI blindly

On Jenkins 2.539+, inspect the CSP section under Manage Jenkins → Security. Jenkins can collect violation information while enforcement is not enabled. Exercise key plugin screens first, update incompatible plugins, then enable general UI CSP in the disposable clone. JCasC equivalent:

security:
  contentSecurityPolicy:
    enforce: true

This controls the general Jenkins UI. For workspace/archive user content, prefer a Resource Root URL for origin isolation. Do not set the user-content policy to an empty string just to make a report render.

8. Prove agent isolation

The agent must not have read/write access to JENKINS_HOME, must not share controller secrets and should run with an OS/container identity appropriate to its trust class. Current agent→controller protections are always enabled; there is no legitimate “turn it off for this plugin” step on Jenkins 2.568.3.

# Run on the agent as harmless evidence.
id
pwd
java -version
printf 'node=%s workspace=%s\n' "$NODE_NAME" "$WORKSPACE"
# A controller-home path should not be mounted/readable on this worker.

9. Before/after evidence

Control Before After proof
Built-in execution 1 executor 0 executors; job waits when agent is offline
CSRF Enabled, unverified Password/session requires crumb; API-token POST works without crumb
Script boundary Approval inventory unknown Reviewed list + Script Security 1422+
TLS/proxy Loopback HTTP Local HTTPS proxy model with correct forwarded/WebSocket headers
CSP Not enforced Compatibility tested, then enforced in disposable clone
Agent Trust undocumented Label, process identity and filesystem boundary recorded

10. Small challenge: choose the layer

A job returns 403 through the proxy. You can see the Jenkins login page and the user authenticates successfully. Before touching CSRF, decide which evidence distinguishes: authorization denial, password/session crumb mismatch, wrong reverse-proxy scheme/host, and a request sent to the wrong item path. Write the four checks and identify which layer each belongs to.

Next lesson

Configuration choices and tradeoffs

Compare direct TLS and reverse-proxy termination, staged versus strict CSP, sandboxed versus trusted Groovy, inbound TCP versus WebSocket agents, and plugin convenience versus controller attack surface.

Knowledge check

Answer before revealing the explanation.

1. If the trusted lab agent goes offline after the built-in executor count is zero, where should the job run?

2. A password-authenticated curl POST gets 403. What is the first CSRF-safe response?

3. Why do we inspect approvals before changing them?

4. When is curl -k acceptable in this lesson?

5. Does enabling general UI CSP fully isolate archived HTML from the Jenkins origin?

Official references and version notes

Verified baseline — 17 September 2026. Labs target Jenkins 2.568.3 LTS with Java 21 (Jenkins 2.568.3 is tested with Java 21 and 25) and Script Security 1422.v06869826dd9b_. Script Security 1415.v9a_f9b_3a_c253d and earlier are affected by multiple sandbox vulnerabilities disclosed 16 September 2026. General Jenkins UI CSP is a core feature in Jenkins 2.539+ but is disabled by default because plugin compatibility varies; introduce it using report evidence and testing. Re-check Jenkins core/plugin advisories before applying these patterns to a long-lived controller.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.