Skip to content

chore: stop the migrate job printing the Neon endpoint, and write down why - #4

Merged
kilianmc merged 1 commit into
devfrom
chore/redact-migrate-log
Aug 13, 2026
Merged

kilianmc merged 1 commit into
devfrom
chore/redact-migrate-log

Conversation

@kilianmc

Copy link
Copy Markdown
Owner

Follow-up to #3, from actually running the thing.

The workflow works

First real dispatch — read-only current, --ref dev, environment=dev — resolved the dev environment's secrets, connected to the dev Neon branch and reported Rev: 0002 (head), with history, upgrade and seed all correctly skipped. So PR #2's migration is now confirmed applied to dev by the workflow, not by a laptop.

And it leaked

alembic current --verbose prints a header before the revision:

Current revision(s) for ***ep-flat-...eu-central-1.aws.neon.tech/neondb?***&sslmode=require:

Alembic obscures the password and GitHub masked the secret, but the Neon endpoint hostname, region and database name reached a public Actions log — breaking the rule written at the top of that very file, via a flag rather than an echo. My error: I specified --verbose.

Impact: reconnaissance only. No credential was exposed, Neon still requires them, and there is no IP allowlist to bypass on the free tier. The run has been deleted, so the hostname is out of the public history.

The fix

Both current steps run bare. alembic current prints 0002 (head), which is the whole point of the audit trail. alembic history --verbose keeps its flag — it reads the migration files and never opens a connection, so there is no URL for it to print.

Also audited the rest of the path rather than assuming: sqlalchemy.engine is pinned to WARNING in alembic.ini, and migrations/env.py never prints a URL. --verbose was the only source.

CLAUDE.md — the two traps that made this workflow inert for a day

  1. workflow_dispatch only registers from the DEFAULT branch. On dev alone it was completely invisible — a 404 from gh, and not in the Actions UI either.
  2. environment chooses the DATABASE; the REF chooses the MIGRATIONS. Independent inputs, and mixing them up is how production gets a revision nobody reviewed.

Plus the constraint that falls out of #3: main's copy must stay byte-identical to dev's, because the merge base predates the file and any difference is an add/add conflict at the first promotion. Hence the companion one-file PR against main — and hence notes going in CLAUDE.md rather than in a comment that would need duplicating.

The security verification pass (Kilian's request)

A dated, tiered checklist, deliberately structured so it cannot become theatre:

  • Tier 1 — already proven by CI on every push, do NOT re-test by hand. Naming these explicitly is the point: re-checking them manually is how a security pass turns into busywork.
  • Tier 2 — only the real deployment can show it. Routing contract, docs-off, CORS preflight from an unknown origin, cookie attributes in devtools, deny-by-default through the rewrite, IDOR with two real accounts (the highest-value item — it is the actual extraction risk), demo writes against a live endpoint, the login limit, response headers including whether frame-ancestors breaks the federated mount, and no non-VITE_ value in the built bundle.
  • Tier 3 — infra outside this repo, where no CI test can ever notice a control going missing. The WAF rule on /api/auth/* above all, plus framework: null, preview SSO, Dependabot alerts, 2FA, and whether Neon's CU-hours match the model.

It also says which items to script and which to hand to Kilian, and to write the outcome down with a date so the next pass verifies rather than re-verifies.

Not in this PR

The 6 open Dependabot alerts (5 starlette, 1 pytest). I checked each against our code: no request.form(), no StaticFiles, no HTTPEndpoint, and nothing in server/ reads request.url.path, url.hostname or the Host header — enforce_auth uses request.scope["route"].path, the matched route template. None is exploitable here, including the one advertised as bypassing path-based security checks. Fixing them anyway means FastAPI 0.128.8 → 0.141.1 and starlette 0.52.1 → 1.6.0 (FastAPI 0.128.8 pins starlette<1.0.0), which is a 0.x→1.x major jump deserving its own PR and its own review.

Risk

Docs and one CI-only file. No app code, no dependency change, no version bump.

…n why

The Migrate workflow's first real run (read-only `current` against dev) worked,
and leaked. `alembic current --verbose` emits a header:

    Current revision(s) for ***ep-flat-...eu-central-1.aws.neon.tech/neondb?***:

Alembic obscures the password and GitHub masked the secret, but the Neon
endpoint hostname, region and database name reached a PUBLIC Actions log —
breaking the rule stated at the top of that very file, via a flag rather than
an `echo`. Credentials were not exposed and the run has been deleted; the
residual risk was reconnaissance only, since Neon still requires credentials
and the free tier has no IP allowlist to bypass.

Both `current` steps now run bare. `alembic current` prints `0002 (head)`,
which is all the audit trail needs. `alembic history --verbose` keeps its flag:
it reads the migration files and never opens a connection, so it has no URL to
print. Also verified nothing else on that path logs one — `sqlalchemy.engine`
is pinned to WARNING in alembic.ini and migrations/env.py never prints it.

CLAUDE.md gains the two traps that made this workflow inert for a day:
`workflow_dispatch` only registers from the DEFAULT branch, and `environment`
selects the database while the REF selects the migrations. Plus the rule that
main's copy must stay byte-identical to dev's, because the merge base predates
the file and any difference is an add/add conflict at the first promotion.

It also gains the end-to-end security verification pass Kilian asked for: a
dated, tiered checklist to run once the product is feature-complete and before
it is shown to anyone. Tier 1 is what CI already proves and must NOT be
re-tested by hand; tier 2 is what only the real deployment can show (routing,
docs-off, CORS, cookie attributes, IDOR with two real accounts, demo writes,
rate limits, headers, bundle secrets); tier 3 is the infra that lives outside
this repo, where no test in CI can ever notice a control going missing — the
WAF rule above all.
@vercel

vercel Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
climb-trainer Ready Ready Preview Aug 13, 2026 6:31pm

This branch was successfully deployed

1 active deployment
Preview — 242aaf00 Deployed Aug 13, 2026 by vercel[bot]
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