KB-3992

VPS-ADMIN-SECURITY-R0 — Minimal Host Secret Hygiene & Infrastructure Closeout (2026-07-24)

12 min read Revision 1
current-statereportvps-admin-security-r0secret-hygienecompose-externalizationnuxt-directus-service-tokenfile-modesworld-readabledot-apr-executeworkflow-steps-deletepermission-driftholdconfig-checkpointgpgencrypted-backupzero-mutationread-onlyno-recreatePASS_VPS_ADMIN_SECURITY_R0_INFRASTRUCTURE_CLOSEOUTRETURN_TO_PRODUCT_WORK2026-07-24

VPS-ADMIN-SECURITY-R0 — Minimal Host Secret Hygiene & Infrastructure Closeout (2026-07-24)

VERDICT: PASS_VPS_ADMIN_SECURITY_R0_INFRASTRUCTURE_CLOSEOUT Mission VPS-ADMIN-SECURITY-R0 SIMPLE REV1 Host Contabo vmi3080463 (38.242.240.89) · disk /dev/sda1 · UTC window 2026-07-24 ~07:52Z → ~08:35Z Authorization accepted: PASS_VPS_CLEAN_MINIMUM_STABLE_HEADROOM Canonical 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.md Mode: 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-execute broken cron NOT disabledHOLD_VPS_ADMIN_SECURITY_APR_DEPENDENCY_FOUND (disable-gate not fully met).
  • §7 workflow_steps/delete permission NOT restoredHOLD_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:

  1. Appended the exact token to /opt/incomex/docker/.env (value byte-identical, 9646908d…).
  2. Replaced L174 literal → ${NUXT_DIRECTUS_SERVICE_TOKEN:?NUXT_DIRECTUS_SERVICE_TOKEN_required}.
  3. 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.shget_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_at NULL) is unexecuted approved work, and carries dedicated reconcile views v_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 via dot-pg-atomic-apply (a registration APR, applied_at NULL), 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 declares ensure_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-tasks does 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):

  1. APR executor repair — fix dot-apr-execute target (localhost:8055directus:8055) or retire it, and adjudicate APR-0234; not an infrastructure task.
  2. 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.