Fail fast on insecure configuration in production #14
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
CIRCA_SECRET_KEYdefaults to the literal stringchange-me-in-production, which isdocumented in the README. That default signs the session cookies. If the app is ever
deployed without overriding it, every session cookie is forgeable by anyone who has
read the README — and nothing in the app would report a problem.
Scope
Fail-fast validation of security-critical configuration at startup, gated on
environment.
Implementation notes
CIRCA_ENV=development|production).CIRCA_SECRET_KEYis unset, still the default, or below a minimum entropy/lengthCIRCA_OAUTH_REDIRECT_URIis not HTTPSwarn and continue — a warning in a log nobody reads is how this default reaches production.
but log a single clear notice that insecure defaults are in use.
HttpOnly,Securein production,SameSite, and a sensible expiry.Done when
References
README.mdenv var table (CIRCA_SECRET_KEYdefaultchange-me-in-production)backend/app/auth/session.py, backend config moduleAGENTS.md: "Security is very important when building the project, and should be a priority"Amended by the audit of 2026-07-28.
Two things this issue does not currently capture:
The amplifier that makes the default key directly exploitable (#57). The session cookie
contains only a user UUID, and the API hands out other users' UUIDs via
created_byonevidence, decisions, comments, and jobs. So forging an admin session is: authenticate → read
decisions → collect the admin UUID → sign a cookie with the README-published default key. No
cryptanalysis. Without that disclosure the attacker would need to guess a UUIDv4.
The environment default points the wrong way.
CIRCA_ENVIRONMENTdefaults todevelopment, and no.envexists in the repo — so an operator who sets only the OAuth varsgets
Secure=Falsecookies, credentialed CORS forlocalhost:5173, andecho=True, whichlogs every SQL statement including comment bodies and notes. Recommend defaulting to
productionso the failure mode is safe, and gating SQL echo on its own setting rather than onenvironment.
Done in
f61226a.backend/tests/test_startup_config.py, 27 tests.The decision worth flagging
The issue body says "keep the current permissive behaviour so local setup stays easy"; the audit comment says the environment default points the wrong way.
CIRCA_ENVIRONMENTnow defaults toproduction— I asked, and that was the call.The reasoning is the audit's: there is no
.envin the repo, so an operator who set only the OAuth variables previously gotSecure=Falsecookies, credentialed CORS forlocalhost:5173, SQL echo printing note and comment bodies to the log, interactive docs published to anyone who could reach the host, and session cookies signed with the key printed in the README. Nothing anywhere reported a problem. Failing closed costs one line in a local.env, and the refusal message names it.It is also a
Literalnow. As a free-formstr,CIRCA_ENVIRONMENT=prodwould have compared unequal to"development", satisfied everyis_developmentcheck, and then sailed past a production guard keyed on the exact word — permissive and unvalidated at once. It is now a startup error.What production refuses to start on
CIRCA_SECRET_KEYunset, still the documented default, or under 32 charactersCIRCA_OAUTH_CLIENT_ID/CIRCA_OAUTH_CLIENT_SECRETunsetresolved_server_metadata_urlproperty the flow uses, so the check cannot pass while the flow fails (previously a rawValueErrorfromregister_providerhalf way through building the app)CIRCA_OAUTH_REDIRECT_URInot HTTPS — the authorization code is delivered there, and a code is a sessionCIRCA_DEV_LOGIN_ENABLEDon (#15's guard, in place before #15's route)Two properties of the refusal are load-bearing and tested as such:
It raises, it does not warn. A warning is precisely what the default key would have produced: a line in a log nobody reads, on a deployment that keeps serving. There is no state of this application worth having where it runs with a signing key published in its own README.
It reports every problem at once. An operator who fixes one variable, restarts, and meets the next refusal three times is an operator who gives up half-configured — which is the state this issue exists to prevent. The message names each variable, includes the
secrets.token_urlsafecommand, and namesCIRCA_ENVIRONMENT=developmentas the way to run locally, because a refusal that does not say that teaches people to disable the check instead of configuring the deployment.The audit's second point, taken
SQL echo moved to its own
CIRCA_SQLITE_ECHO, off by default, rather than being implied byenvironment == "development". The echoed statements include note and comment bodies — what a relative said about a photograph — and "am I in development" is not the same question as "may this go in a log file."Development
Unchanged and permissive: every value that fails in production passes there. It now logs one line naming the concessions in play (default key, no
Secure, credentialed CORS, the bypass, SQL echo), because silently permissive is how the default reached a deployment in the first place.Cookie flags
Reviewed as asked and now pinned per environment:
HttpOnlyandSameSite=strictalways,Secureoutside development,Max-Agepresent — and the deletion mirrors the attributes, because an asymmetry there invites someone to add apathto one side only and quietly break logout.Done when
README env table and
.env.exampleupdated; the default is now presented as development-only. 992 passed, 8 skipped; ruff clean.One knock-on: CI's backend job now sets
CIRCA_ENVIRONMENT=development, because importingapp.mainto export a schema or run migrations is not a deployment.