jenkins-agent-l3-deploy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJenkins-agent L3 deploy (no SSH)
Jenkins Agent L3 部署(无 SSH)
Deploy a docker-compose stack where a Jenkins inbound agent on the target host
runs + locally. The controller never SSHes in.
Runtime secrets are written to tmpfs (file 0444, dir 0700),
mounted as Docker secrets at , and a tiny entrypoint shim
exports them to env at container start — so they are absent from
and from .
deploy.pydocker compose/run/payroll-sec/run/secrets/<name>docker inspect.env部署一个 docker-compose stack,其中目标主机上的 Jenkins inbound agent 在本地运行 + 。controller 绝不会通过 SSH 登录。
运行时 secret 写入 tmpfs (文件 0444,目录 0700),以 Docker secrets 的形式挂载到 ;一个轻量 entrypoint shim 会在容器启动时将它们导出为 env,因此它们不会出现在 或 中。
deploy.pydocker compose/run/payroll-sec/run/secrets/<name>docker inspect.envWHAT
内容
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 lẫn
docker inspect..env - Trigger (when to use vs the SSH variant):
- Agent (this skill, preferred): an inbound agent runs on the target → steps execute locally; only a
shcredential (the cred bundle) is needed. Controller opens no SSH outbound.Secret file - SSH variant: controller into the target (needs an
ssh/scp/rsynccredential). Use only when you cannot run an agent on the target.SSH Username with private key - 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.
- **目的:**搭建/运行一个 Jenkins pipeline,通过目标主机自身的 inbound agent 部署 docker-compose stack(controller 不通过 SSH 登录),并采用 L3 secret:只存在于 tmpfs 上、挂载为 Docker secret,且不出现在 和
docker inspect中。.env - 触发条件(何时使用此方案而非 SSH 变体):
- Agent(此 skill,首选): inbound agent 运行在目标主机上 → steps 在本地执行;只需要一个
shcredential(cred bundle)。Controller 不会建立任何出站 SSH 连接。Secret file - SSH 变体: controller 通过 进入目标主机(需要
ssh/scp/rsynccredential)。仅在目标主机无法运行 agent 时使用。SSH Username with private key - 用户提出“通过 Jenkins Agent 部署”“无 SSH 部署”“用 Agent 代替 SSH”“Docker secrets L3”“从 Docker inspect 中隐藏 secret”“secret 不落盘”等需求,或正在配置将 docker-compose 部署到 Linux 主机的 Jenkins pipeline。
- Agent(此 skill,首选): inbound agent 运行在目标主机上 →
- **非目标:**不搭建 SSH 变体(仅在无法运行 agent 时采用);不修改应用代码来读取 secret(由 shim entrypoint 完成);不安装 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)postgresPOSTGRES_PASSWORD_FILEfs架构(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)postgresPOSTGRES_PASSWORD_FILEfsInput và output contract
输入与输出约定
| Field | Required? | Ý nghĩa | |
|---|---|---|---|
| In | Jenkins controller reachable | 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 | | có | không cần SSH credential |
| In | repo app có | 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 |
| Out | tín hiệu verify | có | |
| 字段 | 是否必填? | 含义 | |
|---|---|---|---|
| 输入 | Jenkins controller 可从 target 访问 | 是 | agent 必须能够连接 |
| 输入 | target Linux:Java 21,用户属于 docker group 且拥有 sudo NOPASSWD | 是 | 见 Gotchas 1、4 |
| 输入 | | 是 | 不需要 SSH credential |
| 输入 | 应用 repo 包含 | 是 | 通过 SCM checkout 获取,或使用已有 workspace |
| 输入 | 勾选要部署的 service(WEB/ETL/DB/NGINX/ALL) | 否 | 选择性部署;默认执行完整部署 |
| 输出 | stack 在 target 上运行 | 是 | build console 显示 |
| 输出 | 验证信号 | 是 | 每个容器均出现 |
Rules và capabilities
规则与能力
- RULE-01 (MUST): Don't pass secrets via compose (visible in
environment:, written todocker inspect)..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 SCM URL at a path the controller can't see — use a network git URL.
file:// - 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.
- RULE-01(MUST):不得通过 compose 传递 secret(这会暴露在
environment:中,并写入docker inspect)。.env - RULE-02(MUST):不得将 secret 文件设为 0600(非 root 容器无法读取)——应使用 0444 文件权限和 0700 目录权限。
- RULE-03(MUST):如果可以运行 agent,则不得依赖 SSH 变体——agent 可保持 SSH 关闭并在本地执行。
- RULE-04(MUST):不得将 SCM URL 指向 controller 无法访问的路径——应使用网络 git URL。
file:// - RULE-05(MUST):Gotchas 1–5(每一项都曾导致一次真实运行失败)必须固化到 setup/pipeline 中。
- 能力:在 Jenkins 上配置 node/credential/job;在 target 上运行 shell + docker + sudo;读取 git repo(SCM)。不会从 controller 发起任何出站 SSH 连接。
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 hoặc
LEAK <c>→ pipeline failed (exit 1), không coi là deploy xong.NOMOUNT <c> - Agent chết / permission denied docker-sudo / secret 0600 → failed, sửa theo bảng Gotchas rồi chạy lại.
UnsupportedClassVersionError - Repo private chưa có deploy key / known_hosts → blocked ở checkout.
- 无法在 target 上运行 agent → 不在此 skill 的处理范围内,应使用 SSH 变体(对此 skill 而言为 blocked)。
- Verify stage 出现 或
LEAK <c>→ pipeline failed(exit 1),不能视为部署完成。NOMOUNT <c> - Agent 因 退出、docker-sudo 出现 permission denied,或 secret 权限为 0600 → failed;按 Gotchas 表修复后重新运行。
UnsupportedClassVersionError - Private repo 缺少 deploy key/known_hosts → 在 checkout 阶段 blocked。
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, | agent online | gotcha 1/4 → sửa, lặp |
| W03 | effect | repo app | Bake | 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) | | LEAK/NOMOUNT → failed |
Chi tiết từng bước (nguồn chân lý cho W01–W05):
| Step | 类型 | 输入 | 操作 | 输出/退出状态 | 失败/下一步 |
|---|---|---|---|---|---|
| W01 | 判断 | target 基础设施 | 选择 agent(优先)或 SSH 变体 | 决策 | 无法运行 agent → 停止,改用 SSH 变体 |
| W02 | 执行 | Jenkins + target | 设置 inbound agent(一次性):node、Java 21、agent.jar、具备 docker+sudo 权限的用户、 | agent online | gotcha 1/4 → 修复后重试 |
| W03 | 执行 | 应用 repo | 将 | image + provisioner | — |
| W04 | 执行 | job | Pipeline 模板(Sync → Deploy → Verify),代码来自 SCM | job 运行 | checkout 失败 → B02 |
| W05 | 确定性 | build | 验证成功(真实信号) | | LEAK/NOMOUNT → failed |
各步骤详情(W01–W05 的事实来源):
Setup the inbound agent (once)
设置 inbound agent(一次性)
- Jenkins → Manage Jenkins → Nodes → New Node: Permanent Agent, label = , Remote root =
<target>, Launch = inbound (JNLP); copy the secret./home/<user>/jenkins-agent - On the target (agent must reach controller +
:8080)::50000Run the agent as a user that is in the docker group AND has sudo NOPASSWD. For production replacebashsudo 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 &with a systemd unit (nohup).Restart=always - Credential: one credential (the cred bundle). No SSH credential.
Secret file
- Jenkins → Manage Jenkins → Nodes → New Node:创建 Permanent Agent,label = ,Remote root =
<target>,Launch = inbound (JNLP);复制该 secret。/home/<user>/jenkins-agent - 在 target 上执行(agent 必须能够访问 controller 的 +
:8080)::50000使用同时属于 docker group 且拥有 sudo NOPASSWD 权限的用户运行 agent。 生产环境应使用 systemd unit 替代bashsudo 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 &(nohup)。Restart=always - Credential:一个 credential(cred bundle)。不需要 SSH credential。
Secret file
Pipeline template (declarative)
Pipeline 模板(declarative)
groovy
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.
groovy
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
''' } }
}
}通过 SCM checkout 提供 workspace 代码,或预先填充 agent workspace。
secrets-entrypoint.sh (baked into each app image; ENTRYPOINT before CMD)
secrets-entrypoint.sh(内置到每个 app image;ENTRYPOINT 位于 CMD 之前)
sh
#!/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. ) writes the secret files to a tmpfs dir
mode 0444 (files) / 0700 (dir), then .
deploy.pydocker compose up -dsh
#!/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 "$@"Provisioner(例如 )会将 secret 文件写入 tmpfs 目录,权限为 0444(文件)/ 0700(目录),然后执行 。
deploy.pydocker compose up -dGotchas (each cost a failed real run — bake these in)
Gotchas(每一项都曾导致真实运行失败——必须全部纳入)
| # | Symptom | Fix |
|---|---|---|
| 1 | Agent dies | Jenkins 2.5xx agent.jar needs Java 21, not 17. |
| 2 | App container crash-loops | 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 | | Jenkins |
| 4 | | The agent process user must be in the docker group and have sudo NOPASSWD. |
| 5 | Reboot → containers fail to mount secret | |
| # | 症状 | 修复 |
|---|---|---|
| 1 | Agent 启动时报 | Jenkins 2.5xx 的 agent.jar 需要 Java 21,不能使用 17。 |
| 2 | App 容器反复崩溃并出现 | Compose 的非 swarm bind-mount 会继承源文件权限;应将 secret 文件写为 0444(目录 0700),确保非 root 容器用户可以读取。 |
| 3 | | Jenkins |
| 4 | Pipeline 中执行 | agent 进程用户必须属于 docker group,并拥有 sudo NOPASSWD 权限。 |
| 5 | 主机重启 → 容器无法挂载 secret | |
Verify success (real signals)
验证成功(真实信号)
- Build console shows (proves it ran on the agent, not controller).
Running on <target> - for every container (secret absent from
CLEAN <c>).docker inspect - (secret present at
MOUNT <c>)./run/secrets - App smoke (e.g. login) returns 200.
- The cred bundle is shredded from (not left on disk).
/dev/shm
- Build console 显示 ,证明任务运行在 agent 上,而非 controller。
Running on <target> - 每个容器都出现 ,表明 secret 未出现在
CLEAN <c>中。docker inspect - 每个容器都出现 ,表明 secret 已挂载到
MOUNT <c>。/run/secrets - App smoke(例如登录)返回 200。
- cred bundle 已从 中 shredded,未残留在磁盘上。
/dev/shm
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: | ALL → full; không tick gì → | 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 | W04 |
| B03 | recovery | target reboot ( | chạy lại deploy (hoặc boot hook) để tái tạo secret trước | — | W05 |
| ID | 类型 | 条件 | 行为 | 跳过/失败 | 汇合点 |
|---|---|---|---|---|---|
| B01 | user_optional | 仅部署部分 service(勾选 WEB/ETL/DB/NGINX,而非 ALL) | 选择性部署: | ALL → full;未勾选任何项 → | W05 |
| B02 | conditional_required | 代码取自 git(生产环境:Pipeline script from SCM) | 配置 SCM;private repo → 在 controller 和 agent 上配置 SSH deploy key + known_hosts | 实验环境无 git server → agent 执行 | W04 |
| B03 | recovery | target 重启( | 重新运行 deploy(或 boot hook),在 | — | W05 |
Reference — Branch details
参考——分支详情
Selective deploy (deploy only some services, not full)
选择性部署(仅部署部分 service,而非全部)
Expose checkboxes (WEB/ETL/DB/NGINX/ALL) and pass the chosen compose
services to the provisioner. Use so a tick rebuilds only that service
(not its dependency chain):
booleanParam--no-depssh
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公开 复选框(WEB/ETL/DB/NGINX/ALL),并将选中的 compose services 传递给 provisioner。使用 ,使一次勾选仅重新构建该 service,而不会构建其依赖链:
booleanParam--no-depssh
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 upprovisioner runs: docker compose up -d --build --no-deps $SVC
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.[ "$NGINX" = true ] && docker restart <proxy-container> # proxy is a separate container, not compose
**依赖图(depends_on)——必须记录**,以便操作人员了解全新部署需要依赖链:
`web → etl → postgres`(proxy/nginx 独立)。使用 `--no-deps` 时,勾选 web = **仅** web;对于全新首次部署,应勾选整个依赖链(或 ALL)。已验证:单独勾选及组合勾选都只会影响所选的 service。Pulling code from git
从 git 拉取代码
Production: configure the job as Pipeline script from SCM (git repo + scriptPath); Jenkins
auto- into the agent workspace, then the pipeline runs. The git URL must be reachable
by BOTH controller (fetch Jenkinsfile) and agent (checkout workspace) — only works on the
side that owns it; in a lab without a git server, have the agent .
checkout scmfile://git clone file:///…/repo.gitPrivate 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
credential (username
SSH Username with private key); SCM URLgitwith that credentialsId.git@github.com:owner/repo.git - Host key gotcha: add to known_hosts on controller AND agent (
github.com) or set the git host-key strategy to "Accept first connection", else the SSH clone fails host-key verification.ssh-keyscan github.com >> ~/.ssh/known_hosts - The deploy key's public part on GitHub must match the private key in Jenkins (compare
fingerprint vs what GitHub shows).
ssh-keygen -lf key.pub
生产环境:将 job 配置为 Pipeline script from SCM(git repo + scriptPath);Jenkins 会自动在 agent workspace 中执行 ,随后运行 pipeline。git URL 必须能被 controller(获取 Jenkinsfile)和 agent(checkout workspace)同时访问—— 仅对拥有该路径的一侧有效;在没有 git server 的实验环境中,让 agent 执行 。
checkout scmfile://git clone file:///…/repo.gitPrivate repo(已使用 GitHub 测试,build SUCCESS):
- 对 private repo 使用 HTTPS 匿名 clone 会失败——必须进行身份验证。
- 使用 SSH deploy key(只读)访问 repo;将 private key 放入 Jenkins 的 credential(username 为
SSH Username with private key);SCM URL 使用git并指定该 credentialsId。git@github.com:owner/repo.git - **Host key 注意事项:**在 controller 和 agent 上将 加入 known_hosts(
github.com),或者将 git host-key strategy 设置为“Accept first connection”;否则 SSH clone 会因 host-key verification 失败。ssh-keyscan github.com >> ~/.ssh/known_hosts - GitHub 上 deploy key 的 public 部分必须与 Jenkins 中的 private key 匹配(比较 指纹与 GitHub 显示的指纹)。
ssh-keygen -lf key.pub
Validation và stopping
验证与停止条件
Stage Verify là cổng tất định: exit 1 khi có /. Cần người xem: console , smoke login 200, 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.
LEAKNOMOUNTRunning on <target>/dev/shmVerify stage 是确定性关卡:出现 / 时 exit 1。人工复核时需要看到:console 显示 、smoke login 返回 200,并且 中不再存在 cred。满足“验证成功”中的 5 个信号后即可停止;若错误出现在 Gotchas 表中,应严格按照 Fix 行修复后重新运行 build,不得依据表外信息进行猜测。
LEAKNOMOUNTRunning on <target>/dev/shmExamples
示例
- Positive: app payroll (web/etl/postgres) — agent label Java 21, user trong docker group + sudo NOPASSWD, build tick ALL → console
<target>,Running on <target>/CLEANcho mọi container, login 200, cred bundle đã shred khỏiMOUNT./dev/shm - Boundary/failure: secret ghi 0600 → container web crash-loop (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ì
Permission denied /run/secrets/*, thiếu etl/postgres → tick cả chuỗi--no-deps(hoặc ALL).web → etl → postgres
- 正向示例: payroll app(web/etl/postgres)——agent label 为 ,使用 Java 21,用户属于 docker group 且拥有 sudo NOPASSWD,build 勾选 ALL → console 显示
<target>,每个容器都有Running on <target>/CLEAN,login 返回 200,cred bundle 已从MOUNT中 shredded。/dev/shm - **边界/失败示例:**将 secret 写为 0600 → web app container 发生 crash-loop,并出现 (gotcha 2)→ provisioner 改为 0444 文件权限 + 0700 目录权限,然后重新运行;首次部署仅勾选 WEB → 由于使用了
Permission denied /run/secrets/*,只有 web 启动,缺少 etl/postgres → 应勾选整个依赖链--no-deps(或 ALL)。web → etl → postgres