REST API, CLI, Script Console, Groovy Administration, Safe Automation, and Administrative Guardrails: Guided Hands-On Workflow and Core Operations
Use a disposable controller and synthetic job to perform read-only REST inspection, trigger one exact build, follow queue-to-build identity, run a safe CLI query, compare that bounded surface with Script Console power, and implement an idempotent administration wrapper that validates controller and target before mutation.
Learning objectives
- Create a disposable controller/job/agent scenario for administrative automation.
- Store fake API authentication outside command-line arguments and logs.
- Perform read-only REST inspection and trigger one exact job.
- Follow the queue item into one numeric build and verify its result.
- Use the current Jenkins CLI safely and implement a guarded administration wrapper.
1. Disposable scenario and guardrails
Use Jenkins 2.568.3 LTS with Java 21,
built-in-node executors set to zero, and one trusted disposable
agent labeled linux-ci. Create a folder
admin-lab containing exactly one Pipeline job named
exact-job. The automation user
ch28-api needs only the permissions required to read
and build this lab job; it must not have
Overall/Administer or Script Console access.
The Pipeline is deliberately small:
pipeline {
agent { label 'linux-ci' }
options { timestamps() }
stages {
stage('Identity') {
steps {
sh 'printf "job=%s\\nbuild=%s\\nnode=%s\\nworkspace=%s\\n" "$JOB_NAME" "$BUILD_NUMBER" "$NODE_NAME" "$WORKSPACE"'
}
}
stage('Bounded work') {
steps { sh 'sleep 3; printf "synthetic-ok\\n" > result.txt' }
}
}
post { always { archiveArtifacts artifacts: 'result.txt', allowEmptyArchive: true } }
}
2. Prove controller and job identity before authentication
export JENKINS_URL='http://127.0.0.1:8080'
export EXPECTED_JOB='admin-lab/exact-job'
mkdir -p ch28-evidence
curl -fsS -D ch28-evidence/controller-headers.txt -o /dev/null "$JENKINS_URL/login"
grep -i '^X-Jenkins:' ch28-evidence/controller-headers.txt
Expected baseline: X-Jenkins: 2.568.3. In production,
also validate the HTTPS certificate/hostname through the normal
trust store; never use -k.
3. Create disposable API authentication without putting the token in argv
Create an API token for ch28-api through that
disposable user’s Security page. Enter it interactively and write a
curl configuration file with owner-only permissions:
mkdir -p "$HOME/.config/ch28"
chmod 700 "$HOME/.config/ch28"
read -rp 'Jenkins user: ' JENKINS_USER
read -rsp 'Disposable API token: ' JENKINS_API_TOKEN; printf '\n'
umask 077
printf 'user = "%s:%s"\n' "$JENKINS_USER" "$JENKINS_API_TOKEN" \
> "$HOME/.config/ch28/curl.conf"
printf '%s:%s\n' "$JENKINS_USER" "$JENKINS_API_TOKEN" \
> "$HOME/.config/ch28/cli-auth.txt"
unset JENKINS_API_TOKEN
stat -c '%a %n' "$HOME/.config/ch28/curl.conf" "$HOME/.config/ch28/cli-auth.txt"
Expected mode is 600. Do not copy either file into the
workspace or evidence directory. Rotate/revoke the disposable token
after cleanup.
4. Read the exact job through REST
JOB_URL="$JENKINS_URL/job/admin-lab/job/exact-job"
curl --config "$HOME/.config/ch28/curl.conf" -fsS \
"$JOB_URL/api/json?tree=fullName,url,buildable,color,lastBuild[number,url,result]" \
| tee ch28-evidence/job-before.json
Verify that fullName is exactly
admin-lab/exact-job. If it is not, stop. This check
prevents a typo or unexpected reverse-proxy/root URL from turning
the later POST into a request against the wrong item.
5. Trigger exactly one build and preserve the queue identity
API-token authentication is exempt from crumbs, so the lab does not fetch one. Capture headers rather than assuming a response code:
curl --config "$HOME/.config/ch28/curl.conf" -fsS \
-D ch28-evidence/trigger-headers.txt -o ch28-evidence/trigger-body.txt \
-X POST "$JOB_URL/build"
grep -Ei '^(HTTP/|Location:)' ch28-evidence/trigger-headers.txt
A successful build trigger normally returns a queue location. Record
the actual Location header, extract the numeric queue
item ID, and query that exact object:
QUEUE_URL=$(awk 'BEGIN{IGNORECASE=1} /^Location:/ {gsub("\\r",""); print $2}' \
ch28-evidence/trigger-headers.txt)
printf '%s\n' "$QUEUE_URL" | tee ch28-evidence/queue-url.txt
curl --config "$HOME/.config/ch28/curl.conf" -fsS \
"${QUEUE_URL}api/json?tree=id,why,cancelled,task[name,url],executable[number,url]" \
| tee ch28-evidence/queue.json
If executable is not present yet, wait briefly and
query the same queue URL again. Do not scan for “the newest
build.”
6. Resolve and verify the numeric build
Once the queue item reports an executable number/URL, store those
values. Query that concrete run until building is
false:
# Set BUILD_URL from the executable.url returned by the queue item.
curl --config "$HOME/.config/ch28/curl.conf" -fsS \
"${BUILD_URL}api/json?tree=number,url,building,result,queueId,duration,estimatedDuration,actions[causes[*]]" \
| tee ch28-evidence/build-final.json
Then inspect the build console or artifact list. The evidence chain is now trigger response → queue item → numeric build → result/artifact, rather than “POST returned success.”
7. Use Jenkins CLI for a read-only operator action
Download the CLI jar from the same controller and record its digest:
curl -fsS "$JENKINS_URL/jnlpJars/jenkins-cli.jar" -o "$HOME/.config/ch28/jenkins-cli.jar"
sha256sum "$HOME/.config/ch28/jenkins-cli.jar" | tee ch28-evidence/cli.sha256
java -jar "$HOME/.config/ch28/jenkins-cli.jar" \
-s "$JENKINS_URL" -webSocket \
-auth @"$HOME/.config/ch28/cli-auth.txt" who-am-i \
| tee ch28-evidence/cli-whoami.txt
Modern Jenkins CLI defaults to WebSocket, but specifying it makes
the lab assumption explicit. The auth file keeps
user:token out of the command line. Never archive that
file.
8. Compare with Script Console without making it the solution
On the disposable controller, a fully trusted administrator may open Manage Jenkins → Script Console and run this read-only snippet:
import jenkins.model.Jenkins
def item = Jenkins.get().getItemByFullName('admin-lab/exact-job')
println "fullName=${item?.fullName}"
println "class=${item?.class?.name}"
The output duplicates information obtainable through the API, yet the script executes in-process with vastly broader capability. That contrast is the lesson: because REST already satisfies the need, Script Console is the wrong routine interface.
9. Build an idempotent, target-verifying wrapper
Create safe_admin.py. It reads authentication from the
permission-restricted file, verifies one exact job, triggers only
that job, follows the queue, and contains a refusal guard for
destructive aliases:
#!/usr/bin/env python3
import base64, json, os, sys, time, urllib.request, urllib.error
BASE = os.environ['JENKINS_URL'].rstrip('/')
EXPECTED = 'admin-lab/exact-job'
AUTH_FILE = os.path.expanduser('~/.config/ch28/cli-auth.txt')
user, token = open(AUTH_FILE, encoding='utf-8').read().strip().split(':', 1)
auth = 'Basic ' + base64.b64encode(f'{user}:{token}'.encode()).decode()
def request(url, method='GET'):
req = urllib.request.Request(url, method=method, headers={'Authorization': auth})
try:
with urllib.request.urlopen(req, timeout=15) as r:
return r.status, dict(r.headers), r.read()
except urllib.error.HTTPError as e:
body = e.read().decode('utf-8', 'replace')[:4000]
raise RuntimeError(f'HTTP {e.code} for {url}: {body}')
def api(url):
status, headers, body = request(url)
return status, headers, json.loads(body)
job_url = BASE + '/job/admin-lab/job/exact-job'
_, _, job = api(job_url + '/api/json?tree=fullName,url,buildable')
if job.get('fullName') != EXPECTED or not job.get('buildable'):
raise SystemExit('REFUSE: exact expected job identity/buildable state not proven')
status, headers, _ = request(job_url + '/build', method='POST')
queue_url = headers.get('Location')
if not queue_url:
raise SystemExit(f'REFUSE: trigger status {status} returned no queue Location')
print('queue=', queue_url)
for _ in range(30):
_, _, q = api(queue_url.rstrip('/') + '/api/json?tree=id,why,cancelled,task[name,url],executable[number,url]')
if q.get('cancelled'):
raise SystemExit('Queue item was cancelled')
if q.get('executable'):
build_url = q['executable']['url']
break
time.sleep(1)
else:
raise SystemExit('Timed out waiting for executable identity')
while True:
_, _, b = api(build_url + 'api/json?tree=number,url,building,result,queueId')
if not b.get('building'):
print(json.dumps(b, indent=2))
if b.get('result') != 'SUCCESS':
raise SystemExit(2)
break
time.sleep(1)
def refuse_ambiguous_destructive_target(target):
if target in {'latest', 'lastBuild', 'lastSuccessfulBuild'} or not target.isdigit():
raise SystemExit(f'REFUSE destructive target: {target!r} is not an explicit numeric build')
# Demonstration only: no DELETE request exists in this lab.
refuse_ambiguous_destructive_target('latest')
The final line should terminate with a refusal after the successful build verification. This is intentional proof that the client will not convert an alias into a destructive request.
10. Mini challenge: choose the correct layer
You need to change a globally configured mail URL currently owned by
JCasC, then trigger admin-lab/exact-job. Which
interfaces should you use? Correct answer: update/review/validate
the JCasC source for the controller configuration, then use the
bounded build endpoint or CLI build command for the exact job. Do
not use Script Console to do both.
Knowledge check
Answer before revealing the explanation.
1. Why does the lab capture the Location header after POSTing /build?
A successful trigger request and the eventual build are different states. The Location header identifies the queue item, which can later resolve to a specific executable build number/URL.
2. Why store CLI authentication in an @file rather than writing user:token directly after -auth?
The CLI documentation recommends file-based authentication because command-line arguments can be visible in process listings, shell history or logs. The auth file can be permission-restricted and destroyed after the lab.
3. What should an idempotent wrapper do before any mutation?
Verify the controller identity/base URL, authenticate as the intended least-privileged user, resolve exactly one target full name, inspect its current state, and decide whether a change is necessary before sending a modifying request.
4. Why is a 201/200 HTTP response not enough evidence of a successful Jenkins build?
The HTTP response only confirms the endpoint accepted or returned the request. A triggered build still moves through queue, agent allocation and execution; its final result and artifacts require separate verification.
5. What is the purpose of the Script Console comparison in this lesson?
To show why it is intentionally not used for the lab workflow. A read-only demonstration on a disposable controller illustrates that Script Console bypasses the bounded resource model and therefore belongs behind much stricter administrative guardrails.
Official references and version notes
- Jenkins Remote Access API — REST-like resource URLs, build submission, depth control and authentication notes.
- Authenticating scripted clients — API-token authentication and Jenkins’ preemptive-auth behavior.
- CSRF Protection — crumb/session behavior and the API-token exemption.
- Jenkins CLI — controller-provided CLI jar, WebSocket/HTTP/SSH modes and recommended authentication methods.
- Script Console — in-process Groovy administration and remote execution surfaces.
- Jenkins permissions — why administrative/Script Console capability implies controller compromise-level authority.
- In-process Script Approval and Script Security plugin — sandbox/approval behavior for plugin-provided Groovy surfaces; this is not a Script Console safety wrapper.
- Jenkins LTS changelog and Java support policy.
2.568.3 LTS with Java 21; Jenkins
2.568.3 is tested with Java 21 and 25. The lab uses only Jenkins
core Remote API/CLI plus Script Security
1422.v06869826dd9b_ as the current Groovy-sandbox
reference. On modern Jenkins, the CLI client defaults to WebSocket;
HTTP mode is explicit, and file-based/environment authentication is
preferred over exposing a token as a command-line argument.
API-token-authenticated requests are exempt from CSRF crumbs;
password/session POSTs require the crumb/session flow. Re-check
endpoint/plugin/security documentation before reusing automation.
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.