Expose drift verifier through a fixed sudo helper
CI / tests-and-images (pull_request) Has been cancelled
CI / tests-and-images (pull_request) Has been cancelled
This commit is contained in:
1 parent
711700fcc9
commit
02cf4e6e95
10 files changed
+52
-25
No files matched your search
@@ -21,6 +21,10 @@ jobs:
|
||||
run: python3 -m unittest discover -s infra/backup -p 'test_*.py' -v
|
||||
- name: Check recovery host guard
|
||||
run: bash infra/bootstrap/test-host-guard.sh
|
||||
- name: Check infrastructure shell syntax
|
||||
run: |
|
||||
bash -n infra/bootstrap/bootstrap-ubuntu-24.04.sh infra/bootstrap/host-guard.sh \
|
||||
infra/bootstrap/verify.sh infra/bootstrap/verify-helper.sh infra/deploy/install-helper.sh
|
||||
- name: Run rollback helper tests
|
||||
run: python3 -m unittest discover -s infra/deploy -p 'test_*.py' -v
|
||||
- name: Validate production Compose and Caddyfile
|
||||
|
||||
@@ -26,9 +26,9 @@ checks. Never print a private key, token, or complete environment file.
|
||||
- Host package installation, service setup, firewall changes, secret creation,
|
||||
backup creation, recovery apply, and deployment require owner approval and a
|
||||
reviewed root command. After the owner installs the root-owned repository
|
||||
checkout, `infra/deploy/install-helper.sh` can grant only the validated
|
||||
rollback command through sudoers; that helper is not installed on the audited
|
||||
VPS.
|
||||
checkout, `infra/deploy/install-helper.sh` can grant a fixed read-only
|
||||
verifier and guarded rollback command through sudoers; neither helper is
|
||||
installed on the audited VPS.
|
||||
- Never connect to other VPS hosts or Synology as part of this deployment.
|
||||
|
||||
This execution environment is WSL2. The repository is on the Windows-mounted
|
||||
@@ -59,6 +59,7 @@ limitation and do not retrieve it elsewhere.
|
||||
| Role | Operations |
|
||||
|---|---|
|
||||
| `codex` over SSH | Read-only host diagnostics; inspect service health and public listeners |
|
||||
| Restricted verifier | Runs the root-owned read-only drift check; not installed on the audited VPS |
|
||||
| Restricted rollback helper | Can restore prior local backend/OpenSearch images from a verified backup after an explicit data-compatibility confirmation; not installed on the audited VPS |
|
||||
| VPS owner/root | Bootstrap, package installation, SSH/firewall, secret setup, recovery, and approval of production changes |
|
||||
| Gitea repository writer | Feature branch push and PR creation; protected-main merge needs an approval |
|
||||
@@ -52,8 +52,9 @@ from planned configuration.
|
||||
- Recovery bootstrap has an explicit `--recovery-target` mode that refuses the
|
||||
inventoried production hostname/IP; its host-identity guard has shell tests.
|
||||
The mode has not been run on a VPS.
|
||||
- The repository defines a root-owned rollback helper and owner-run installer
|
||||
with a single-command sudoers allowlist. Neither is installed on the VPS.
|
||||
- The repository defines a root-owned read-only verifier, rollback helper, and
|
||||
owner-run installer with exact sudoers command rules. None is installed on
|
||||
the VPS.
|
||||
- An isolated local Compose smoke test built the 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,7 +3,7 @@
|
||||
| Date | Decision | Reason and alternatives | Status / limits |
|
||||
|---|---|---|---|
|
||||
| 2026-10-11 | Use Docker Compose on the dedicated Ubuntu VPS | Matches the existing production Compose baseline and workload; Kubernetes adds no demonstrated need | Planned; Docker is not installed |
|
||||
| 2026-10-11 | Keep `codex` out of sudo and the Docker group | Docker socket access is effectively root; a separate installer can grant only the root-owned rollback helper after checkout ownership and sudoers validation | Helper is defined in Git; not installed on VPS |
|
||||
| 2026-10-11 | Keep `codex` out of sudo and the Docker group | Docker socket access is effectively root; a separate installer can grant only a fixed read-only verifier and root-owned rollback helper after checkout ownership and sudoers validation | Helpers are defined in Git; not installed on VPS |
|
||||
| 2026-10-11 | Put Caddy and Search API on a proxy network and OpenSearch on a separate internal network | Keeps OpenSearch unreachable from the proxy and host; the API has no published host port | Repository Compose target only; not deployed |
|
||||
| 2026-10-11 | Use Caddy in Compose for HTTPS at `akyldash.kg` | Automatic certificate issuance/renewal keeps proxy config small; Nginx with separately managed certificates was considered | Caddy image pinned by digest; requires public ports 80/443 and owner confirmation of Cloudflare Full (strict) |
|
||||
| 2026-10-11 | Mount the review signing key as a Compose secret file | Avoids putting its value in command-line arguments; an environment variable or CLI value is easier to inspect in process metadata | Implemented in repository Compose; not deployed |
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
Production deployment is not enabled or tested on the VPS. An isolated local
|
||||
Compose smoke test passed; the VPS has no Docker, Git,
|
||||
application data, proxy, registry pull configuration, backup process, or
|
||||
restricted deploy-helper. Do not deploy until the owner has completed the root
|
||||
bootstrap, provided a protected secret file and a reviewed proxy/firewall
|
||||
configuration, and a backup and restore have been tested.
|
||||
restricted verification/rollback helpers. Do not deploy until the owner has
|
||||
completed the root bootstrap, provided a protected secret file and a reviewed
|
||||
proxy/firewall configuration, and a backup and restore have been tested.
|
||||
|
||||
## First host preparation
|
||||
|
||||
@@ -23,9 +23,9 @@ configuration, and a backup and restore have been tested.
|
||||
`/etc/akyldash/production.env` plus `/etc/akyldash/review_secret` outside
|
||||
Git. The secret file must be root-readable only.
|
||||
3. After reviewing the root-owned checkout, run
|
||||
`sudo bash infra/deploy/install-helper.sh`. It installs the backup reader and
|
||||
rollback helper and adds a sudoers rule for that helper only; it does not
|
||||
grant general sudo or Docker access.
|
||||
`sudo bash infra/deploy/install-helper.sh`. It installs the backup reader,
|
||||
read-only verifier, and rollback helper with exact sudoers command rules; it
|
||||
does not grant general sudo or Docker access.
|
||||
4. Confirm DNS points through Cloudflare to this origin, then have the owner
|
||||
review firewall rules and open only TCP `80` and `443` in addition to the
|
||||
existing SSH access. The Compose Caddy service obtains and renews its HTTPS
|
||||
|
||||
@@ -21,9 +21,9 @@ run recovery commands against the existing production by accident.
|
||||
recovery mode refuses the inventoried production hostname/IP and writes a
|
||||
root-only marker at `/etc/akyldash/recovery-target`. It installs Docker
|
||||
Engine, Compose, Git, Docker log limits, and target directories.
|
||||
4. Install the limited rollback helper with
|
||||
4. Install the limited read-only verifier and rollback helper with
|
||||
`sudo bash infra/deploy/install-helper.sh`. It validates the root-owned
|
||||
checkout and gives `codex` access only to that helper.
|
||||
checkout and gives `codex` access only to those fixed commands.
|
||||
5. Configure DNS to point through Cloudflare to the recovery host and allow the
|
||||
owner to review/open TCP `80` and `443`. The production Compose Caddy service
|
||||
obtains its origin certificate automatically. Verify Cloudflare Full
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
checkout=/opt/akyldash/current
|
||||
if [[ $# -ne 0 || $EUID -ne 0 || ! -d $checkout || -L $checkout || $(stat -c %U "$checkout") != root ]]; then
|
||||
echo 'Refusing verification: the approved root-owned checkout is unavailable.' >&2
|
||||
exit 1
|
||||
fi
|
||||
unsafe=$(find "$checkout" -xdev \( ! -user root -o -perm /022 \) -print -quit)
|
||||
if [[ -n $unsafe ]]; then
|
||||
echo 'Refusing verification: the checkout contains a writable or non-root-owned path.' >&2
|
||||
exit 1
|
||||
fi
|
||||
exec /bin/bash "$checkout/infra/bootstrap/verify.sh" --remote
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
|
||||
if [[ ${1:-} != --remote ]]; then
|
||||
ssh_alias=${1:-akyldash_vps}
|
||||
exec ssh -F "${HOME}/.ssh/config" -o BatchMode=yes "$ssh_alias" 'sudo -n bash -s -- --remote' < "$0"
|
||||
exec ssh -F "${HOME}/.ssh/config" -o BatchMode=yes "$ssh_alias" 'sudo -n /usr/local/sbin/akyldash-verify'
|
||||
fi
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
@@ -129,14 +129,15 @@ for endpoint in $udp_listeners; do
|
||||
failed=1
|
||||
done
|
||||
|
||||
sudoers_rule='codex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-rollback *'
|
||||
if [[ -x /usr/local/sbin/akyldash-rollback && $(stat -c %U:%a /usr/local/sbin/akyldash-rollback) == root:750 && \
|
||||
expected_sudoers=$'codex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-verify\ncodex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-rollback *'
|
||||
if [[ -x /usr/local/sbin/akyldash-verify && $(stat -c %U:%a /usr/local/sbin/akyldash-verify) == root:750 && \
|
||||
-x /usr/local/sbin/akyldash-rollback && $(stat -c %U:%a /usr/local/sbin/akyldash-rollback) == root:750 && \
|
||||
-f /usr/local/lib/akyldash/archive.py && $(stat -c %U:%a /usr/local/lib/akyldash/archive.py) == root:644 && \
|
||||
-f /etc/sudoers.d/akyldash-codex && $(stat -c %U:%a /etc/sudoers.d/akyldash-codex) == root:440 && \
|
||||
$(cat /etc/sudoers.d/akyldash-codex) == "$sudoers_rule" ]]; then
|
||||
echo 'PASS limited rollback helper and sudoers rule present'
|
||||
$(cat /etc/sudoers.d/akyldash-codex) == "$expected_sudoers" ]]; then
|
||||
echo 'PASS read-only verifier, rollback helper, and exact sudoers rules present'
|
||||
else
|
||||
echo 'FAIL limited rollback helper or sudoers rule missing/different'
|
||||
echo 'FAIL restricted helper or sudoers installation missing/different'
|
||||
failed=1
|
||||
fi
|
||||
if [[ -f /opt/akyldash/deployed-image.txt ]]; then
|
||||
|
||||
@@ -9,7 +9,8 @@ fi
|
||||
checkout=/opt/akyldash/current
|
||||
rollback_source=$checkout/infra/deploy/rollback.py
|
||||
backup_source=$checkout/infra/backup/archive.py
|
||||
if [[ ! -d $checkout/.git || ! -f $rollback_source || ! -f $backup_source ]]; then
|
||||
verify_source=$checkout/infra/bootstrap/verify-helper.sh
|
||||
if [[ ! -d $checkout/.git || ! -f $rollback_source || ! -f $backup_source || ! -f $verify_source ]]; then
|
||||
echo "Install the approved repository checkout at $checkout first." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -32,7 +33,8 @@ if grep -R -nE '(^|[[:space:],])codex([[:space:],]|$)' /etc/sudoers /etc/sudoers
|
||||
echo "Existing codex sudoers rules need owner review before installing the helper." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(stat -c %U "$checkout") != root ]] || find "$checkout" -xdev \( ! -user root -o -perm /022 \) -print -quit | grep -q .; then
|
||||
unsafe=$(find "$checkout" -xdev \( ! -user root -o -perm /022 \) -print -quit)
|
||||
if [[ $(stat -c %U "$checkout") != root || -n $unsafe ]]; then
|
||||
echo "The checkout must be root-owned and not group/world-writable." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -41,7 +43,7 @@ if ! command -v visudo >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for path in /usr/local/sbin/akyldash-rollback /usr/local/lib/akyldash/archive.py /etc/sudoers.d/akyldash-codex; do
|
||||
for path in /usr/local/sbin/akyldash-verify /usr/local/sbin/akyldash-rollback /usr/local/lib/akyldash/archive.py /etc/sudoers.d/akyldash-codex; do
|
||||
if [[ -L $path ]]; then
|
||||
echo "Refusing symlink at $path." >&2
|
||||
exit 1
|
||||
@@ -50,10 +52,11 @@ done
|
||||
|
||||
install -d -o root -g root -m 0755 /usr/local/lib/akyldash
|
||||
install -o root -g root -m 0644 "$backup_source" /usr/local/lib/akyldash/archive.py
|
||||
install -o root -g root -m 0750 "$verify_source" /usr/local/sbin/akyldash-verify
|
||||
install -o root -g root -m 0750 "$rollback_source" /usr/local/sbin/akyldash-rollback
|
||||
|
||||
sudoers_file=/etc/sudoers.d/akyldash-codex
|
||||
expected='codex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-rollback *'
|
||||
expected=$'codex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-verify\ncodex ALL=(root) NOPASSWD: /usr/local/sbin/akyldash-rollback *'
|
||||
created=false
|
||||
if [[ -e $sudoers_file ]]; then
|
||||
if [[ $(stat -c %U:%a "$sudoers_file") != root:440 || $(cat "$sudoers_file") != "$expected" ]]; then
|
||||
@@ -82,4 +85,4 @@ if ! visudo -c -f /etc/sudoers; then
|
||||
echo "Sudoers validation failed; the newly added rule was removed." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Installed root-owned rollback helper; codex receives no general sudo or Docker access."
|
||||
echo "Installed read-only verification and guarded rollback helpers; codex receives no general sudo or Docker access."
|
||||
@@ -70,6 +70,9 @@ target:
|
||||
restricted_helper:
|
||||
path: /usr/local/sbin/akyldash-rollback
|
||||
status: repository code and owner-run installer exist; not installed on VPS
|
||||
read_only_verifier:
|
||||
path: /usr/local/sbin/akyldash-verify
|
||||
status: repository code and owner-run installer exist; not installed on VPS
|
||||
checks:
|
||||
- infra/bootstrap/verify.sh akyldash_vps
|
||||
- docker compose -f deploy/production/compose.yaml ps
|
||||
|
||||
Reference in new issue
Block a user