@@ -4202,6 +4202,30 @@ control_preview() { # <request-file> <id> <actor> <control-dir>
42024202 rm -f " $errf "
42034203}
42044204
4205+ # Hand the operator-facing stack files that the ROOT control-runner just wrote back to the stack
4206+ # owner (#33 v1.4). control_run_pending is root (User=root in pithead-control.service), so its
4207+ # `apply` renders `.env` under `umask 077` as root:root 0600 and rewrites the Caddyfile as root —
4208+ # but pithead runs a NON-ROOT operator model ($REAL_USER), and a normal operator-run apply leaves
4209+ # these files owned by the operator. Without this, the operator's next `status`/`apply` can't even
4210+ # read .env (Permission denied), which is what the tier-4 gate caught. The target owner is DERIVED
4211+ # from config.json's on-disk owner — an operator-owned file the dashboard container CANNOT write
4212+ # (its raw config.json mount was dropped in #440; control_commit's `cp` also preserves its inode/
4213+ # owner), so nothing from the request or spool can steer the chown. $USER/$SUDO_USER are NOT usable
4214+ # here — the runner is root, so they read as root. The control-dir (staged/results/audit) is
4215+ # deliberately host-owned and is NOT touched: that rw/ro split is the #33 trust boundary.
4216+ control_reown_operator_files () {
4217+ local owner f
4218+ # GNU stat first, BSD fallback (see the provision_onion_client_auth note). No owner → skip.
4219+ owner=$( stat -c ' %u:%g' " $CONFIG_FILE " 2> /dev/null || stat -f ' %u:%g' " $CONFIG_FILE " 2> /dev/null) || owner=" "
4220+ [ -n " $owner " ] || return 0
4221+ for f in " $ENV_FILE " " Caddyfile" " ${CONFIG_FILE} .bak-control" ; do
4222+ [ -e " $f " ] || continue
4223+ # Fail safe: a chown that can't complete leaves the pre-existing bug, never corrupts state.
4224+ chown " $owner " " $f " 2> /dev/null ||
4225+ warn " Could not re-own $f to $owner after the control apply — the operator may need to chown it by hand (#33)."
4226+ done
4227+ }
4228+
42054229# Commit: apply the HOST-SIDE staged copy from the matching preview. A tampered second request
42064230# can't swap the config — commit carries only the id; the config it applies is the one previewed.
42074231control_commit () { # <id> <actor> <control-dir>
@@ -4235,6 +4259,7 @@ control_commit() { # <id> <actor> <control-dir>
42354259 cp " $staged " " $CONFIG_FILE "
42364260 " $0 " apply -y > " $logf " 2>&1 || rc=$?
42374261 if [ " $rc " -eq 0 ]; then
4262+ control_reown_operator_files # the root apply wrote .env/Caddyfile as root — give them back (#33)
42384263 control_write_result " $cdir /results" " $id " " $( jq -n ' {status:"applied",ts:(now|floor)}' ) "
42394264 control_audit " $cdir /audit/control.log" " $id " " $actor " " commit" " applied" " $keys "
42404265 else
0 commit comments