Skip to content

Add admin.allowed_hosts to let /admin accept LAN Host headers#1

Merged
splch merged 7 commits into
mainfrom
admin-allowed-hosts
Jul 6, 2026
Merged

Add admin.allowed_hosts to let /admin accept LAN Host headers#1
splch merged 7 commits into
mainfrom
admin-allowed-hosts

Conversation

@mocha

@mocha mocha commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

/admin's DNS-rebinding guard builds its accepted-Host allowlist from
api.bind plus the loopback aliases (127.0.0.1/localhost/[::1]). That
works when api.bind is the default 127.0.0.1:PORT, but for any
non-loopback deployment api.bind has to be a wildcard address like
0.0.0.0:PORT — and the literal string "0.0.0.0:PORT" is never a real
browser Host header. So with a wildcard bind, /admin returns 403 host not allowed for every possible client, with no way to opt in.

Hit this running a node on a LAN container (api.bind = "0.0.0.0:8080") —
/admin was reachable over HTTP for search, but unreachable for anything
requiring the Host check, from any real client on the network.

Fix

New [admin] config section:

[admin]
allowed_hosts = ["mynode.lan:8080", "192.168.1.50:8080"]

Additive to the existing api.bind/loopback allowlist — same exact
(case-insensitive) Host-header match as before, just operator-extensible.
Follows the existing config module's conventions (deny_unknown_fields,
derived Default, documented in the commented DEFAULT_CONFIG_TOML
template, covered in docs/MANUAL.md).

Testing

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, and
    cargo test (63 tests, all existing suites + a new config test and an
    extension to tests/admin_ui.rs) all pass.
  • Functional: built this branch and ran it on an actual LAN-bound node
    (api.bind = "0.0.0.0:8080"), confirmed /admin returned 403 before
    the config change and 200 (all 7 operation forms rendering) after,
    hitting it from a separate host on the network by both hostname and IP.

mocha and others added 4 commits July 6, 2026 15:24
/admin's DNS-rebinding guard only ever accepted Host values derived from
api.bind plus the loopback aliases. With api.bind = "0.0.0.0:PORT" (needed
for any non-loopback deployment), the literal string "0.0.0.0:PORT" is
never a real browser Host header, so /admin is unreachable by LAN IP or
hostname with no way to opt in.

Adds a new [admin] config section with an allowed_hosts list, additive to
the existing api.bind/loopback allowlist. Same Host-header exact-match gate
as before, just an operator-controlled list of extra values.
- tests/admin_ui.rs: the valid-config-save assertion appends
  "page_size = 7\n" directly onto base_cfg assuming [api] is the last
  open table; my [admin] section landed after [api] and broke that.
  Moved [admin] before [api] in base_cfg.
- config.rs: clippy (derivable_impls) flagged the manual Default impl
  for AdminCfg since it's just Vec::new(); derive it instead.

Verified locally: cargo fmt --check, cargo clippy --all-targets -- -D
warnings, and cargo test (63 tests) all pass clean.
Review fixes on top of the allowed_hosts feature:

- config.rs: reject allowed_hosts entries that are empty or contain '/'
  or whitespace. Such a value can never equal a real Host header, so the
  misconfiguration would otherwise present as the same 403 the operator
  is trying to escape, with no signal. The check runs on both config
  paths (startup load and the admin editor validate through the same
  fn). IPv6 literals like "[::1]:8080" stay legal, pinned by test.
- docs/SPEC.md §3 claims "all defaults shown"; add the [admin] section
  so that stays true (marked post-v1 like the /admin surface in §10).
- docs/MANUAL.md: turn the "see the admin page above" cross-reference
  into the anchor link used elsewhere (the section is below, not above);
  document the new validation rule; state the exposure plainly (a listed
  name admits everyone on the network to every operation, CSRF token
  included; it is not authentication); drop an em dash per the repo
  prose convention.
- config.rs: collapse the [admin] template comment to one line to match
  every other option in DEFAULT_CONFIG_TOML.

Gates: cargo fmt --check, cargo clippy --all-targets -- -D warnings,
cargo test (64 tests) all green; the reject path also exercised against
the real binary (a scheme-bearing entry fails startup with the new
message).
No behavior change. Drop the rationale comments (validate()'s checks
carry none), reshape the validation test after peer_id_validated, revert
the AdminState field doc to its original line, and cut the MANUAL
additions down to the delta: the trust-model and loopback details they
repeated already live in the adjacent paragraphs and linked section.

@splch splch left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

nice addition! i ran into this issue earlier today as well

splch added 3 commits July 6, 2026 16:45
The rebuilt site shipped with SPEC's planning estimate (~6,300) in the
stats strip and the graveyard card; src/ is 8,490 lines as of 0.3.0,
same src-only count the old site's 8,447 used.
@splch splch merged commit 67dd038 into main Jul 6, 2026
1 check passed
@splch splch deleted the admin-allowed-hosts branch July 6, 2026 23:54
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.

2 participants