DIRECTUS-OWNER-SUPERADMIN-DUAL — Closeout Report (2026-07-24)
DIRECTUS-OWNER-SUPERADMIN-DUAL — Closeout Report
Verdict: PASS_DIRECTUS_DUAL_SUPERADMIN_OWNER_READY
Date (UTC): 2026-07-24
Entry gate: PASS_VPS_ADMIN_SECURITY_R0_INFRASTRUCTURE_CLOSEOUT / VPS_STABLE_FOR_PRODUCT_WORK=YES
Next gate: RETURN_TO_PRODUCT_WORK
Mutation surface: one directus_users row (owner) + one new Secret Manager secret. Zero container restarts. Zero credential values in this report.
0. Outcome — final topology achieved
nmhuyen@gmail.com = active Super Admin (Administrator) [OWNER, primary]
admin@example.com = active Super Admin (Administrator) [DEFAULT, preserved unchanged]
ACTIVE_FULL_ADMIN_COUNT = 2
ACTIVE_ADMIN_SESSIONS = 0
The owner was promoted by attaching the existing Administrator role — it inherits admin_access
through the existing role→policy link. No new role, policy, access row, or third admin was created.
The default admin was not touched in any way.
3. Gate 0 — measured live truth (read-only, matched accepted entry state)
| Identity | ID | Pre-state |
|---|---|---|
Owner nmhuyen@gmail.com |
ae44e653-df5d-4a3c-bf6e-714485b616d2 |
active · role NULL · admin_access false · old 32-char static token present · TFA off · provider default |
Default admin@example.com |
6abdec55-d911-44df-af96-3cf60b9654af |
active · role Administrator · admin_access true · password login PASS · static token PASS |
| Administrator role | a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac |
— |
| Administrator policy (admin_access=t, app_access=t) | 8a613123-2538-4943-9d67-b6d261a4789c |
— |
| role→policy access row | 37e64534-2c50-48cd-97e4-a4ddf5f3f0df |
role-based (no per-user row) |
| Machine-transition user | 73aa333b-3bf9-4763-b25f-27102ebb06b1 |
non-admin, identity intact |
ACTIVE_FULL_ADMIN_COUNT = 1,ACTIVE_ADMIN_SESSIONS = 0at entry.- Owner old token was orphaned: ≠ admin/machine/agent-data/SYNC/NUXT secrets; authenticated to nothing (no role → no perms); its only file references were three inert historical DB dumps (no live consumer). Safe to clear.
4. DOTs authored (minimal, paired, no new framework)
Reused-first check: dot-directus-breakglass-rotate is hard-scoped to admin@example.com only
("never touches any other user") → cannot promote the owner. Created exactly the two DOTs §4 allows:
| DOT | Tier | Scope |
|---|---|---|
/opt/incomex/dot/bin/dot-directus-owner-admin-promote |
B | ONE atomic PATCH of the owner user; rollback-on-failure; never touches default admin / machine identity / any authz object / any container |
/opt/incomex/dot/bin/dot-directus-owner-admin-verify |
A | read-only paired verifier; zero negative password probes (lockout-proof); one temporary owner session, revoked |
Both modelled on the proven break-glass pair: secret boundary via curl --config <(...) (never argv/temp
file), SIGPIPE-safe password generation, login-then-logout session hygiene, fail-closed traps. 0755 root:root.
5. Recovery checkpoint (encrypted, offsite) — before any mutation
Logical name: DIRECTUS_OWNER_PROMOTE_PRE_20260724T095012Z
Pipeline: existing backup-to-gdrive.sh (pg_dump | gzip | gpg streamed; plaintext never on disk).
Artifact: incomex-prod-directus-db-20260724T095012Z.sql.gz.gpg (+ .meta.json)
Remote: gdrive-backup:incomex-encrypted-v1/ (append-only prefix; nothing evicted)
UPLOAD = PASS
REMOTE_READBACK = PASS (downloaded from Drive, byte-for-byte)
SHA256_MATCH = YES 6fa87de7aa758f308da84573cb445713386e978f12f777fad5c59e67f471abc1
(status == meta.json == remote readback)
Ciphertext = OpenPGP pubkey enc, recipient keyid E800896AF06E9B61
Recipient fpr = 31799F7AC051EBA68FB3F13BDCC1369B371837BA (VPS holds pubkey only — cannot decrypt)
PLAINTEXT_RESIDUE = 0
No restore test performed (same pipeline restore-proven 2026-07-21 / 2026-07-24).
6–9. Owner promotion, custody, default preservation
Atomic promotion (dot-directus-owner-admin-promote --execute) — a single transactional PATCH,
authenticated by the break-glass admin static token (used as bearer only; never modified):
PATCH /users/ae44e653… { status: active, role: Administrator, password: <fresh 32-char>, token: null }
Post-conditions (PG ground truth + live), all PASS:
OWNER_ROLE = Administrator OWNER_ADMIN_ACCESS = true OWNER_STATUS = active
OWNER_STATIC_TOKEN = NULL OWNER_PASSWORD_LOGIN = PASS(200)
OWNER_OLD_STATIC_TOKEN = DENIED(401)
OWNER_CAN_READ_USERS/ROLES/POLICIES = PASS(200) OWNER_SERVER_INFO_ADMIN_VIEW = PASS
DEFAULT admin still active (untouched)
§6 fresh password: generated in-DOT (32-char alnum, no reuse of any prior owner/default/SM value); handled only inside the DOT boundary.
§9 custody:
/etc/incomex/c2b1/breakglass/owner-admin.env 0600 root:root (parent dir 0700)
OWNER_ENV_EMAIL_EQUALS_LIVE_USER = YES OWNER_ENV_PASSWORD_LOGIN = PASS
Owner password value present ONLY in owner-admin.env — absent from compose, docker/.env,
.env.local, crontab, source, Git, KB, MacBook.
current.env (default break-glass) UNCHANGED (mtime 2026-07-23 11:25).
The spent rollback snapshot (owner-promote-previous.env) was shredded after the old token was proven dead.
§8 default admin preserved: active · Administrator · password login PASS · static token PASS.
ACTIVE_FULL_ADMIN_COUNT = 2. Owner and default hold distinct tokens (owner NULL).
10. One Secret Manager bundle for both admins
Inventory: no existing structured dual-admin bundle (existing DIRECTUS_ADMIN_TOKEN is a single-value,
now-rotated-away token; unsuitable). Created exactly one:
Secret: DIRECTUS_SUPERADMIN_BUNDLE (project github-chatgpt-ggcloud / 812872501910)
Labels: purpose=directus-superadmin-dual, created-by=owner-promote-20260724
Versions: 1 (no prior version deleted — none existed)
Payload (JSON, one resource, two credential sets):
owner: {email: nmhuyen@gmail.com, password: <fresh>, static_token: null}
default: {email: admin@example.com, password: <current verified>, static_token: <current verified>}
Verified by readback + live auth (no value printed):
SM_OWNER_EMAIL_EQUALS_LIVE = YES SM_OWNER_TOKEN_IS_NULL = YES SM_OWNER_PASSWORD_AUTHENTICATES = PASS
SM_DEFAULT_EMAIL_EQUALS_LIVE = YES SM_DEFAULT_PASSWORD_AUTHENTICATES = PASS SM_DEFAULT_TOKEN_AUTHENTICATES = PASS
Payload built in tmpfs, added via --data-file (never argv), tmpfs shredded. Neither live account was
changed to match any old SM value.
11. Secret Manager call-frequency audit (read-only)
Runtime architecture (Directus service + six machine consumers):
NORMAL_RUNTIME_SECRET_MANAGER_READS = 0 (cron-env.sh sources /opt/incomex/docker/.env;
machine consumers read machine-transition.env — local only)
LOCAL_ROOT_ONLY_ENV_IS_RUNTIME_SOURCE = YES
SECRET_MANAGER_IS_OFF_VPS_RECOVERY_CUSTODY = YES
Classified gcloud secrets versions access call-sites:
| Call-site | Secret | Cadence | Class | Action |
|---|---|---|---|---|
deploys/web-test/scripts/integrity/watchdog-monitor.sh |
DIRECTUS_ADMIN_TOKEN |
hourly (fallback) | DEAD_OR_VESTIGIAL | HOLD (see below) |
deploys/web-test/scripts/integrity/cron-integrity.sh |
DIRECTUS_ADMIN_TOKEN |
6-hourly (fallback) | DEAD_OR_VESTIGIAL | HOLD (see below) |
scripts/git-push-gh-daily.sh (v1) |
gh_pat_sync_secrets |
not scheduled (v2 in cron has no SM call) | DEAD_OR_VESTIGIAL (superseded) | none |
dot/bin/dot-{e2e-test,pg-restore-verify,env-restore,schema-apply,knowledge-sync-agentdata,verify-ai-connections,ai-manifest} |
various | on-demand only (none in cron/timers) | RECOVERY_ONLY / manual | none |
Finding (HOLD sub-item — HOLD_SM_CADENCE_INTEGRITY_FALLBACK):
The two web-test integrity monitors read DIRECTUS_ADMIN_TOKEN from docker/.env first and fall back to
Secret Manager only if empty. The C2B1-XV rotation (2026-07-23) neutralized that key in docker/.env,
so the SM fallback now fires every run (hourly + 6-hourly). Evidence measured this session:
docker/.env value empty; SM DIRECTUS_ADMIN_TOKEN ≠ live admin token and returns HTTP 401
(rotated-away / dead). These scripts therefore fetch a dead token on a fixed cadence.
Why HOLD, not fix, in this mission:
- It is not the Directus service or the six machine-consumer runtime path (those are 0 SM reads).
- The fix is a code change to the
web-testintegrity subsystem (repoint to the correct current credential from a local canonical source, or gate the fallback), not a mere schedule change. - That subsystem is entangled with the open dieu31 investigation (the watchdog alerts are about "Runner Điều 31"). Safe reduction cannot be proven within this mission's scope.
SCHEDULE_MUTATION = 0. Owner promotion + SM bundle still PASS (§11 explicitly permits this).
Recommended follow-up (separate window): in watchdog-monitor.sh and cron-integrity.sh, replace the
DIRECTUS_ADMIN_TOKEN acquisition with the current canonical local credential (e.g. the shared
machine-transition token for read-only liveness checks), removing the SM fallback entirely →
FREQUENT_SM_CALLS → 0. Do not update the SM DIRECTUS_ADMIN_TOKEN to the live token: that would
institutionalise hourly SM reads of a live admin credential (worse).
12. Final verification
OWNER_SUPERADMIN_EMAIL = nmhuyen@gmail.com DEFAULT_SUPERADMIN_EMAIL = admin@example.com
OWNER_PASSWORD_LOGIN = PASS DEFAULT_PASSWORD_LOGIN = PASS
OWNER_STATIC_TOKEN = NULL DEFAULT_STATIC_TOKEN = PASS
OWNER_STATUS = active DEFAULT_STATUS = active
ACTIVE_FULL_ADMIN_COUNT = 2 ACTIVE_ADMIN_SESSIONS = 0
ONE_CANONICAL_SM_BUNDLE = PASS (DIRECTUS_SUPERADMIN_BUNDLE, 1 version)
NORMAL_RUNTIME_SM_READS = 0 (service+consumers); integrity-monitor fallback HELD (§11)
WORLD_READABLE_SECRET_FILES = 0 (credential custody/config)
SHARED_MACHINE_IDENTITY = PASS (granted 200 / admin surface 403 / token bound)
MACHINE_CONSUMERS = machine identity intact; nuxt homepage 200, agent-data healthy,
reconcile-tasks + reconcile-knowledge "All in sync" (4/6 live-checked;
dot-collection-health / dot-apr-health share the same intact identity and
run on their own cron — not invoked here, respecting the APR hard-lock)
CORE_CONTAINERS = 10 CORE_CONTAINER_CIDS_UNCHANGED = YES PRODUCTION_RESTARTED = 0
MAC_FILES_MUTATED = 0 (all work via SSH on the VPS; DOTs staged in ephemeral session
scratchpad then installed on the VPS; no MacBook / Mac MCP / user files touched)
ALL_DIRECTUS_MUTATIONS_VIA_DOT = YES DIRECT_AGENT_WRITE_COUNT = 0
Paired verifier --phase final: 37 PASS / 0 FAIL. No-drift snapshot: permissions 1243 · malformed 632 ·
policies 9 · roles 10 · access 10 (identity promotion structurally cannot alter these — only one user row changed).
13. Deferred items (remain deferred — untouched)
dot-apr-execute / APR-0234 · workflow_steps/delete permission · 632 malformed permission rows ·
GCP legacy cleanup · GitHub legacy cleanup · off-GCP private-key escrow · 2FA setup (owner may enable later
as a separate UI action — does not block this mission).
Observations (non-blocking, out of scope)
- World-readable historical DB dumps contain tokens:
/opt/incomex/backups/mysql-pre-pg-migration.sqland/opt/incomex/docker/directus-dump.sql(both 0644). Pre-existing; recommend a future hygiene pass (restrict perms or purge). Not modified here. - The
HOLD_SM_CADENCE_INTEGRITY_FALLBACKfinding above is worth folding into the dieu31 work.
Landmines recorded
docker exec -iinside anssh 'bash -s'heredoc drains the script stdin → usedocker exec(no-i) in bash-fed scripts, or run as a file with</dev/null.- Directus 11.5 admin access = policy-level (
directus_policies.admin_access) reached viadirectus_accessfrom a role; setting a user'sroleto Administrator is sufficient — no per-user access row needed. - Old owner static token authenticated to nothing pre-promotion (user had no perms) yet was still present; a bearer-token denial (401) — not a password login — is the lockout-safe way to prove it dead.
- SM
DIRECTUS_ADMIN_TOKEN(v1, 2026-05-22) holds a rotated-away token (401); two integrity monitors fetch it hourly/6-hourly becausedocker/.envwas neutralised.
PASS_DIRECTUS_DUAL_SUPERADMIN_OWNER_READY → next gate RETURN_TO_PRODUCT_WORK.