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.
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.
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.
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?
It should remain queued for an eligible agent. Falling back to the controller would defeat the isolation control.
2. A password-authenticated curl POST gets 403. What is the first CSRF-safe response?
Preserve the response, fetch a crumb with the same authenticated session/cookie, resend the exact request, and verify authorization. Do not disable CSRF.
3. Why do we inspect approvals before changing them?
Approvals are persisted security state. Removing one can break jobs; adding one broadens controller capability. Evidence ties each approval to a consumer and supports a narrow rollback.
4. When is curl -k acceptable in this lesson?
Only for the explicitly isolated self-signed local TLS verification. Production verification must trust the real certificate chain rather than bypassing it.
5. Does enabling general UI CSP fully isolate archived HTML from the Jenkins origin?
No. User-generated workspace/archive content has a separate risk model; a Resource Root URL provides stronger origin isolation.
Official references and version notes
- Jenkins LTS changelog — confirms Jenkins 2.568.3 (2 September 2026) and tested Java 21/25.
- Securing Jenkins — access control, controller isolation, CSRF, CSP, user content, ports and credentials.
- Controller Isolation — do not run builds on the built-in node; agent→controller access control is always enabled since Jenkins 2.326.
- CSRF Protection — crumb/session behavior and API-token exemption.
- Content Security Policy — general UI CSP in Jenkins 2.539+, rollout guidance and Resource Root URL recommendation for user content.
- Reverse proxy configuration — request/response rewriting, forwarded headers, context paths and WebSocket handling.
- Script Security plugin and Jenkins Security Advisory 2026-09-16 — sandbox fixes in Script Security 1422.v06869826dd9b_.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.