Skip to content

feat(infra): define a staging target and an RLS cutover runbook - #219

Merged
dshakes merged 3 commits into
masterfrom
feat/staging-rls-target
Aug 5, 2026
Merged

dshakes merged 3 commits into
masterfrom
feat/staging-rls-target

Conversation

@dshakes

@dshakes dshakes commented Aug 5, 2026

Copy link
Copy Markdown
Owner

You asked me to turn on RLS in staging. There is no staging. The chart had values.yaml and values-dev.yaml and 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.yaml

LANTERN_RLS_ENFORCE=1 for 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.md

Leads 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_state in 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_app or 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

check result
role lantern_app, rolcanlogin=t, rolsuper=f
grants all 5 tables SELECT+INSERT true
policies 45 tenant_isolation
isolation owner sees 17323 runs across 4 tenants; lantern_app scoped to one sees 17318; 0 from other tenants

Those 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 lint passes.

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.
@github-actions github-actions Bot added the domain:infra Touches infra / CI / deploy / migrations label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

Blocking

  • infra/helm/lantern/values-staging.yaml:77 and infra/helm/lantern/values-staging.yaml:96: the staging RLS env vars are not rendered into either Pod. The control-plane template only emits LISTEN_ADDR, LOG_LEVEL, DB/S3/Redis envs, etc., and never renders LANTERN_ENV, LANTERN_RLS_ENFORCE, or LANTERN_APP_DB_PASSWORD; same for workflow-engine. The secret template also ignores secrets.appDbPassword, so the documented --set secrets.appDbPassword=... is a no-op. Result: deploying this staging values file does not enable RLS at all, while the PR/runbook says it does. See control-plane deployment, workflow-engine deployment, and secrets. The chart needs explicit env + secret wiring or a generic env renderer before this PR is effective.

Non-Blocking

  • docs/runbooks/rls-cutover.md:11: the runbook says both services refuse to start with LANTERN_RLS_ENFORCE=1 and no password, but the control-plane only warns outside production and continues with the privileged pool. Since staging sets LANTERN_ENV: "staging", this claim is false for the exact target environment unless the startup guard is changed or the runbook is softened. See main.go and rlsGuardDecision.

I could not complete helm template because chart dependencies are not vendored in this checkout, but the template source is sufficient to confirm the missing value consumption.

@compass-sdlc-bot

Copy link
Copy Markdown

🔒 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.

@compass-sdlc-bot compass-sdlc-bot Bot added the agent:needs-fix Reviewer found Blocking issues -> triggers the auto-fix loop label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Independent Audit Report: RLS Cutover & Helm Staging Configuration

I have completed a thorough code and architecture audit of the unified diff in pr-diff.patch against the base branch.

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: infra/helm/lantern/templates/control-plane/deployment.yaml, infra/helm/lantern/templates/workflow-engine/deployment.yaml, and infra/helm/lantern/templates/secrets.yaml.

  • The Issue: The new values-staging.yaml file defines LANTERN_RLS_ENFORCE: "1", LANTERN_ENV: "staging", and introduces secrets.appDbPassword: "". However, the Helm chart templates completely ignore these configurations:

    1. The container env blocks in templates/control-plane/deployment.yaml and templates/workflow-engine/deployment.yaml are statically hardcoded and do not loop over or reference .Values.controlPlane.env or .Values.workflowEngine.env. Thus, LANTERN_RLS_ENFORCE and LANTERN_ENV are never injected as environment variables into the containers.
    2. The secret key app-db-password is entirely missing from templates/secrets.yaml, and LANTERN_APP_DB_PASSWORD is not mapped in the container environments.
  • The Security Impact:
    Because LANTERN_ENV is not injected, it defaults to empty, and IsProd() returns false (dev mode). In dev mode, when LANTERN_RLS_ENFORCE is empty, the startup guards (runStartupGuards in main.go) print a warning and continue, silently aliasing the restricted AppPool to the privileged superuser connection pool.

    Staging will deploy successfully, the /healthz probe will report OK, but Row-Level Security will be completely bypassed—rendering the multi-tenant isolation gate inactive without the operator's knowledge.

  • Required Remediation:

    • In templates/secrets.yaml, define the app-db-password key:
      app-db-password: {{ .Values.secrets.appDbPassword | b64enc | quote }}
    • In the service deployments, explicitly map the required environment variables:
      - name: LANTERN_ENV
        value: {{ .Values.controlPlane.env.LANTERN_ENV | quote }}
      - name: LANTERN_RLS_ENFORCE
        value: {{ .Values.controlPlane.env.LANTERN_RLS_ENFORCE | quote }}
      - name: LANTERN_APP_DB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: {{ include "lantern.fullname" . }}-secrets
            key: app-db-password

2. ⚠️ [OPERATIONAL CONCERN] Immediate Startup Crash in Real Production Mode

  • The Issue: Once the environment variable injection is fixed, setting LANTERN_ENV: "staging" and LANTERN_RLS_ENFORCE: "1" will trigger runStartupGuards to evaluate IsProd() == true.
  • The Impact: When IsProd() is true, the server fails closed and crashes on startup if any of the following production-critical variables are missing:
    • LANTERN_GRPC_SERVICE_TOKEN
    • LANTERN_SCHEDULER_GRPC_ADDR
    • LANTERN_RECEIPT_SECRET
    • LANTERN_CREDENTIAL_KEY
      None of these variables are specified or overridden in values-staging.yaml.
  • Remediation: The runbook (rls-cutover.md) should explicitly document that these environment-specific, production-required variables must be injected during staging deployment, otherwise the pods will crash immediately upon start.

3. ℹ️ [CORRECTNESS] Dead Configurations in values-staging.yaml

  • The Issue: values-staging.yaml sets LOG_LEVEL: "info" under controlPlane.env and workflowEngine.env.
  • The Impact: These values are completely ignored. In the deployment manifests, LOG_LEVEL is retrieved via a configMapKeyRef pointing to LOG_LEVEL in the ConfigMap, which is globally governed by .Values.global.logLevel. Specifying them per-service in values-staging.yaml is dead configuration.

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.
@compass-sdlc-bot compass-sdlc-bot Bot added agent:needs-fix Reviewer found Blocking issues -> triggers the auto-fix loop and removed agent:needs-fix Reviewer found Blocking issues -> triggers the auto-fix loop labels Aug 5, 2026
@compass-sdlc-bot

Copy link
Copy Markdown

🔒 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.
@compass-sdlc-bot compass-sdlc-bot Bot added agent:reviewed-clean Reviewer found no Blocking issues this round and removed agent:needs-fix Reviewer found Blocking issues -> triggers the auto-fix loop labels Aug 5, 2026
@dshakes
dshakes merged commit b185244 into master Aug 5, 2026
16 checks passed
@dshakes
dshakes deleted the feat/staging-rls-target branch August 5, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:reviewed-clean Reviewer found no Blocking issues this round domain:infra Touches infra / CI / deploy / migrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant