Skip to content

sec(data): DORMANT postgres-customers admin lockdown (truehomie root-cause vector)#61

Merged
mastermanas805 merged 2 commits into
masterfrom
fix/postgres-customers-public-admin-lockdown
Jun 6, 2026
Merged

sec(data): DORMANT postgres-customers admin lockdown (truehomie root-cause vector)#61
mastermanas805 merged 2 commits into
masterfrom
fix/postgres-customers-public-admin-lockdown

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

What & why

Hardens the OPEN root cause of the truehomie-db DROP incident (2026-06-03) — a direct, public-internet admin connection to the shared customer Postgres that can DROP DATABASE/DROP ROLE with no audit_log row (the missing-audit-row signature of the incident). Matches docs/ci/DATA-INTEGRITY-DROP-PATH-AUDIT.md §truehomie root-cause hypotheses H1 (the confirmed vector).

DORMANT. NOT applied to prod. Requires USER/OPERATOR review + a maintenance window. Rule 15: infra has no auto-apply; this is high blast radius (shared customer-DB tier).

CONFIRMED via config + SAFE checks (verify-don't-assert, 2026-06-06)

  • pg.instanode.dev152.42.154.144 — the same IP as api/redis/mongo.instanode.dev (the shared DO LoadBalancer fronting ingress-nginx). pg is publicly DNS-routed.
  • nc -z -w5 pg.instanode.dev 5432 succeeds from the public internet — TCP handshake only (no auth, no SQL, no DDL attempted).
  • The postgres-customers pod runs the stock pgvector/pgvector:pg16 image with NO custom pg_hba.conf / postgresql.conf / POSTGRES_HOST_AUTH_METHOD and no config mount → the image default host all all all scram-sha-256 (catch-all) is in effect: the admin role (instant_cust) can authenticate from any source that reaches the listener.
  • postgres-customers Service is ClusterIP; the public exposure is via the external instant-pg-proxy + ingress-nginx tcp-services (not in this repo).
  • A postgres-customers-ingress NetworkPolicy already exists (provisioner/migrator/worker only); its prod-apply state is unverified (no auto-apply).

Still hypothesis (NOT asserted)

  • That an external actor actually authenticated as admin over this path (we did not attempt auth — out of scope). C1–C3 prove the path is open, not that it was used.
  • The instant-pg-proxy's own role-gate — its config lives in the separate InstaNode-dev/instant-pg-proxy repo.

Net: the hardening agent's #1 hypothesis is corroborated, not refuted.

What this changes

  • k8s/data/postgres-customers-lockdown.yaml (new) — custom pg_hba.conf ConfigMap that rejects the admin role (instant_cust/postgres) from the public internet while still allowing in-cluster admin (provisioner/migrator/worker via pod CIDR 10.0.0.0/8) and customer usr_* roles from anywhere (customer connect path preserved). Keyed on the role name so it holds even if pg-proxy SNATs customer traffic to a pod IP.
  • k8s/data/networkpolicy.yaml (edit) — truehomie-context annotations + a dormant (commented) pg-proxy ingress rule (network-layer second line; the admin boundary does NOT depend on it).
  • POSTGRES-CUSTOMERS-LOCKDOWN-RUNBOOK.md (new) — confirmed-vs-hypothesis, the legitimate-consumer map, pre-apply verification (confirm the live admin role name; the pg-proxy SNAT/NetPol interaction; hunt for any public-admin automation), apply (apply CM → patch pod to mount + -c hba_file=... → rollout), legit-access-still-works verify (provisioner admin select 1, real /db/new + customer connect, backup CronJob smoke), external-admin-closed verify (SAFE rejection test — no SQL, no valid creds needed), and rollback.

Legitimate consumers preserved

provisioner (admin DSN, in-cluster), migrator, worker (read), customers (usr_* via pg-proxy), backup CronJob — all preserved (see runbook §2). The one thing closed: a psql -U instant_cust from outside the cluster.

Safety

  • Nothing applies automatically. The pg_hba change is online-reloadable; the pod patch is a brief single-replica restart (provisioner retries, customers reconnect).
  • Biggest break-risk = an undocumented consumer using public admin access; the runbook's pre-apply step (e) hunts for exactly that. If found → migrate it in-cluster before apply.
  • Validated locally: yamllint (repo config) + kubeconform -strict (5/5 valid).

Defense-in-depth context

Infra half of the truehomie fix. App half already shipped: provisioner guardedDrop chokepoint + DDL-audit + CI guard (PR #50); drop-metric alert/tile/catalog (infra PR #60).

🤖 Generated with Claude Code

mastermanas805 and others added 2 commits June 6, 2026 19:52
…cause vector)

Closes the OPEN root cause of the truehomie-db DROP incident (2026-06-03): a
direct public-internet admin connection to the shared customer Postgres that
can DROP DATABASE/ROLE with no audit_log row.

CONFIRMED via config + SAFE checks (verify-don't-assert, 2026-06-06):
  - pg.instanode.dev -> 152.42.154.144 (same DO LB IP as api/redis/mongo); pg
    is publicly DNS-routed.
  - `nc -z pg.instanode.dev 5432` succeeds from the public internet (TCP
    handshake only -- NO auth, NO SQL attempted).
  - postgres-customers pod runs stock pgvector/pgvector:pg16 with NO custom
    pg_hba/postgresql.conf/POSTGRES_HOST_AUTH_METHOD and no config mount ->
    image default `host all all all scram-sha-256` (catch-all): the admin role
    (instant_cust) can authenticate from anywhere that reaches the listener.

STILL HYPOTHESIS (not asserted): that an external actor actually used this to
drop truehomie (we did not attempt auth); the instant-pg-proxy's own role-gate
(its config lives in the separate InstaNode-dev/instant-pg-proxy repo).

Fix (staged DORMANT -- operator-applies in a maintenance window; rule 15: infra
has no auto-apply, high blast radius):
  - k8s/data/postgres-customers-lockdown.yaml: custom pg_hba.conf ConfigMap that
    REJECTS the admin role (instant_cust/postgres) from the public internet while
    still allowing in-cluster admin (provisioner/migrator/worker via pod CIDR)
    AND customer usr_* roles from anywhere (customer connect path preserved).
  - k8s/data/networkpolicy.yaml: truehomie context annotations + a DORMANT
    pg-proxy ingress rule (network-layer second line; admin boundary does not
    depend on it -- pg_hba role-reject holds regardless of SNAT source).
  - POSTGRES-CUSTOMERS-LOCKDOWN-RUNBOOK.md: confirmed-vs-hypothesis, legitimate-
    consumer map, pre-apply verification (live admin role name, pg-proxy SNAT/
    NetPol interaction, hunt for public-admin automation), apply, legit-access-
    still-works verify, external-admin-closed verify (SAFE rejection test), and
    rollback.

NOT applied to prod. Requires USER/OPERATOR review + maintenance window.
Validated locally: yamllint (repo config) + kubeconform -strict (5/5 valid).

Defense-in-depth: this is the infra half. App half already shipped (provisioner
guardedDrop chokepoint + DDL-audit + CI guard, PR #50; drop-metric alert/tile/
catalog, infra PR #60). Audit: docs/ci/DATA-INTEGRITY-DROP-PATH-AUDIT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…roxy-IP reject

Live pre-apply verification (do-nyc3-instant-prod, 2026-06-06) found the PR's
pg_hba was incomplete and would NOT have closed the truehomie vector:

1. TWO superusers on the prod customer pod, not one: `instanode_admin` (the
   role api/worker connect with via CUSTOMER_DATABASE_URL — the CONFIRMED
   truehomie vector role) AND `instant_cust` (provisioner's POSTGRES_CUSTOMERS_URL
   POSTGRES_USER). The PR rejected only `instant_cust`, leaving the actual vector
   (`instanode_admin`) matching the catch-all customer allow. Both now rejected.

2. The pg-proxy SNAT problem: instant-pg-proxy is a normal in-cluster pod that
   re-originates TCP, so an external `-U instanode_admin` arrives SNAT'd to a
   proxy pod IP inside 10.0.0.0/8 — a plain `10.0.0.0/8 allow` would match it.
   Baseline probe confirmed the live vector is OPEN (external admin reaches
   scram). Added proxy-pod-IP `reject` lines (10.109.4.113, 10.109.0.101)
   ordered BEFORE the in-cluster allow so the SNAT'd external admin is rejected
   while legit consumer pods (direct svc DSN, different IPs) still authenticate.
   Documented the churn dependency + the durable pg-proxy role-gate follow-up.

3. Preserved live local/loopback/replication `trust` lines (readiness probe +
   backup pg_dumpall) instead of switching to peer. Added doadmin belt-suspenders.

Runbook: added §3a (pg-proxy SNAT + proxy-IP churn operator note) and the L1-L5
live-confirmed findings table (supersedes H1-H3). NetworkPolicy NOT applied
(verified not enforced in prod; applying as-is would default-deny the proxy path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 78cb667 into master Jun 6, 2026
3 checks passed
mastermanas805 added a commit that referenced this pull request Jun 6, 2026
…+ drill log

Follow-up to the 2026-06-06 apply of PR #61. The lockdown was applied to prod via
`kubectl patch` (imperative). This makes the durable repo manifest match the live
state so a future apply of postgres-customers.yaml does NOT silently revert the
lockdown back to the vulnerable catch-all pg_hba:

- mount the postgres-customers-hba ConfigMap at /etc/postgresql/pg_hba.conf (subPath)
- start postgres with `-c hba_file=... -c password_encryption=scram-sha-256`
- strategy RollingUpdate → Recreate (the RWO PVC deadlocks a rolling update on a
  Multi-Attach error; Recreate terminates the old pod first — brief downtime,
  acceptable for single-replica stateful)

Runbook §9 Drill Log records the apply result: external admin (instanode_admin +
instant_cust) now REJECTED at pg_hba (verified live; baseline reached scram), all
in-cluster admin + customer usr_* paths preserved (verified), no rollback. Lists
the operator follow-ups (durable pg-proxy role-gate; proxy-IP churn refresh;
networkpolicy.yaml apply-exclude).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mastermanas805 added a commit that referenced this pull request Jun 6, 2026
…+ drill log (#63)

Follow-up to the 2026-06-06 apply of PR #61. The lockdown was applied to prod via
`kubectl patch` (imperative). This makes the durable repo manifest match the live
state so a future apply of postgres-customers.yaml does NOT silently revert the
lockdown back to the vulnerable catch-all pg_hba:

- mount the postgres-customers-hba ConfigMap at /etc/postgresql/pg_hba.conf (subPath)
- start postgres with `-c hba_file=... -c password_encryption=scram-sha-256`
- strategy RollingUpdate → Recreate (the RWO PVC deadlocks a rolling update on a
  Multi-Attach error; Recreate terminates the old pod first — brief downtime,
  acceptable for single-replica stateful)

Runbook §9 Drill Log records the apply result: external admin (instanode_admin +
instant_cust) now REJECTED at pg_hba (verified live; baseline reached scram), all
in-cluster admin + customer usr_* paths preserved (verified), no rollback. Lists
the operator follow-ups (durable pg-proxy role-gate; proxy-IP churn refresh;
networkpolicy.yaml apply-exclude).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant