jenkins-agent-l3-deploy
Deploy a docker-compose app via a Jenkins INBOUND AGENT running on the target server (no SSH from controller), with Docker-secrets "L3" hardening — runtime secrets live on tmpfs and never appear in `docker inspect` nor in `.env`. Use when the user wants: "deploy via jenkins agent", "no-ssh deploy", "agent thay vì ssh", "docker secrets L3", "ẩn secret khỏi docker inspect", "secret không nằm trên đĩa", or is wiring a Jenkins pipeline that deploys docker-compose to a Linux host. Encodes the gotchas learned from a real 1:1 run (Java 21, 0444 secret perms, 0400 file-credential, agent user needs docker+sudo).
How do I install this agent skill?
npx skills add https://github.com/rheinmir/setup --skill jenkins-agent-l3-deployIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides a template for deploying Docker applications via a Jenkins inbound agent. It focuses on 'L3' hardening by keeping secrets in memory-backed filesystems (tmpfs) to prevent them from appearing in Docker inspection or on physical disks. While it requires high privileges (sudo) and performs standard automation tasks, it follows security best practices for CI/CD environments.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 1 issue
What does this agent skill do?
Jenkins-agent L3 deploy (no SSH)
Deploy a docker-compose stack where a Jenkins inbound agent on the target host
runs deploy.py + docker compose locally. The controller never SSHes in.
Runtime secrets are written to tmpfs /run/payroll-sec (file 0444, dir 0700),
mounted as Docker secrets at /run/secrets/<name>, and a tiny entrypoint shim
exports them to env at container start — so they are absent from docker inspect
and from .env.
WHAT
Purpose và context
- Purpose: dựng/chạy pipeline Jenkins deploy một stack docker-compose QUA inbound agent trên chính target (controller không SSH vào), với secret L3: chỉ nằm trên tmpfs, mount làm Docker secret, không có trong
docker inspectlẫn.env. - Trigger (when to use vs the SSH variant):
- Agent (this skill, preferred): an inbound agent runs on the target →
shsteps execute locally; only aSecret filecredential (the cred bundle) is needed. Controller opens no SSH outbound. - SSH variant: controller
ssh/scp/rsyncinto the target (needs anSSH Username with private keycredential). Use only when you cannot run an agent on the target. - User nói "deploy via jenkins agent", "no-ssh deploy", "agent thay vì ssh", "docker secrets L3", "ẩn secret khỏi docker inspect", "secret không nằm trên đĩa", hoặc đang nối pipeline Jenkins deploy docker-compose lên host Linux.
- Agent (this skill, preferred): an inbound agent runs on the target →
- Non-goals: không dựng biến thể SSH (chỉ khi không chạy được agent); không sửa code ứng dụng để đọc secret (shim entrypoint lo); không cài Jenkins controller.
Mental model
Architecture (L3):
secret source (cred bundle)
→ deploy provisioner writes 7 files to tmpfs /run/payroll-sec (file 0444, dir 0700 root)
→ docker-compose `secrets:` mounts each at /run/secrets/<name> (tmpfs, ro)
→ secrets-entrypoint.sh in each image: for f in /run/secrets/*; export $(UPPER basename)=$(cat f); exec "$@"
⇒ secret NOT in `docker inspect` (set at runtime), NOT on disk (RAM only)
postgres uses the official POSTGRES_PASSWORD_FILE; app containers use the
entrypoint shim so no application code changes (avoids the Edge-runtime fs
trap if a framework imports the secret module in middleware).
Input và output contract
| Field | Required? | Ý nghĩa | |
|---|---|---|---|
| In | Jenkins controller reachable :8080 + :50000 từ target | có | agent phải kết nối được |
| In | target Linux: Java 21, user thuộc docker group + sudo NOPASSWD | có | xem Gotchas 1, 4 |
| In | Secret file credential (cred bundle) | có | không cần SSH credential |
| In | repo app có docker-compose.yml, deploy.py(.example), Dockerfile + secrets-entrypoint.sh | có | qua SCM checkout hoặc workspace có sẵn |
| In | tick chọn service (WEB/ETL/DB/NGINX/ALL) | không | selective deploy; mặc định full |
| Out | stack chạy trên target | có | build console Running on <target> |
| Out | tín hiệu verify | có | CLEAN <c> + MOUNT <c> mọi container, smoke 200, cred bundle đã shred |
Rules và capabilities
- RULE-01 (MUST): Don't pass secrets via compose
environment:(visible indocker inspect, written to.env). - RULE-02 (MUST): Don't store the secret files 0600 (non-root container can't read) — use 0444 + dir 0700.
- RULE-03 (MUST): Don't rely on the SSH variant if you can run an agent — agent keeps SSH closed and runs locally.
- RULE-04 (MUST): Don't point a
file://SCM URL at a path the controller can't see — use a network git URL. - RULE-05 (MUST): Gotchas 1–5 (mỗi cái đã làm hỏng một lần chạy thật) phải được bake vào setup/pipeline.
- Capabilities: cấu hình node/credential/job trên Jenkins; chạy shell + docker + sudo trên target; đọc git repo (SCM). Không mở SSH outbound từ controller.
Failure boundaries
- Không chạy được agent trên target → ngoài phạm vi, dùng biến thể SSH (blocked cho skill này).
- Stage Verify in
LEAK <c>hoặcNOMOUNT <c>→ pipeline failed (exit 1), không coi là deploy xong. - Agent chết
UnsupportedClassVersionError/ permission denied docker-sudo / secret 0600 → failed, sửa theo bảng Gotchas rồi chạy lại. - Repo private chưa có deploy key / known_hosts → blocked ở checkout.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | hạ tầng target | Chọn agent (ưu tiên) hay SSH variant | quyết định | không chạy được agent → dừng, SSH variant |
| W02 | effect | Jenkins + target | Setup inbound agent (once): node, Java 21, agent.jar, user docker+sudo, Secret file credential | agent online | gotcha 1/4 → sửa, lặp |
| W03 | effect | repo app | Bake secrets-entrypoint.sh vào mỗi image (ENTRYPOINT trước CMD); provisioner ghi secret 0444/0700 | image + provisioner | — |
| W04 | effect | job | Pipeline template (Sync → Deploy → Verify), code từ SCM | job chạy | checkout lỗi → B02 |
| W05 | deterministic | build | Verify success (real signals) | Running on <target>, CLEAN, MOUNT, smoke 200, cred shredded | LEAK/NOMOUNT → failed |
Chi tiết từng bước (nguồn chân lý cho W01–W05):
Setup the inbound agent (once)
- Jenkins → Manage Jenkins → Nodes → New Node: Permanent Agent, label =
<target>, Remote root =/home/<user>/jenkins-agent, Launch = inbound (JNLP); copy the secret. - On the target (agent must reach controller
:8080+:50000):
Run the agent as a user that is in the docker group AND has sudo NOPASSWD. For production replacesudo apt-get install -y openjdk-21-jre-headless # Java 21 — NOT 17 (see gotcha 1) J21=$(ls /usr/lib/jvm/java-21-openjdk-*/bin/java | head -1) mkdir -p ~/jenkins-agent && cd ~/jenkins-agent curl -sO http://<CONTROLLER>:8080/jnlpJars/agent.jar nohup "$J21" -jar agent.jar -url http://<CONTROLLER>:8080/ \ -secret <AGENT_SECRET> -name <target> -workDir ~/jenkins-agent > agent.log 2>&1 &nohupwith a systemd unit (Restart=always). - Credential: one
Secret filecredential (the cred bundle). No SSH credential.
Pipeline template (declarative)
pipeline {
agent { label '<target>' } // runs ON the target, no ssh
environment { APP = '/home/<user>/<app-dir>' }
stages {
stage('Sync') { steps { sh '''
set -e
cp -f docker-compose.yml deploy.py.example "$APP/"
cp -f web/Dockerfile web/secrets-entrypoint.sh "$APP/web/"
cp -f etl/Dockerfile etl/secrets-entrypoint.sh "$APP/etl/"
cd "$APP" && cp -f deploy.py.example deploy.py
''' } }
stage('Deploy') { steps { withCredentials([file(credentialsId:'app-cred', variable:'CRED')]) { sh '''
set -e
mkdir -p /dev/shm/pl && chmod 700 /dev/shm/pl
cp "$CRED" /dev/shm/pl/cred.txt && chmod 600 /dev/shm/pl/cred.txt # gotcha 3
cd "$APP" && ln -sf /dev/shm/pl/cred.txt cred.txt
sudo python3 deploy.py --no-nginx --etl-url /etl
rm -f cred.txt; shred -uf /dev/shm/pl/cred.txt 2>/dev/null || rm -f /dev/shm/pl/cred.txt
''' } } }
stage('Verify') { steps { sh '''
set -e
for c in <containers>; do
docker inspect "$c" --format "{{range .Config.Env}}{{println .}}{{end}}" \
| grep -iqE "SESSION_SECRET=|AUTH_USERS=|PASSWORD=|DATABASE_URL=" && { echo "LEAK $c"; exit 1; } || echo "CLEAN $c"
docker exec "$c" ls /run/secrets >/dev/null 2>&1 && echo "MOUNT $c" || { echo "NOMOUNT $c"; exit 1; }
done
''' } }
}
}
Provide the workspace code via SCM checkout, or pre-populate the agent workspace.
secrets-entrypoint.sh (baked into each app image; ENTRYPOINT before CMD)
#!/bin/sh
set -e
if [ -d /run/secrets ]; then
for f in /run/secrets/*; do [ -f "$f" ] || continue
name=$(basename "$f" | tr '[:lower:]' '[:upper:]'); export "$name=$(cat "$f")"
done
fi
exec "$@"
The provisioner (e.g. deploy.py) writes the secret files to a tmpfs dir
mode 0444 (files) / 0700 (dir), then docker compose up -d.
Gotchas (each cost a failed real run — bake these in)
| # | Symptom | Fix |
|---|---|---|
| 1 | Agent dies UnsupportedClassVersionError: class 65.0 … up to 61.0 | Jenkins 2.5xx agent.jar needs Java 21, not 17. |
| 2 | App container crash-loops Permission denied /run/secrets/* | Compose non-swarm bind-mounts the source file's perms; write secret files 0444 (dir 0700) so the non-root container user can read. |
| 3 | shred /dev/shm/.../cred: Permission denied | Jenkins file credential is delivered 0400 (read-only); cp then chmod 600, and use `shred -uf … |
| 4 | docker/sudo "permission denied" inside the pipeline | The agent process user must be in the docker group and have sudo NOPASSWD. |
| 5 | Reboot → containers fail to mount secret | /run is tmpfs → secrets cleared on reboot; re-run the deploy (or a boot hook) to re-materialize before compose up. |
Verify success (real signals)
- Build console shows
Running on <target>(proves it ran on the agent, not controller). CLEAN <c>for every container (secret absent fromdocker inspect).MOUNT <c>(secret present at/run/secrets).- App smoke (e.g. login) returns 200.
- The cred bundle is shredded from
/dev/shm(not left on disk).
Branches
| ID | Kind | Guard | Hành vi | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | user_optional | chỉ deploy vài service (tick WEB/ETL/DB/NGINX thay ALL) | Selective deploy: booleanParam → deploy.py --services "$SVC" + --no-deps; NGINX thì docker restart proxy riêng | ALL → full; không tick gì → none = secrets only, no up | W05 |
| B02 | conditional_required | code lấy từ git (production: Pipeline script from SCM) | cấu hình SCM; repo private → SSH deploy key + known_hosts trên controller VÀ agent | lab không git server → agent git clone file:///…/repo.git | W04 |
| B03 | recovery | target reboot (/run tmpfs xoá secret) | chạy lại deploy (hoặc boot hook) để tái tạo secret trước compose up | — | W05 |
Reference — Branch details
Selective deploy (deploy only some services, not full)
Expose booleanParam checkboxes (WEB/ETL/DB/NGINX/ALL) and pass the chosen compose
services to the provisioner. Use --no-deps so a tick rebuilds only that service
(not its dependency chain):
if [ "$ALL" = true ]; then SVC=""; else
S=""; [ "$WEB" = true ] && S="$S web"; [ "$ETL" = true ] && S="$S etl"; [ "$DB" = true ] && S="$S postgres"
[ -z "$(echo $S|xargs)" ] && SVC=none || SVC=$(echo $S|xargs)
fi
deploy.py --services "$SVC" # "" = full ; "web etl" = those ; "none" = secrets only, no up
# provisioner runs: docker compose up -d --build --no-deps $SVC
[ "$NGINX" = true ] && docker restart <proxy-container> # proxy is a separate container, not compose
Dependency graph (depends_on) — document it so operators know a fresh deploy needs the chain:
web → etl → postgres (proxy/nginx independent). With --no-deps, tick web = ONLY web; for a
clean first deploy tick the whole chain (or ALL). Verified: single ticks + combos all touch exactly
the chosen services.
Pulling code from git
Production: configure the job as Pipeline script from SCM (git repo + scriptPath); Jenkins
auto-checkout scm into the agent workspace, then the pipeline runs. The git URL must be reachable
by BOTH controller (fetch Jenkinsfile) and agent (checkout workspace) — file:// only works on the
side that owns it; in a lab without a git server, have the agent git clone file:///…/repo.git.
Private repo (tested with GitHub, build SUCCESS):
- HTTPS anonymous clone fails for a private repo — needs auth.
- Use an SSH deploy key (read-only) on the repo; put the private key in a Jenkins
SSH Username with private keycredential (usernamegit); SCM URLgit@github.com:owner/repo.gitwith that credentialsId. - Host key gotcha: add
github.comto known_hosts on controller AND agent (ssh-keyscan github.com >> ~/.ssh/known_hosts) or set the git host-key strategy to "Accept first connection", else the SSH clone fails host-key verification. - The deploy key's public part on GitHub must match the private key in Jenkins (compare
ssh-keygen -lf key.pubfingerprint vs what GitHub shows).
Validation và stopping
Stage Verify là cổng tất định: exit 1 khi có LEAK/NOMOUNT. Cần người xem: console Running on <target>, smoke login 200, /dev/shm không còn cred. Dừng khi đủ 5 tín hiệu ở "Verify success"; lỗi thuộc bảng Gotchas → sửa đúng dòng Fix rồi chạy lại build, không đoán ngoài bảng.
Examples
- Positive: app payroll (web/etl/postgres) — agent label
<target>Java 21, user trong docker group + sudo NOPASSWD, build tick ALL → consoleRunning on <target>,CLEAN/MOUNTcho mọi container, login 200, cred bundle đã shred khỏi/dev/shm. - Boundary/failure: secret ghi 0600 → container web crash-loop
Permission denied /run/secrets/*(gotcha 2) → provisioner đổi sang 0444 + dir 0700, chạy lại; tick chỉ WEB lần deploy đầu → web lên một mình vì--no-deps, thiếu etl/postgres → tick cả chuỗiweb → etl → postgres(hoặc ALL).
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/rheinmir/setup/jenkins-agent-l3-deploy">View jenkins-agent-l3-deploy on skillZs</a>