Skip to content

Guard admin endpoints behind auth by default#443

Open
ChristianPavilonis wants to merge 3 commits intomainfrom
feature/guard-admin-endpoints
Open

Guard admin endpoints behind auth by default#443
ChristianPavilonis wants to merge 3 commits intomainfrom
feature/guard-admin-endpoints

Conversation

@ChristianPavilonis
Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis commented Mar 5, 2026

Summary

  • Fix security vulnerability where /admin/keys/rotate and /admin/keys/deactivate were publicly accessible when no handler regex covered /admin/* paths
  • Settings::from_toml_and_env() now returns a hard error when no handler covers admin endpoints, catching misconfiguration at build time
  • enforce_basic_auth remains a single, simple mechanism — admin protection is enforced by requiring handler coverage in the configuration

Changes

File Change
crates/common/src/auth.rs Simplify enforce_basic_auth — remove is_admin_path() and runtime admin guard; admin paths use standard handler-based auth
crates/common/src/settings.rs Add build-time validation: from_toml_and_env() errors when uncovered_admin_endpoints() finds unprotected admin paths; update doc comments and tests
crates/common/build.rs Remove cargo:warning loop (redundant — from_toml_and_env now errors on uncovered admin endpoints)
crates/common/src/test_support.rs Add ^/admin handler to base test settings
trusted-server.toml Add ^/admin handler to dev config

Closes

Closes #400

Test plan

  • cargo test --workspace (472 tests pass)
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Admin paths (/admin/*) were only auth-gated when a configured handler's
path regex happened to match them. The default config (^/secure) left
/admin/keys/rotate and /admin/keys/deactivate publicly accessible.

Now enforce_basic_auth always denies admin paths that no handler covers,
and a startup warning alerts operators when no handler matches /admin/.

Closes #400
@ChristianPavilonis ChristianPavilonis self-assigned this Mar 5, 2026
P2: Check all admin endpoints (rotate + deactivate) in coverage
    warning instead of only checking rotate.

P2: Move admin coverage warning from per-request main() to build
    time via cargo:warning in build.rs, avoiding log noise in
    Fastly Compute where every request is a fresh WASM instance.

P3: Guard exact /admin path in addition to /admin/ prefix so
    future endpoints at that path are also protected.
Copy link
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

This PR correctly identifies and fixes the security gap where admin endpoints could be accessed without authentication when no handler is configured. The fail-closed approach is sound. However, the implementation adds complexity that may not be necessary given the existing handler-based auth system.

♻️ refactor

  • Simplify: use config validation instead of runtime guard: The handler-based auth already enforces credentials correctly. Instead of adding is_admin_path() and special-case logic in enforce_basic_auth, make Settings::validate() return an error when no handler covers admin endpoints. This keeps enforce_basic_auth unchanged, eliminates the dual-mechanism (is_admin_path prefix check vs ADMIN_ENDPOINTS list), and catches misconfiguration at build time as an error rather than silently returning 401s at runtime. The existing uncovered_admin_endpoints() method is the right building block — call it from validate() instead of build.rs.

@aram356
Copy link
Collaborator

aram356 commented Mar 6, 2026

To clarify the refactor suggestion above — the required config is just a standard [[handlers]] entry in trusted-server.toml:

[[handlers]]
path = "^/admin"
username = "admin"
password = "a-strong-password"

With the validation approach, Settings::validate() would error at build time if this entry is missing, ensuring admin endpoints are always covered by the existing handler-based auth — no runtime special-casing needed.

Move admin endpoint protection from a runtime check in enforce_basic_auth
to a build-time validation error in Settings::from_toml_and_env. This
eliminates the dual-mechanism (is_admin_path prefix check vs handler-based
auth) and catches misconfiguration at build time rather than silently
returning 401s at runtime.
@ChristianPavilonis
Copy link
Collaborator Author

Addressed the review feedback — refactored from runtime guard to build-time config validation:

  • Removed is_admin_path() and the special-case admin branch from enforce_basic_auth — it's back to a single, simple mechanism
  • Added a hard error in Settings::from_toml_and_env() when uncovered_admin_endpoints() returns any uncovered paths — build fails immediately with a clear message
  • Removed the cargo:warning loop in build.rs (redundant now that from_toml_and_env errors)
  • Added ^/admin handler to trusted-server.toml and test fixtures
  • Added from_toml_and_env_rejects_config_without_admin_handler test

All 472 tests pass, clippy clean, fmt clean.

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.

Admin endpoints unprotected unless handler regex covers them

2 participants