fix: restrict runner log ACL across rotation
This commit is contained in:
1 parent
73aa147881
commit
7701e77b3d
7 files changed
+102
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
Reference in new issue
Block a user