Playbook — gitlab-runner cannot traverse /root/s3v2-prod/ (preflight always fails)
Status: ACTIVE (verified live on bms-1 ns367522, 2026-08-09)
Scope: bms-1 (94.23.26.113), Pinbox24 W4 microservice s3-v2-v42-prod GitLab CI deploy.
Owner: sys-admin (infra-task worker on bms-4). Any change to the /root ACL itself → notify sys-security.
Origin: #5971 (rerouted from #5471’s blocked deploy).
Symptom
The GitLab CI deploy job for pinbox24-ms-s3-v2 (runner s3-v2-bms1-autodeploy on bms-1) fails
its persistent-patch preflight:
ERROR: Patch missing: /root/s3v2-prod/patches/mailgunFileHandler.helper.js
…even though the file exists with correct perms (644 root:root). The preflight runs
test -f <path> as the gitlab-runner OS user (uid 1002), not as root.
Root cause
/root is drwx------ (700, root:root) by default. The gitlab-runner user cannot traverse
(x bit) into /root at all — so every path under /root/s3v2-prod/ is unreachable to it,
regardless of the (correct) permissions on the subdirectories and files inside. This is structural:
the deploy preflight for any file under /root/s3v2-prod/ has never been passable by the runner.
Confirm the diagnosis (read-only):
ssh root@94.23.26.113 '
stat -c "%a %U:%G %n" /root /root/s3v2-prod /root/s3v2-prod/patches
sudo -u gitlab-runner test -x /root && echo "traverse-OK" || echo "traverse-DENIED"
sudo -u gitlab-runner test -f /root/s3v2-prod/patches/mailgunFileHandler.helper.js \
&& echo "file-READ-OK" || echo "file-MISSING-OR-DENIED"
'The fix — least-privilege ACL (chosen over chmod 755 /root)
The issue listed four options. The applied fix is an ACL (POSIX setfacl), done in a
least-privilege form, not chmod 755 /root (which would expose all of root’s home to every local
user) and not moving the directory tree (a large cross-repo change touching the GitLab CI script,
compose bind-mounts, and repo docs).
| Path | ACL granted to gitlab-runner | Why this level |
|---|---|---|
/root | --x (execute/traverse only, no read) | Runner can pass through /root to reach s3v2-prod, but cannot list or read anything else in root’s home. Narrowest grant that unblocks the preflight. |
/root/s3v2-prod | r-x + default r-x | Read+traverse the deploy-artifacts tree; the default ACL makes new files/dirs inherit runner-read. |
/root/s3v2-prod/patches | r-x + default r-x (see hardening) | Read+traverse the patches dir; default ACL keeps regenerated patch files readable. |
…/patches/*.helper.js | r-- | Read the individual preflight patch files. |
Security note — why this is defensible without a full sys-security gate: gitlab-runner is
already a member of the docker group (groups … 998(docker)), which is root-equivalent (it can
docker run -v /:/host and read anything). Granting it traverse-only on /root therefore adds no
meaningful privilege it did not already effectively hold — it only makes an existing capability
usable by the CI preflight without a container. The --x (no r) grant on /root specifically
avoids letting the runner enumerate root’s home, keeping the blast radius minimal. Still: any edit
to the /root ACL is worth a one-line heads-up to sys-security because ACLs on /root are unusual
and easy to lose track of.
Commands (idempotent — safe to re-run)
# traverse-only on /root → runner can pass through, cannot read root's home
setfacl -m u:gitlab-runner:x /root
# read+traverse on the deploy tree, with a DEFAULT so new files inherit runner-read
setfacl -m u:gitlab-runner:rx /root/s3v2-prod
setfacl -d -m u:gitlab-runner:rx /root/s3v2-prod
setfacl -R -m u:gitlab-runner:rx /root/s3v2-prod # existing files/dirs
setfacl -m u:gitlab-runner:rx /root/s3v2-prod/patches
setfacl -d -m u:gitlab-runner:rx /root/s3v2-prod/patches # DEFAULT — durability (see below)Durability gap this closes (#5971)
The initial fix set the default ACL on /root/s3v2-prod but not on /root/s3v2-prod/patches.
POSIX default ACLs are inherited only from the immediate parent at file-creation time, so a patch
file regenerated in patches/ via rm+recreate (rather than in-place overwrite) would be born
with no runner ACL — silently reintroducing the exact preflight failure on the next deploy that
rewrites the patch. Setting default:user:gitlab-runner:r-x on patches/ (last command above)
makes every regenerated patch file readable by the runner automatically, matching the pattern
already on the parent dir.
ACLs are stored in ext4 extended attributes and survive reboots. They do not survive a later
chmod that drops the mask, or the directory being deleted and recreated by a fresh checkout.
Verify (after any change, and as a standalone health check)
ssh root@94.23.26.113 '
for p in /root /root/s3v2-prod /root/s3v2-prod/patches; do
sudo -u gitlab-runner test -x "$p" && echo "$p x-OK" || echo "$p x-DENIED"
done
sudo -u gitlab-runner test -f /root/s3v2-prod/patches/mailgunFileHandler.helper.js \
&& echo "preflight-file READABLE" || echo "preflight-file BLOCKED"
getfacl -p /root/s3v2-prod/patches | grep -E "gitlab-runner|default"
'All lines must read x-OK / READABLE. If any is DENIED/BLOCKED, re-run the fix commands.
Related
- #5471 — the W4
s3-v2-v42-produpload-retry deploy blocked on this preflight. docs/w3-w4-stack-operations.md— Pinbox24 W3/W4 ownership + permission matrix (this is a sys-admin/infra-task op).docs/playbooks/pinbox24-s3v2-master-branch-autodeploy.md— the coupled merge⇒deploy behaviour of the samepinbox24-ms-s3-v2pipeline; relevant when the deploy actually runs after this unblock.