16 KiB
Administration API access - 0.28.1
Sign-in boundary
The administration listener remains separate from reverse-proxy listeners and uses
loopback HTTP by default. Settings -> Administration can enable HTTPS, select either
the application-managed default certificate or an inventory certificate, persist a
new bind address and allow explicit DNS hostnames. A persisted non-loopback bind is
accepted when HTTPS is enabled. The legacy startup flags remain only for deliberately
plain remote HTTP. Public vhost /docs paths are not captured by the admin listener.
When the dashboard is published through a trusted reverse proxy, an explicit
administration.allowed_hosts DNS entry is matched against the external authority rather
than the private admin socket port. X-Forwarded-Host is trusted only from loopback or a
configured trusted proxy; the external X-Forwarded-Proto additionally requires the
forwarded-proto trust setting except for loopback. Origin/CSRF checks and Secure cookies use
that external origin. Direct IP/localhost access still requires the real administration port.
Publishing the panel through RustProxy itself
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved rustproxy_session cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname is explicitly present in administration.allowed_hosts, the selected backend directly addresses the configured administration socket by IP, and that backend has accept_local_listener_risk=true. The control-plane route is forced out of shared cache.
This is a narrow exception to the normal isolation rule. For every ordinary backend RustProxy still removes rustproxy_session before forwarding requests and strips an upstream attempt to set that cookie in responses. DNS backend names are not accepted for the special self-admin cookie path. This prevents a normal application backend from reading or replacing the administration session.
The anonymous page contains only the sign-in/setup form, logo, shared stylesheet, a dedicated login script, and filtered language strings. It does not fetch the configuration, status, metrics, logs, certificate inventory, dashboard modules or the full translation bundle. Adding a new language JSON still discovers that language at build time; login strings use the same fallback rules.
Exact anonymous methods/paths:
| Method | Paths | Content |
|---|---|---|
| GET/HEAD | /, /overview, /vhosts, /redirects, /streams, /acl-profiles, /configuration, /tls, /cache, /logging, /settings |
Minimal login when no valid cookie is present; the same URL returns the authenticated application shell when the supplied session is valid. /configuration is a compatibility alias for Settings -> Manual config. |
| GET/HEAD | /assets/login.js, /assets/login-langs.json, /assets/style.css, /favicon.svg |
Login assets; no runtime state |
| GET/HEAD | /healthz |
{"status":"ok"} only |
| GET/HEAD | /api/auth/status |
{"setup_required":false} or true only |
| POST | /api/auth/login, /api/auth/setup |
Credential/capability validation, exact Origin and request marker required |
The document allowlist above is intentionally session-aware: a browser refresh on a protected
panel path first validates the cookie server-side instead of rendering a false logged-out state.
The login document also rechecks /api/auth/session when restored from browser history, preventing
a stale back/forward-cache copy from contradicting a still-valid session. The authenticated shell
also treats auth.invalid_origin during startup/session revalidation as a stale restored-page signal
and performs at most one reload of the current same-origin URL. A repeated failure is shown normally;
server Host/Origin, request-marker and CSRF validation is unchanged. Every other admin
path/method, including full assets, /api/metrics, /metrics, unknown endpoints and the OpenAPI
schema, requires an active database session.
Missing/invalid/expired cookies return 401 {"error":"auth.required"} before
query/body extraction. Invalid Host/Origin may return 403 first. Only GET/HEAD
/docs and /docs/ navigate to login through a fixed 303 /?next=docs redirect;
API/spec/assets never return login HTML instead of a status error.
Administrator browser sessions have a configurable absolute lifetime of 7–30 days
(default 30) through administration.session_days. The server stores only a hash of the
session secret, keeps the cookie HttpOnly/SameSite=Strict (and Secure on HTTPS), and
never extends a session past its creation-time absolute expiry. The cookie is refreshed
with only the remaining lifetime so an upgraded still-valid short session can adopt the
new configured bound. Logout, password change and revoke-all still invalidate server-side
sessions immediately.
Responses are no-store. Headers include CSP, nosniff, frame denial, no-referrer and restricted camera/microphone/geolocation permissions. Error responses do not expose request-body excerpts for malformed anonymous login/setup input. Anonymous responses contain no application version; fingerprint/revision response headers are stripped on the admin surface. Authenticated status and the protected schema intentionally retain the current version. The application name/logo and the single setup-needed bit are intentional public information, not claims of anonymity or software fingerprint prevention.
Authenticated GET /api/status also returns a small runtime object with the platform,
statistics adapter name, resolved data directory and SQLite database path. This is used by
the Settings UI so Linux, BSD/OPNsense and custom installations show their actual runtime
paths instead of browser-side guesses. The object is not available anonymously and does
not expose credentials, environment values, certificate contents or arbitrary filesystem
listings.
Docs Reader and API view
/docs opens an authenticated documentation workspace. The default Reader tab loads
/docs/content.json, a build-time bundle of the project Markdown documentation listed in
docs/README.md. The bundle contains documentation source text only; it does not contain
live configuration, credentials or runtime state. Reader links between bundled Markdown
documents stay inside the same authenticated documentation surface.
The separate API tab uses Swagger UI. /docs/openapi.json describes the local endpoints and common request schemas; it
contains no live configuration, actual credentials, generated keys, user accounts,
cache entries or listener addresses. Extensible schemas summarize configuration;
the Rust validator remains authoritative for the full configuration model.
Documentation uses the same HttpOnly rustproxy_session cookie as the panel.
Do not paste session cookies into the schema or store them in browser storage.
GET execution is enabled by default; write execution requires a checkbox and an
explicit confirmation for each request. The script validates the current session,
allows only documented same-origin operations, and sends the current session's
X-CSRF-Token plus X-RustProxy-Request: 1. The browser supplies Origin. The server
performs the real authorization checks regardless of any browser options.
Login/setup operations and indefinite SSE streams are documented but not executed by Swagger UI. Native logout/password writes can invalidate the browser session; reload or sign in again afterward. Ordinary API mutations still require the administrator session, Origin/request marker and CSRF token.
0.23.0 adds one deliberately narrow exception for certificate automation. An operator can create one or more Certificate API Key ID + Bearer token credentials in Settings -> Certificate API. Each credential may carry an operator comment and authorizes only:
POST /api/certificates/deployPOST /api/certificates/reloadPOST /api/certificates/{certificate_id}/reload
Both Authorization: Bearer <token> and X-RustProxy-Certificate-Key: <key-id> are required.
The clear token is returned only on creation/rotation; the database stores its SHA-256 hash. Up to 32 Key IDs can coexist, and each can be revoked independently or all at once. The credential does not authorize configuration, logs, metrics, users or any other endpoint. Non-loopback use is accepted only on the configured HTTPS administration surface. Revocation from Settings invalidates that credential immediately.
Local host automation does not need this token. The installed binary can run
rust-proxy tls deploy ... or rust-proxy tls reload directly; it obtains short-lived local
CLI authority from the local database and calls the guarded loopback administration API.
dev.sh is a development helper and is not required in production.
The external validator, query-supplied schema/server configuration and persisted
Swagger authorization are disabled. Runtime JS/CSS are served from the same admin
origin behind its guard. Only the two necessary Swagger vendor asset filenames, the documentation bundle and a plain-text license notice
(/docs/licenses.txt) are routed; all require an administrator session.
Swagger's inline layout styles have a document-scoped CSP exception; scripts stay
self-only with no unsafe-eval allowance. The ordinary dashboard CSP is unchanged.
Logout broadcasts an in-memory notification between tabs. The panel drops its operational document on logout/revocation; documentation clears its schema, responses and write opt-in. Docs recheck sessions on actions, visibility and every 30 seconds while visible. Browser history restoration revalidates; this is not a promise to erase browser/OS screenshots or data intentionally downloaded by an admin.
First build / offline assets (fixed in 0.7.2)
No SWAGGER_UI_DOWNLOAD_URL environment variable or .cargo/config.toml is required.
The default is Swagger UI 5.32.11, selected by scripts/build/swagger_assets.py and
called from this application's build.rs. The former utoipa-swagger-ui adapter
is removed: there is no separate dependency build silently choosing its older
historical browser default. Direct Cargo builds using --manifest-path use the
project's absolute manifest directory, not the caller's working directory.
Python 3.10+ is now also required for direct Cargo builds, as it already was
for dev.sh. build.rs tries python3, then python if that executable is absent.
Use RUST_PROXY_PYTHON=/absolute/path/to/python3 to select an interpreter explicitly.
The helper uses Python's verified HTTPS transport and standard-library ZIP reader;
curl is no longer a Swagger build requirement. Missing tools/network/assets give
an actionable build error rather than an absent-environment-variable panic.
Normal build (no Swagger environment setup):
cd RustProxy
bash dev.sh check
bash dev.sh test
bash dev.sh release
The first build still needs the normal Cargo registry and the pinned upstream archive. Swagger JS/CSS and license notices are embedded in both debug and release binaries, served behind the existing admin guard. No runtime download, CDN, public asset endpoint or alternate authentication mechanism was introduced.
An offline or GitHub-restricted build can use a reviewed local copy of the same
GitHub tag archive, with its original swagger-ui-5.32.11/ directory structure:
export SWAGGER_UI_DOWNLOAD_URL=file:///srv/build-assets/swagger-ui-5.32.11.zip
# Optional: require the archive digest independently verified by your build team.
# export SWAGGER_UI_ARCHIVE_SHA256=<64-hex-digit-verified-digest>
bash dev.sh check
bash dev.sh test
bash dev.sh release
Unset/empty source values use the default pin. Different remote URLs and old tag
archives are rejected. Local file URLs support escaped spaces and must be absolute.
SWAGGER_UI_ARCHIVE_SHA256, when set, checks the entire archive before publication.
No upstream checksum is fabricated: HTTPS provenance, the archive's declared tag
and its layout are not themselves a cryptographic authenticity guarantee for an
operator-supplied ZIP. Independently review/pin local inputs.
The helper only reads exact, bounded members; it does not extract arbitrary ZIP
paths. It retains licenses/notices, validates package name/version, rejects duplicate
required members, symlinks and oversized assets, caches only validated downloads in
Cargo's output directory, and writes outputs atomically. Failed preparation blocks
compilation instead of embedding stale assets. A source.json receipt records
archive and output SHA-256 values in OUT_DIR/swagger-ui/.
Cargo tracks changes to the helper, source/digest/interpreter environment variables,
local or cached ZIP and generated outputs. Repeated identical preparation preserves
output timestamps. CARGO_NET_OFFLINE=true forbids a new helper download; ordinary
Cargo dependencies must also be cached for a fully offline build. A local archive
is the recommended explicit setup for cargo --offline; Cargo CLI offline flags
are not assumed to propagate automatically into subprocess environments.
0.24.1 adds cross-platform statistics dependencies. This packaging workspace has no Cargo toolchain, so it does not fabricate or hand-edit a resolver result. Generate and preserve Cargo.lock with Cargo on the compiler-enabled release host, then run cargo check --locked / the normal dev.sh gates before deployment.
Build-helper tests use synthetic ZIPs; they do not claim to render Swagger. This environment lacks Rust/Cargo and cannot fetch the upstream assets. Compilation, real embedding/rendering and live access-control acceptance remain to be run on a build server. Browser tests still label their simulated API/Swagger renderer.
Focused live regression probe
After running the built server:
python3 tools/audit/admin_audit.py --url http://127.0.0.1:9090
The script sends anonymous read-only loopback probes, follows no redirects, ignores proxy environment variables, prints no response bodies, and fails on unexpected public content/statuses. It does not send credentials or mutate configuration. It is also called by the real-binary security integration suite. Rust router tests add write/method/duplicate-cookie/CSRF checks. A passing probe is a regression gate, not a complete pentest, dependency scan or guarantee against all vulnerabilities.
Custom location headers
Vhosts -> edit -> Custom locations -> Location headers. Add rules independently
for proxy requests (to backend) and responses (to client). set replaces, append
adds another field value, remove has no value. Maximum 32 rules per direction.
The existing JSON editor is synchronized and remains optional. Invalid JSON,
newlines, unknown operations and protected names cannot silently save an old draft.
Rules apply global -> vhost -> matching location. Proxy transport, forwarding, verified authentication identity, configured Server/security/diagnostic policies remain system-managed and can override or forbid a conflicting custom rule. Values are literal, not Nginx variables; changing Host/forwarded identity still uses the designated backend/forwarding controls. Headers for sibling locations remain unchanged. No configuration schema migration is needed for existing header rules.
References
- OWASP REST Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Swagger cookie authentication: https://swagger.io/docs/specification/v3_0/authentication/cookie-authentication/
- Swagger UI configuration: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
- Cargo configuration discovery: https://doc.rust-lang.org/cargo/reference/config.html
- Cargo build scripts: https://doc.rust-lang.org/cargo/reference/build-scripts.html
- Pinned browser release: https://github.com/swagger-api/swagger-ui/tree/v5.32.11