Skip to content

feat(metrics): require a bearer token on /metrics when configured#177

Merged
rubenhensen merged 3 commits into
mainfrom
feat/metrics-auth
Jul 14, 2026
Merged

feat(metrics): require a bearer token on /metrics when configured#177
rubenhensen merged 3 commits into
mainfrom
feat/metrics-auth

Conversation

@rubenhensen

Copy link
Copy Markdown
Contributor

What

Protect the Prometheus /metrics endpoint with an optional bearer token, enforced in the app itself.

  • New metrics_token config key (also settable via ROCKET_METRICS_TOKEN).
  • When set/metrics requires Authorization: Bearer <token>, compared in constant time (subtle, reusing the renamed constant_time_eq helper). Missing/wrong → 401.
  • When unset → endpoint stays open (unchanged behaviour) and a warning is logged at startup. This keeps the rollout non-breaking: deploy → set the token on each instance → point Prometheus at it, with no downtime window.
  • /health stays public.

Why

/metrics is currently reachable unauthenticated on the public internet (e.g. https://storage.staging.postguard.eu/metrics). It's only aggregate operational counters — no PII/secrets — so low severity, but it leaks upload volume, storage, and client mix, and is a recon aid. Restricting at the ingress isn't viable because cryptify also runs on an external host we can't firewall, so the auth has to live in the app and travel with it.

Enabling it

Set a token per instance and configure Prometheus to send it:

# config.toml — prefer the env var for the secret
metrics_token = ""            # or: ROCKET_METRICS_TOKEN=…
# prometheus scrape_config
authorization:
  type: Bearer
  credentials: ""

Notes

Covered by unit tests: metrics_requires_bearer_when_token_configured (no/wrong → 401, correct → 200) and metrics_open_when_token_unset. cargo test: 127 passing.

Add an optional `metrics_token` config (also settable via ROCKET_METRICS_TOKEN).
When set, /metrics requires `Authorization: Bearer <token>` (constant-time
compared via subtle); when unset it stays open and logs a startup warning, so
rolling this out doesn't break existing scrapers. The auth lives in the app,
not the ingress, so it travels with cryptify to external hosts that can't be
firewalled. /health stays public.
@dobby-coder

dobby-coder Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

On it, @rubenhensen! I'm reviewing your PR adding bearer token auth to the metrics endpoint and will post a consolidated review shortly.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Reviewed PR #177 (optional bearer-token auth on /metrics).

Code review: Logic is correct — the MetricsAuth request guard reads CryptifyConfig from managed state (prod wires it via AdHoc::config, so no silent bypass), unset token leaves the endpoint open with a startup warning, set token enforces a constant-time bearer comparison via the renamed constant_time_eq helper (rename applied consistently to the recovery-token path and its test). Build clean; full suite 127/127 incl. both new metrics-auth tests; cargo fmt --check and clippy -D warnings both clean.

Rule compliance: Per-rule check across all Dobby rules + cryptify repo notes — no violations. Conventional-commit PR title checks out, tests added, figment test-harness pattern + #[rocket::async_test] convention followed, no .github/workflows touched.

One minor, non-blocking nit inline (empty-string token half-state). Good to merge.

Comment thread src/main.rs
.extract::<CryptifyConfig>()
.expect("Missing configuration");

if config.metrics_token().is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking, misconfiguration-only): this startup warning only fires when metrics_token().is_none(). Setting metrics_token = "" (empty string) yields Some(""), so no warning is logged, yet /metrics is then gated behind an empty bearer token — a confusing, trivially-bypassable half-state. Consider treating an empty/blank token as unset (warn + leave open) or rejecting it at startup. Not blocking.

@dobby-coder

dobby-coder Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Approved! ✅ All 127 tests pass, and are clean, and the implementation looks correct.

One non-blocking nit worth a follow-up when convenient: if is set to an empty string in config, it resolves to — which silently skips the startup warning but still gates behind a trivially-bypassable empty token. Worth either treating blank as unset or rejecting it at startup to avoid surprising behaviour.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

@dobby-coder

dobby-coder Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

On it, @rubenhensen! I'll do the find-and-replace across the codebase and open a draft PR with the changes for review.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

/dobby fix conflicts

@dobby-coder

dobby-coder Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

On it, @rubenhensen! I'm picking up the merge conflicts on #177 and will resolve them, then open a draft PR with the fix and run a review loop before marking it ready.

# Conflicts:
#	conf/config.toml
#	src/main.rs

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Consolidated post-PR review of #177 (optional bearer-token auth on /metrics).

