[Hardening] Reject placeholder and weak secrets at startup #106
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
The backend accepts whatever secrets it is given — there is no strength or placeholder check anywhere:
webapp/backend/app/config.py:28—secret_key: str(required but unvalidated). This value both signs sessions and derives the AES-GCM key that encrypts all stored secrets (webapp/backend/app/crypto.py:35-46,_derive_key), so a guessableSECRET_KEYexposes every credential in the settings table.webapp/backend/app/config.py:53—bot_api_key: str = ""(defaults to empty; an empty shared key weakens backend↔bot auth).database_url/database_migrate_url(config.py:31-33); there is no standalone password field to validate..env.exampleships guessable placeholders that a self-hoster can miss::24SECRET_KEY=replace-with-openssl-rand-hex-32:35POSTGRES_PASSWORD=changeme:38POSTGRES_MIGRATE_PASSWORD=changeme_migrate:41/:45— the same passwords appear insideDATABASE_URL/DATABASE_MIGRATE_URLMotivation
A deployment that copies
.env.exampleand misses one value boots successfully with guessable credentials. Failing fast at startup with an actionable message is the cheapest possible protection for self-hosters.Fix / Spec
Add pydantic
field_validators inwebapp/backend/app/config.py:secret_key: reject the exact.env.exampleplaceholder (replace-with-openssl-rand-hex-32) and enforcelen >= 32.database_url/database_migrate_url: reject when the parsed password component ischangemeorchangeme_migrate(parse withurllib.parse/SQLAlchemy URL, don't substring-match the whole URL).bot_api_key: reject the empty string is not desired if the bot is optional — instead reject only known placeholder values (e.g.changeme) and values shorter than 16 chars when set. (If the codebase treats empty as "bot not configured", keep that semantic — verify howrequire_bot_authhandles an empty key and close any bypass there.)SECRET_KEY is still the .env.example placeholder. Generate one with: openssl rand -hex 32Acceptance criteria
.env.exampleplaceholder secret exits immediately with a message naming the env var and theopenssl rand -hex 32generation command.SECRET_KEYshorter than 32 chars is rejected.changeme/changeme_migrateis rejected.References
webapp/backend/app/config.py:28(secret_key),:31-33(DB URLs),:53(bot_api_key)webapp/backend/app/crypto.py:35-46(_derive_keyfrom SECRET_KEY).env.example:24,:35,:38,:41,:45webapp/backend/app/auth/dependencies.py(require_bot_auth— empty-key behavior)Filed from the July 2026 full-project review.
Picking this up as part of a v3.3.0 push. Landing on branch
hardening/backendtogether with #88, #90, #97, and #109 (grouped by component to keep the diffs reviewable).Note for implementation: CI (
.forgejo/workflows/ci.yml) currently setsSECRET_KEY: ci-secret-not-used-in-tests— 27 chars, which the newlen >= 32rule rejects. CI env has to move to a compliant dummy in the same commit or the build breaks.Fixed on
main(commitf25ee12, merged via1c9c19f). Pydanticfield_validators inconfig.py:secret_keyrejects the exact.env.exampleplaceholder and enforceslen >= 32;database_url/database_migrate_urlrejectchangeme/changeme_migratematched against the parsed password (via SQLAlchemymake_url, not a substring — a DB whose name ischangemeis accepted);bot_api_keyallows empty (see below) but rejects known placeholders and<16chars when set. Each message names the env var and givesopenssl rand -hex 32. CI'sSECRET_KEY(was 27 chars) updated to a compliant dummy in the same commit.require_bot_authempty-key finding — not a bypass, it is fail-closed.get_bot_api_keyreturnsNonefor an empty key, and the dependency doesif not stored or not compare_digest(...) → 401, so every bot request is rejected when no key is configured. Empty genuinely means "bot not configured", so it's left permitted; added a regression test asserting the 401. Tests (12) cover accept + reject per validator. Backend suite green (358 passed), ruff clean.