fix: restrict runner log ACL across rotation
CI / tests-and-images (pull_request) Failing after 12s
Runner smoke test / runner-smoke (pull_request) Successful in 4s

This commit is contained in:
agent committed 2026-10-14 23:47:57 +03:00
1 parent 73aa147881
commit 7701e77b3d
7 files changed
+102 -9

No files matched your search

+2
View File
@@ -33,6 +33,7 @@ jobs:
run: |
bash infra/runner/test-runner-config.sh
bash infra/runner/test-ci-container-operations.sh
bash infra/runner/test-runner-log-acl.sh
bash infra/runner/test-install-runner-idempotency.sh
bash infra/runner/test-install-runner-guard.sh
- name: Check infrastructure shell syntax
@@ -45,6 +46,7 @@ jobs:
infra/runner/install-runner.sh infra/runner/install-runner-lib.sh \
infra/runner/act-runner-healthcheck.sh infra/runner/test-runner-config.sh \
infra/runner/test-ci-container-operations.sh \
infra/runner/test-runner-log-acl.sh \
infra/runner/test-install-runner-idempotency.sh infra/runner/test-install-runner-guard.sh
python3 -c "import ast,pathlib; ast.parse(pathlib.Path('infra/runner/secure-runner-image.py').read_text())"
- name: Run rollback helper tests
+6 -3
View File
@@ -22,9 +22,12 @@ checks. Never print a private key, token, or complete environment file.
- SSH alias: `akyldash_vps`, user `codex`, host `206.223.249.118`, port `22`.
- SSH configuration: `~/.ssh/config`.
- Identity file: `~/.ssh/akyldash_codex` (path only; never copy its contents).
- `codex` can run unprivileged read-only diagnostics. It has no sudo or Docker
group membership; Docker CLI access to the daemon is denied. Do not add it to
either group.
- `codex` can run unprivileged read-only diagnostics and read
`/var/log/akyldash-runner/act_runner.log` through a file ACL. It has no sudo
or Docker group membership; Docker CLI access to the daemon is denied. Do not
add it to either group. The runner installer in PR #46 narrows this to search
permission on the dedicated log directory and read permission on that one
file, preserved by systemd startup and logrotate hooks.
- Bootstrap is complete. Docker and Compose are active, but no root-owned
repository checkout or restricted helper is installed yet. The owner can run
`infra/bootstrap/audit-host.sh` from the reviewed root-owned checkout to
+17 -2
View File
@@ -47,6 +47,16 @@ systemd timer checks the runner service and the rootless Docker API each minute;
on failure it restarts the fixed runner service. No UFW changes or public
metrics ports are needed. Runner cache is disabled.
The runner daemon log is `/var/log/akyldash-runner/act_runner.log`. The
installer keeps its directory root-owned, grants `codex` only search permission
on that directory, and limits file access to the runner owner plus a named
read-only ACL for `codex`; group and other users have no file access. Systemd
starts with `UMask=0077`, and its fixed `ExecStartPost` command reapplies the
file ACL. Logrotate uses `copytruncate` and reapplies the same ACL after each
rotation. This access is limited to the runner daemon log; it does not grant
access to runner registration state, job workspaces, Docker state, or Gitea
workflow logs. The daemon log records task dispatch metadata, not step output.
The registered project label is `akyldash-ci`, mapped to the pinned
`docker.gitea.com/runner-images:ubuntu-24.04` image. Both `ci.yaml` and
`runner-smoke.yaml` target this exact project label; neither uses the generic
@@ -231,5 +241,10 @@ that `runner-smoke.yaml` succeeds. Read-only checks on 2026-10-14 confirmed the
runner service and health timer are active, the runner account has no
supplementary groups, and the active service uses the rootless Docker socket.
The live service has `ProtectHome=read-only`; the installer still needs the PR
update applied by an owner-run command after merge. The repository runner
inventory API returns 403 to the current non-owner API account.
update applied by an owner-run command after merge. Read-only SSH now confirms
that act_runner 0.2.13 declares `[akyldash-ci]` and accepts task 301; its log
does not include workflow step output. The Actions jobs API returns an empty
list for completed run 49, so step details are not available through the
current API account. The repository runner inventory API returns 403 to the
current non-owner API account. PR #46 pins the narrow log ACL and its rotation
hooks; this installer change has not been applied to the VPS.
+15
View File
@@ -48,6 +48,14 @@ unverified.
filters are unset; the live process itself runs as UID/GID `999:988`, has
effective and permitted capabilities `0`, and `NoNewPrivs=1`. No
supplementary groups are present.
- The owner granted `codex` ACL access to `/var/log/akyldash-runner/act_runner.log`.
Read-only SSH confirmed the directory is `akyldash-runner:akyldash-runner`
mode `0750` with `codex:r-x`, while the log is `root:root` mode `0644` with
`codex:r--` and `other::r--`. The inspected log contains runner task metadata
only, no workflow step output. The active log is therefore currently
world-readable; PR #46 changes IaC to a root-owned directory, runner-owned
private log, `codex` search/read ACLs only, and ACL reapplication after
service startup and log rotation. No VPS changes were made.
- The unprivileged check did not find `/var/run/docker.sock`; the runner user
is not in `sudo` or `docker` groups. Production secret file metadata remains
root-only and was not independently checked. The Gitea runner inventory API
@@ -127,6 +135,13 @@ unverified.
isolated BuildKit VM reached through a short-lived, TLS-authenticated
`buildctl` connection. No VPS changes were made; image builds remain
unverified until the owner provisions that builder.
- Runner log lines for run 49 show act_runner 0.2.13 with registered label
`[akyldash-ci]` accepted task 301 at 2026-10-15 01:16:11 +05. The main CI
task failed at 01:16:24 +05, and smoke task 302 started immediately after.
These daemon logs do not record the failed step. Gitea 1.25.5's Actions jobs
endpoint returns an empty list for completed run 49; the current API account
cannot use the owner-only jobs endpoint to obtain the log download ID. The
exact failing step is undetermined.
- An isolated local Compose smoke test built backend/OpenSearch images,
reached healthy status for OpenSearch, backend, and Caddy, and returned HTTP
200 for `/review` through Caddy. Its containers, networks, and volumes were
+3 -1
View File
@@ -115,8 +115,10 @@ target:
service_status: active; owner reports registered and Idle; owner reports smoke workflow passes on akyldash-ci
systemd_protect_home: read-only in the live unit as a temporary owner change; installer update is in PR
systemd_verification: verify passes; security exposure 7.4 medium; live runner process UID/GID 999:988, effective/permitted capabilities 0, NoNewPrivs 1, no supplementary groups
runner_log_observed: /var/log/akyldash-runner/act_runner.log; codex ACL read works; current file root:root 0644 is also other-readable; log contains task metadata but no job step output
runner_log_acl_iac: root-owned directory with codex search-only ACL; runner-owned private log with codex read-only ACL; systemd and logrotate reapply; not installed on VPS
ci_status: main workflow uses ubuntu-latest in main and has no matching runner; PR changes it to akyldash-ci
owner_action: review and merge the PR, then rerun the root installer to pin ProtectHome and verify the CI workflow's isolated image-build path
owner_action: review and merge the PR, then rerun the root installer to pin ProtectHome and runner log ACLs; inspect run 49 step logs because current non-owner API lacks access
checks:
- infra/bootstrap/verify.sh akyldash_vps
- docker compose -f deploy/production/compose.yaml ps
+27 -3
View File
@@ -8,6 +8,8 @@ readonly runner_xdg_config_home="$runner_home/.config"
readonly runner_xdg_data_home="$runner_runtime_home/.local/share"
readonly runner_store="$runner_home/docker-data"
readonly runner_image="$runner_home/docker-store.img"
readonly runner_log_dir=/var/log/akyldash-runner
readonly runner_log="$runner_log_dir/act_runner.log"
readonly runner_mount_unit_name="$(systemd-escape --path --suffix=mount /var/lib/akyldash-runner/docker-data)"
readonly runner_mount_unit="/etc/systemd/system/$runner_mount_unit_name"
readonly runner_image_bytes=16106127360
@@ -76,7 +78,7 @@ for file in /etc/subuid /etc/subgid; do
awk -F: -v user="$runner_user" -v start="$subid_start" -v end="$subid_end" '$1 != user && $2 <= end && $2 + $3 - 1 >= start { conflict=1 } END { exit conflict }' "$file" || die "runner subordinate ID range conflicts with another entry in $file"
done
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y uidmap dbus-user-session curl logrotate docker-ce-rootless-extras
DEBIAN_FRONTEND=noninteractive apt-get install -y uidmap dbus-user-session curl logrotate acl docker-ce-rootless-extras
runner_ensure_account "$runner_user" "$runner_home" || die 'runner account creation failed'
runner_lock_home "$runner_home" || die 'runner home protection failed'
@@ -106,8 +108,25 @@ if [[ ! -e $runner_store && ! -L $runner_store ]]; then
install -d -o root -g root -m 0755 "$runner_store"
fi
[[ -d $runner_store && ! -L $runner_store ]] || die 'runner mount path is not a directory'
install -d -o root -g "$runner_user" -m 0750 /etc/akyldash-runner
install -d -o "$runner_user" -g "$runner_user" -m 0750 /var/log/akyldash-runner
install -d -o root -g "$runner_user" -m 0750 /etc/akyldash-runner "$runner_log_dir"
[[ -d $runner_log_dir && ! -L $runner_log_dir ]] || die 'runner log directory must be a real directory'
[[ ! -L $runner_log ]] || die 'runner log path must not be a symbolic link'
if [[ -e $runner_log ]]; then
[[ -f $runner_log ]] || die 'runner log path must be a regular file'
else
install -o "$runner_user" -g "$runner_user" -m 0600 /dev/null "$runner_log"
fi
chown "$runner_user:$runner_user" "$runner_log"
chmod 0600 "$runner_log"
setfacl -b "$runner_log_dir"
setfacl -k "$runner_log_dir"
setfacl -b "$runner_log"
setfacl -m u:codex:--x "$runner_log_dir"
setfacl -m u:codex:r--,g::---,m::r-- "$runner_log"
getfacl -cp "$runner_log_dir" | grep -Fqx 'user:codex:--x' || die 'runner log directory ACL is not restrictive'
getfacl -cp "$runner_log" | grep -Fqx 'user:codex:r--' || die 'runner log read ACL is missing'
getfacl -cp "$runner_log" | grep -Fqx 'group::---' || die 'runner log is readable by its group'
getfacl -cp "$runner_log" | grep -Fqx 'other::---' || die 'runner log is readable by other users'
runner_ensure_image "$runner_image" "$runner_image_bytes" 0 711 || die 'runner image initialization failed'
runner_ensure_mount "$runner_image" "$runner_store" "$runner_mount_unit_name" "$runner_mount_unit" || die 'runner mount activation failed'
@@ -187,6 +206,8 @@ RestrictSUIDSGID=yes
ReadWritePaths=$runner_runtime_home $runner_xdg_config_home /var/log/akyldash-runner /run/user/$runner_uid
StandardOutput=append:/var/log/akyldash-runner/act_runner.log
StandardError=append:/var/log/akyldash-runner/act_runner.log
UMask=0077
ExecStartPost=/usr/bin/setfacl -m u:codex:r--,g::---,m::r-- /var/log/akyldash-runner/act_runner.log
[Install]
WantedBy=multi-user.target
@@ -208,6 +229,9 @@ runner_install_content /etc/logrotate.d/akyldash-runner root root 0644 <<'EOF'
missingok
notifempty
copytruncate
postrotate
/usr/bin/setfacl -m u:codex:r--,g::---,m::r-- /var/log/akyldash-runner/act_runner.log
endscript
}
EOF
runner_install_file "$(dirname "$0")/akyldash-runner-healthcheck.service" /etc/systemd/system/akyldash-runner-healthcheck.service root root 0644
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
installer=infra/runner/install-runner.sh
grep -Fq 'logrotate acl docker-ce-rootless-extras' "$installer"
grep -Fq 'readonly runner_log_dir=/var/log/akyldash-runner' "$installer"
grep -Fq 'readonly runner_log="$runner_log_dir/act_runner.log"' "$installer"
grep -Fq 'install -d -o root -g "$runner_user" -m 0750 /etc/akyldash-runner "$runner_log_dir"' "$installer"
grep -Fq 'install -o "$runner_user" -g "$runner_user" -m 0600 /dev/null "$runner_log"' "$installer"
grep -Fq 'chown "$runner_user:$runner_user" "$runner_log"' "$installer"
grep -Fq 'chmod 0600 "$runner_log"' "$installer"
grep -Fq 'setfacl -b "$runner_log_dir"' "$installer"
grep -Fq 'setfacl -k "$runner_log_dir"' "$installer"
grep -Fq 'setfacl -b "$runner_log"' "$installer"
grep -Fq 'setfacl -m u:codex:--x "$runner_log_dir"' "$installer"
grep -Fq 'setfacl -m u:codex:r--,g::---,m::r-- "$runner_log"' "$installer"
grep -Fq "grep -Fqx 'group::---'" "$installer"
grep -Fq "grep -Fqx 'other::---'" "$installer"
grep -Fq 'UMask=0077' "$installer"
grep -Fq 'ExecStartPost=/usr/bin/setfacl -m u:codex:r--,g::---,m::r-- /var/log/akyldash-runner/act_runner.log' "$installer"
grep -Fq ' copytruncate' "$installer"
grep -Fq ' postrotate' "$installer"
grep -Fq ' /usr/bin/setfacl -m u:codex:r--,g::---,m::r-- /var/log/akyldash-runner/act_runner.log' "$installer"
grep -Fq ' endscript' "$installer"
if grep -Eq 'setfacl .*(-R|default:)|chmod 0644.*act_runner.log|other::r--' "$installer"; then
echo 'Runner log ACL must stay limited to codex reading the single log file' >&2
exit 1
fi
echo 'runner log ACL checks passed'