VPS-ADMIN-SECURITY-R0 — Minimal Host Secret Hygiene & Infrastructure Closeout (2026-07-24)
VPS-ADMIN-SECURITY-R0 — Minimal Host Secret Hygiene & Infrastructure Closeout (2026-07-24)
VERDICT:
PASS_VPS_ADMIN_SECURITY_R0_INFRASTRUCTURE_CLOSEOUTMissionVPS-ADMIN-SECURITY-R0 SIMPLE REV1Host Contabovmi3080463(38.242.240.89) · disk/dev/sda1· UTC window 2026-07-24 ~07:52Z → ~08:35Z Authorization accepted:PASS_VPS_CLEAN_MINIMUM_STABLE_HEADROOMCanonical inputs:vps-clean-minimum-2026-07-24.md,vps-recovery-prep-2026-07-24.md,c2b1-xv-dot-only-admin-rotation-restart-post-restore-2026-07-23.mdMode: host-file changes only (compose token externalization + file-mode hygiene). All Directus/PostgreSQL access READ-ONLY. Zero container restart/recreate. Zero credential rotation. Zero Directus/schema/PG mutation.
Declared residuals carried by PASS (all by design, deferred to dedicated missions):
ROOT_CAN_TECHNICALLY_BYPASS_DOT = YES— this mission is not a root/SSH re-architecture.- §6
dot-apr-executebroken cron NOT disabled →HOLD_VPS_ADMIN_SECURITY_APR_DEPENDENCY_FOUND(disable-gate not fully met). - §7
workflow_steps/deletepermission NOT restored →HOLD_WORKFLOW_STEPS_DELETE_PRODUCT_REQUIREMENT_FOUND(product requirement; needs canary+DOT repair).
1. Verdict
PASS_VPS_ADMIN_SECURITY_R0_INFRASTRUCTURE_CLOSEOUT. The two actionable host-hygiene items closed with proof (compose secret externalized; world-readable secret file eliminated). The two permission/executor items were correctly classified and deferred (not acted on) because their disable/restore gates were not cleanly met — exactly the conservative outcomes the mission anticipated. No production disruption; all 10 core containers unchanged and healthy.
2. Baseline (Gate 0)
Rollback dir /root/vps-admin-security-r0/ (0700); pre-change copies (0600): docker-compose.yml.pre, env.pre, env.local.pre, root-crontab.pre.
| Item | Baseline |
|---|---|
Disk / |
61% used, 39 GB free |
| Core containers | 10, all running/healthy (nginx no healthcheck) |
| Pre-existing restart counts | claude-kb=2, claude-mcp=2, nuxt=2 (heap-OOM history), rest=0 |
docker-compose.yml |
mode 644 root:root, sha256 d4a0dc48… |
docker/.env |
mode 600 root:root, sha256 ed4befea… |
.env.local |
mode 644 root:root, sha256 eebdfc43… |
| Live admin sessions | 0 (1 expired row) |
| Machine-transition perms | 36 |
| Root crontab | captured (9167 B) |
Token identity (3-way, hash-only, token never printed): compose L174 literal == running nuxt Config.Env == machine-transition token (/etc/incomex/c2b1/machine-transition.env key MACHINE_TRANSITION_DIRECTUS_TOKEN) — all sha256 9646908d… (64-char). The literal is the non-admin machine identity token, not the retired admin token.
3. Compose secret externalization (§4)
NUXT_DIRECTUS_SERVICE_TOKEN was a LITERAL at docker-compose.yml:174 (single occurrence; single compose file, no override; COMPOSE_FILE unset). Atomic patch:
- Appended the exact token to
/opt/incomex/docker/.env(value byte-identical,9646908d…). - Replaced L174 literal →
${NUXT_DIRECTUS_SERVICE_TOKEN:?NUXT_DIRECTUS_SERVICE_TOKEN_required}. - Atomic temp-file+rename, owner/group preserved.
Proof (post-patch): docker compose config --quiet exit 0; rendered token sha256 9646908d… (unchanged).
COMPOSE_CONTAINS_LITERAL_MACHINE_TOKEN = NO
COMPOSE_USES_REQUIRED_VARIABLE = YES
DOTENV_TOKEN_EQUALS_RUNNING_TOKEN = YES
DOTENV_TOKEN_EQUALS_MACHINE_IDENTITY = YES
FUTURE_NUXT_RECREATE_CONFIG_VALID = YES
No docker compose up, no nuxt recreate. New hashes: compose 2be202e9…, .env da2b3eee….
4. File permission hygiene (§5)
Consumer analysis proved the split: docker/.env and docker-compose.yml are read only by root (verified: the incomex user cannot read docker/.env, so it cannot run this compose project). .env.local (holds DIRECTUS_ADMIN_EMAIL/PASSWORD/TOKEN) has a live non-root reader: incomex-crontab tools dot-context-pack-build.sh/verify.sh → source dot/config/environment.sh → get_admin_credentials() greps .env.local (incomex uid=1001, gid=1001 only). No writer to .env.local exists (read-only). No container mounts any of these files.
| File | Before | After | Rationale |
|---|---|---|---|
docker/.env |
600 root:root | 600 root:root (unchanged) | already correct |
docker-compose.yml |
644 root:root | 640 root:root | root-only consumer; ≤0640 ceiling; literal secret already removed |
.env.local |
644 root:root | 640 root:incomex | remove world-read, preserve incomex group-read (strictly tighter than 644, zero risk) |
Consumer preservation verified: incomex still reads .env.local via group; incomex still cannot read compose (expected). /etc/incomex/*.env (machine-transition, breakglass/current, agent-data, reconcile/directus-admin) and scripts/cron-env.sh already 0600.
WORLD_READABLE_ACTIVE_SECRET_FILES = 0
5. dot-apr-execute disposition (§6) — HOLD, cron untouched
*/5 cron dot-apr-execute --local is fully broken: every run fails curl (7) Failed to connect to localhost port 8055 (it targets localhost:8055; Directus is only at directus:8055 / 172.18.0.4:8055). Log 9.6 MB, all-error, 0 success. DB confirms 0 APRs applied in 90 days (last applied_at 2026-04-20).
Disable-gate (all 4 required) — NOT met:
RECENT_SUCCESSFUL_EXECUTIONS = 0✅ (met)PENDING_REQUIRED_WORK = 0❌ — APR-0234 (id 234, auto-approved 2026-04-20,applied_atNULL) is unexecuted approved work, and carries dedicated reconcile viewsv_apr_0234_reconcile_plan/status. Adjudicating it inert is an APR content decision the mission forbids.CURRENT_FUNCTIONAL_PATH_USES_OTHER_DOT = YES❌ — the recent APR-0447 (2026-07-23) was applied viadot-pg-atomic-apply(a registration APR,applied_atNULL), not proven to execute actionable approved APRs; active Directus flow[S155-P1c] APR Created → Execute Signal(d5bef140) still wired into the chain.LIVE_DEPENDENTS— only.bak-*files/reports reference the executor; none active.
→ HOLD_VPS_ADMIN_SECURITY_APR_DEPENDENCY_FOUND. Cron line and script not touched. Log growth is benign (~1.7 MB/month; 39 GB free). Defer to a dedicated APR mission with canary + DOT.
6. workflow_steps/delete classification (§7) — HOLD, no permission change
Machine-transition-policy (7c19e8cc…) has workflow_steps create/read/update but not delete (former perm id 1550 gone; total machine perms = 36). Evidence of live requirement:
- Recent valid use: the machine-transition identity itself performed 4 workflow_steps deletes on 2026-07-22 (11:17–14:40); admin's 5 deletes were back in March.
- Governed capability:
dot/bin/dot-schema-workflow-governance-ensure(the "AI Agent" permission baseline) explicitly declaresensure_permission "workflow_steps" "delete" '["id"]'— the product's own schema governance mandates it. - Active integration: flow route "Workflow Steps Delete → AD".
- Nuance: no denied ops in 48h; governance tool is manual (not scheduled); recurring
reconcile-tasksdoes not delete workflow_steps.
→ HOLD_WORKFLOW_STEPS_DELETE_PRODUCT_REQUIREMENT_FOUND. PERMISSION_RESTORED = NO; baseline not ratified at 36 (governed intent is 37). Re-grant belongs to a product-specific permission repair with canary + explicit DOT mutation.
7. Expired-session classification (§8)
1 admin session row (admin@example.com) expired 2026-07-23 09:49:16Z — dead ~22.5 h ago (is_live = false).
ACTIVE_ADMIN_SESSIONS = 0
EXPIRED_ADMIN_ROWS = 1
EXPIRED_SESSION_ROW_IS_LIVE_CREDENTIAL = NO
SESSION_TABLE_MUTATION = 0 (row NOT deleted)
8. Root/DOT enforcement residual (§9)
ROOT_CAN_TECHNICALLY_BYPASS_DOT = YES
DIRECT_AGENT_WRITE_USED = 0 (0 Directus/PG writes this mission)
ROOT_OPERATOR_TRUST_BOUNDARY = CURRENT_ONE_VPS_OPERATIONAL_MODEL
Compensating controls intact: Directus/Schema/PG mutation via DOT; reports carry mutation ledger; break-glass root:0600 (/etc/incomex/c2b1/breakglass/current.env = 600 root:root); admin machine consumer = 0; backup/restore proven. SSH/sudo/agent-runner/Mac control plane unchanged.
9. Config checkpoint (§10)
Reused the production pipeline primitives (backup-to-gdrive.sh: recipient pubkey /etc/incomex/backup/directus-backup-recipient.pub, gpg --recipient-file --encrypt, rclone gdrive-backup:), config-only (DB backup/restore not re-run), into a dedicated subfolder to avoid retention rotation.
Artifact: incomex-encrypted-v1/vps-admin-security-r0/incomex-config-checkpoint-20260724T083243Z.tar.gz.gpg (+.meta.json). Bundle = patched compose + root-only docker/.env + .env.local + root-crontab + MANIFEST (modes/hashes).
CONFIG_CHECKPOINT_CREATED = YES
REMOTE_READBACK_HASH_MATCH = YES (da59f81d…, 8151 B)
DOWNLOAD_ROUNDTRIP_HASH_MATCH = YES
CONTENT_LIST_HAS_COMPOSE_ENV_CRONTAB = YES
CIPHERTEXT_RECIPIENT_VERIFIED = YES (keyid E800896AF06E9B61, local + downloaded copy)
PLAINTEXT_RESIDUE_ZERO = YES (shredded)
PRIVATE_KEY_READ_OR_IMPORTED = NO (0 secret keys on host & Mac)
Deviation (declared): plaintext decrypt-verify was substituted with recipient-keyid verification — matching the pipeline's own VERIFY stage — plus content-list-before-encryption and a genuine download round-trip. Full private-key decrypt is by off-VPS custody (OFF_VPS_DECRYPTION_RECOVERY = PASS, 2026-07-24), because PERSISTENT_GPG_PRIVATE_KEY_ON_VPS = 0 and §3 forbids reading the private key. Neither VPS nor Mac holds the private key; none imported.
10. Product smoke (§11) — all PASS
DISK_USED_PERCENT = 61 (≤65) DISK_FREE_GB = 39 (≥35)
CORE_CONTAINERS = 10 CORE_CONTAINER_CIDS_UNCHANGED = YES (10/10)
CORE_IMAGE_IDS_UNCHANGED = YES PRODUCTION_CONTAINER_RESTARTED = 0 (restart counts 10/10 unchanged)
DIRECTUS_HEALTH = PASS (healthy + /server/health 200)
NUXT_HOME = PASS (200 via nginx) NUXT_API_HEALTH = PASS (/api/health 200)
AGENT_DATA_HEALTH = PASS AGENT_API_EXECUTOR_HEALTH = PASS
CLAUDE_MCP_HEALTH = PASS CLAUDE_KB_HEALTH = PASS
UPTIME_KUMA_HEALTH = PASS POSTGRES_HEALTH = PASS QDRANT_HEALTH = PASS
NGINX_HEALTH = PASS (:80 301→https, :443 200)
BALO_GUARD_37 = PASS (checks=37, exit 0; socket/gid-987/compose contract intact)
SHARED_MACHINE_IDENTITY = PASS ACTIVE_ADMIN_SESSIONS = 0
WORLD_READABLE_ACTIVE_SECRET_FILES = 0
DIRECTUS_SCHEMA_PG_MUTATION = 0 (machine perms 36, sessions/workflow_steps=70/approval_requests=232 unchanged)
CREDENTIAL_ROTATION = 0 MAC_FILES_MUTATED = 0 (only session scratchpad + agent memory; no MCP/SSH/system files)
11. Exact next gate
RETURN_TO_PRODUCT_WORK.
Two deferred sub-missions (do NOT block product work; each needs its own canary + DOT):
- APR executor repair — fix
dot-apr-executetarget (localhost:8055→directus:8055) or retire it, and adjudicate APR-0234; not an infrastructure task. - workflow_steps/delete permission repair — re-grant former perm 1550 to the machine policy per
dot-schema-workflow-governance-ensure, with canary and explicit DOT mutation.
No further infrastructure mission opened.