Skip to content

Mask the SMTP username, and name public tokens PUBLIC - #156

Merged
davegaeddert merged 1 commit into
masterfrom
settings-secrets
Sep 21, 2026
Merged

davegaeddert merged 1 commit into
masterfrom
settings-secrets

Conversation

@davegaeddert

Copy link
Copy Markdown
Member

Plain masks a setting only when its annotation is Secret[...]. EMAIL_HOST_PASSWORD was; EMAIL_HOST_USER wasn't. With Postmark, Mailgun and SES the SMTP username is the API token, so the startup "Settings from env" dump printed a live credential into a production log. Found tonight while upgrading an app.

  • EMAIL_HOST_USER: Secret[str]. Secret is annotation-only, so the SMTP backend still receives the real value; only the display changes.
  • CONNECT_PAGEVIEWS_TOKEN → CONNECT_PAGEVIEWS_PUBLIC_TOKEN (env PLAIN_CONNECT_PAGEVIEWS_PUBLIC_TOKEN), still a plain str. It is a public endpoint token that {% connect_pageviews %} renders into every page's HTML, so masking it would be false security. The name now says that.
  • A contract test pins the convention repo-wide: a setting whose last underscore-separated segment is TOKEN/KEY/SECRET/PASSWORD/USER/USERNAME/DSN/CREDENTIAL is either Secret[...] or carries PUBLIC in its name. Last segment, because the trailing noun is what says what a value is, which is why AUTH_USER_SESSION_HASH_FIELD, PASSWORD_HASHERS, OAUTH_SERVER_ACCESS_TOKEN_EXPIRY and EMAIL_SSL_KEYFILE need no exception. There is no allowlist: the escape hatch is spelled in the setting's own name.

The test AST-parses every default_settings.py off disk as well as walking the registered settings, because the test app installs only one package and would never have seen EMAIL_HOST_USER. Reverting the annotation makes it fail with the file and line.

Audited all 126 settings across 18 modules by what the value is and who reads it. Everything else that carries a credential was already Secret, including POSTGRES_URL and POSTGRES_MANAGEMENT_URL, which are documented with user:password@. No other leak.

Two things found and deliberately not changed: plain settings get <NAME> prints the raw value with no masking, which is arguably an explicit reveal but is one command from a CI log; and if a future URL setting can carry a password, redacting userinfo inside display_value() would read better than masking the whole URL, but it would change rendering for every URL-valued setting.

Not in this repo: dropseed/plain-public documents the old setting name in app/templates/apps/connect.html.

Verified: ./scripts/fix clean; plain 839 passed, plain-email 13, plain-connect 25; ./scripts/type-check plain and ./scripts/type-validate clean.

EMAIL_HOST_USER was a plain str, so the startup "Settings from env" dump
and `plain settings list` printed it in full. With Postmark, Mailgun, and
SES the SMTP username is the API token itself, so that leaks a live
credential into production logs. Secret[T] is an annotation marker only --
the runtime value and the SMTP backend are unchanged.

Rename CONNECT_PAGEVIEWS_TOKEN to CONNECT_PAGEVIEWS_PUBLIC_TOKEN. It is
genuinely public (the {% connect_pageviews %} tag renders it into page
HTML), so masking it would be false security; saying PUBLIC in the name
makes that visible at a glance instead.

Add a contract test pinning the convention: a setting whose last name
segment is TOKEN/KEY/SECRET/PASSWORD/USER is either Secret[...] or says
PUBLIC in its name. It checks both the live settings registry and every
package's default_settings.py on disk, so a package the test app does not
install is still covered.
@pullapprove5

pullapprove5 Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor
PASS: 1 review scope passed
Scope Progress
✅ code 1/1

View in PullApprove

Next steps:

@davegaeddert
davegaeddert merged commit e7e5e7b into master Sep 21, 2026
8 checks passed
@davegaeddert
davegaeddert deleted the settings-secrets branch September 21, 2026 22:45
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