Code review (Review Dobby 2): No findings — build clean, full suite 127/127 incl. the two new metrics-auth tests, cargo fmt --check and clippy -D warnings clean. Merge 104926d resolved both conflicts correctly (kept main's pkg_url=https://pkg.postguard.eu/ with trim_end_matches, kept the metrics_token startup warning); no conflict markers. The MetricsAuth guard reads CryptifyConfig via managed state (AdHoc::config in prod, .manage in tests), constant-time compares, returns 401 on missing/wrong token, and stays open-with-warning when unset.

Rule compliance: Per-rule check across all Dobby rules + cryptify repo notes (figment test-harness pattern, repo notes, x-postguard-header) — no violations. Conventional-commit title (feat(metrics): ...) passes, tests added and follow the #[rocket::async_test] + figment harness convention, no .github/workflows touched, security hardening developed openly (adds protection, discloses no unpatched vuln).

No blocking issues. Good to merge.

@dobby-coder

dobby-coder Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Reviewed and approved! ✅ Zero findings across code review and a full rules compliance check (127/127 tests passing, build + fmt + clippy all clean). The PR looks good to merge.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

/dobby fix conflicts

@dobby-coder

dobby-coder Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Merged latest main into feat/metrics-auth and resolved the conflict in src/config.rs. The conflicts were purely additive — metrics_token (this PR) and usage_db (from #176 on main) touch adjacent lines but are independent, so both fields, their From mapping, accessor methods, and for_test defaults are kept side by side.

Verified after merge: cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test --all-targets (146 passing, incl. metrics_requires_bearer_when_token_configured, metrics_open_when_token_unset, and the usage_db config tests). PR is now mergeable.

@rubenhensen
rubenhensen merged commit ccc638f into main Jul 14, 2026
7 checks passed
@rubenhensen
rubenhensen deleted the feat/metrics-auth branch July 14, 2026 11:43

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated post-PR gate on #177 (optional bearer-token auth on /metrics, after /dobby fix conflicts) — requesting two minor, PR-introduced doc/changelog fixes. No bugs; correctness, build, fmt, and tests are all green.

Rule check (per-rule Haiku sweep): all rules compliant.

  • cargo fmt --all -- --check clean; conflict resolution is not a no-op (141 insertions across 4 files vs origin/main); PR title is conventional (feat(metrics): …); tests present (2 new metrics tests); no public security-disclosure leak in the diff/comments; figment test-harness and cryptify architecture notes respected.
  • Re-verified on PR HEAD 21b9871: both new tests pass (metrics_open_when_token_unset, metrics_requires_bearer_when_token_configured).

Code review (Review Dobby 2): logic correct — the MetricsAuth guard reads CryptifyConfig from managed state (prod wires it via AdHoc::config), so /metrics auth is genuinely enforced in production, not silently open; constant-time token compare preserved. 4 minor findings, no bugs.

Please fix (PR-introduced):

  1. CHANGELOG — no [Unreleased] entry for this feature. The repo follows Keep a Changelog and sibling #183 added entries; add one (e.g. under ### Added or ### Security) describing the optional /metrics bearer token.
  2. Stale doc cross-reference — the rename recovery_tokens_matchconstant_time_eq left a dangling reference in the doc comment on cryptify_tokens_match (src/main.rs:617). See inline comment.

Non-blocking nits (inline): case-insensitive Bearer scheme match (RFC 7235); constant_time_eq now duplicates the pre-existing cryptify_tokens_match helper.

Comment thread src/main.rs
.headers()
.get_one("Authorization")
.and_then(|h| {
h.strip_prefix("Bearer ")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking): the Authorization scheme match only accepts Bearer /bearer . Per RFC 7235 auth schemes are case-insensitive, so e.g. BEARER is rejected. Harmless in practice (Prometheus sends Bearer), but a case-insensitive scheme check would be more robust.

Comment thread src/main.rs
/// independent of where the bytes start to differ — defeats timing oracles
/// on secret comparisons. Used for the recovery token and the `/metrics`
/// bearer token. A length difference returns early (lengths aren't secret).
fn constant_time_eq(presented: &str, expected: &str) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two items from this rename:

  1. Stale cross-reference (please fix): renaming recovery_tokens_matchconstant_time_eq left a dangling reference — the doc comment on cryptify_tokens_match at src/main.rs:617 still reads "mirroring recovery_tokens_match". Update it to constant_time_eq.
  2. Duplicate helper (non-blocking): constant_time_eq is now a near-verbatim duplicate of cryptify_tokens_match (src/main.rs:620) — both do early length-return + subtle::ConstantTimeEq. Consider collapsing into the single generalized helper. Low priority; the duplicate predates this PR, but the rename makes it redundant.

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