Repository navigation
feat(infra): define a staging target and an RLS cutover runbook - #219
Conversation
RLS shipped for the control-plane and the workflow engine and then sat switched off everywhere, because there was nowhere to switch it on: the chart had values.yaml and values-dev.yaml and nothing between them. Asked to enable it in staging, I found staging does not exist. Merged-and-unenabled is not done, so this defines the target rather than pretending the work is finished. values-staging.yaml sets LANTERN_RLS_ENFORCE=1 for both services, 2 replicas, info logging, and takes the lantern_app password at deploy time. The password is NOT in the file and must not be the superuser's — a superuser BYPASSES RLS, so pointing the app pool at it leaves policies inert while every dashboard reports enforcement on. That is the specific false-green this whole change exists to avoid. The runbook leads with ordering, because the failure mode is not gradual: with the flag on and no usable credential, both services refuse to start. That is deliberate — a service that believes it is enforcing and is not is worse than one that is plainly down — but it means the wrong order takes an environment offline rather than degrading it. It also covers the part that actually bites: grants. Policies restrict ROWS; grants decide whether the role may touch the TABLE at all. A tenant-scoped transaction in the engine writes runs + journal_events + run_locks + step_state in ONE transaction, so one missing grant fails ordinary work with a permission error rather than a row-level denial. Every command was run against a real database rather than written from memory: the role check, the grant matrix, the policy count (45), and the isolation proof. On a 4-tenant database the owner sees 17323 runs, lantern_app scoped to one tenant sees 17318, and 0 rows from other tenants — the 5 hidden rows are what make the test non-vacuous, so the runbook now tells operators to compare the two totals and warns that equal numbers mean the check proved nothing. Nothing is enabled by this change. helm lint passes.
🔎 Codex cross-audit (agent:audit)Blocking
Non-Blocking
I could not complete |
|
🔒 Reviewer verdict: BLOCKING — values-staging.yaml sets LANTERN_RLS_ENFORCE/LANTERN_APP_DB_PASSWORD/LANTERN_ENV that the Helm deployment templates never actually consume, so the staging RLS cutover is a silent no-op. |
🔷 Gemini cross-audit (agent:audit-gemini)Independent Audit Report: RLS Cutover & Helm Staging ConfigurationI have completed a thorough code and architecture audit of the unified diff in Below are the findings, led by a Blocking security risk that results in a silent, fail-open tenant-isolation bypass in staging. 1. 🛑 [BLOCKING / CRITICAL] Silent RLS Enforcement Failure (Silent Fail-Open)Location:
2.
|
The reviewer caught this and it was the whole point of the change, inverted.
values-staging.yaml declared LANTERN_RLS_ENFORCE=1, LANTERN_ENV=staging and
secrets.appDbPassword. None of them reached a container. The deployment
templates hardcode their env blocks — the control-plane referenced exactly one
key, `.Values.controlPlane.env.LISTEN_ADDR` — and secrets.yaml had no
app-db-password entry at all. Everything else in that values file was
decoration.
So a staging deploy would have come up clean, passed its probes, reported
healthy, and enforced NOTHING. Worse than not shipping it: a values file that
reads as "RLS is on here" while the control is off is exactly the fail-open this
change exists to prevent, and it would have been believed.
`helm lint` passed the whole time. It validates that a chart is well-formed, not
that a value survives to a container — the same shape of check-measuring-the-
wrong-thing that keeps turning up.
Both deployments now inject LANTERN_ENV and LANTERN_RLS_ENFORCE unconditionally
(defaulting to empty, so no behaviour change for values files that omit them),
and LANTERN_APP_DB_PASSWORD from the new secret key.
Verified by RENDERING the manifests, not by linting:
before: LANTERN_RLS_ENFORCE 0 occurrences, LANTERN_APP_DB_PASSWORD 0,
app-db-password 0
after: LANTERN_RLS_ENFORCE "1" x2 (control-plane + workflow-engine),
LANTERN_ENV "staging" x2, and the secret decoding to the supplied
value rather than empty
Still enables nothing on its own — staging must be deployed with the password
for any of it to take effect.
|
🔒 Reviewer verdict: BLOCKING — values-staging.yaml sets LANTERN_ENV=staging, which flips on fail-closed startup guards (JWT_SECRET, LANTERN_RECEIPT_SECRET, LANTERN_CREDENTIAL_KEY, LANTERN_GRPC_SERVICE_TOKEN) that are never wired into the Helm chart, so control-plane/workflow-engine will crash-loop regardless of the RLS setup. |
Second blocking finding on this PR, and the deeper one. LANTERN_ENV=staging is not a label — it is prod-like, so it ARMS the control-plane's fail-closed startup guards. Setting it demanded JWT_SECRET, LANTERN_CREDENTIAL_KEY, LANTERN_RECEIPT_SECRET and LANTERN_GRPC_SERVICE_TOKEN, and three of those had ZERO references anywhere in the chart. The chart could not supply them at any price. A staging deploy would have Fatal'd at boot on a secret that looks nothing to do with RLS. I should have caught this: I documented several of those guards myself, earlier today, in this repo's own CLAUDE.md. Writing a guard down is not the same as remembering it applies to you. The chart now carries credential-key, receipt-secret and grpc-service-token (alongside the app-db-password added earlier) and injects all of them into both services — unconditionally and empty by default, so dev and kind deployments are untouched. The runbook lists the full required set with a single copy-pasteable helm command, because supplying only the RLS password fails on something that reads as unrelated. Verified by RENDERING with every secret set, not by linting: LANTERN_ENV, LANTERN_RLS_ENFORCE, LANTERN_APP_DB_PASSWORD, LANTERN_CREDENTIAL_KEY, LANTERN_RECEIPT_SECRET, LANTERN_GRPC_SERVICE_TOKEN — 2 occurrences each (control-plane + workflow-engine), and all four secrets decoding to their supplied values rather than empty. helm lint passes, but it passed when the values were inert too; the render is the check that means anything here.
You asked me to turn on RLS in staging. There is no staging. The chart had
values.yamlandvalues-dev.yamland nothing between them, and the only cluster context is an unrelated employer EKS. So RLS shipped for the control-plane and the workflow engine and then sat switched off everywhere, because there was nowhere to switch it on.Merged-and-unenabled is not done. This defines the target instead of pretending otherwise. It enables nothing.
values-staging.yamlLANTERN_RLS_ENFORCE=1for both services, 2 replicas, info logging, password supplied at deploy time.The password is not in the file and must not be the superuser's — a superuser bypasses RLS, so pointing the app pool at it leaves policies inert while every dashboard reports enforcement on. That is precisely the false-green this work exists to prevent.
docs/runbooks/rls-cutover.mdLeads with ordering, because the failure mode isn't gradual: flag on + no usable credential = both services refuse to start. Deliberate (a service that believes it's enforcing and isn't is worse than one plainly down), but it means the wrong order takes an environment offline rather than degrading it.
It covers the step that actually bites — grants. Policies restrict rows; grants decide whether the role may touch the table. A tenant-scoped transaction in the engine writes
runs+journal_events+run_locks+step_statein one transaction, so one missing grant fails ordinary work with a permission error, not a row-level denial.Also documents the two roles and why neither alone is sufficient: tenant work must use
lantern_appor policies don't apply; the scheduler's cross-tenant poll must not, because a restricted connection with no GUC matches no rows and the engine would quietly stop finding work.Every command was run, not written from memory
lantern_app,rolcanlogin=t,rolsuper=ftruetenant_isolationlantern_appscoped to one sees 17318; 0 from other tenantsThose 5 hidden rows are what make the proof non-vacuous — so the runbook tells operators to compare the two totals and warns that equal numbers mean the check proved nothing (single-tenant database, or enforcement not actually on).
helm lintpasses.