Warden
One door to your homelab.
Warden is a self-hosted reverse proxy and dashboard built around passkeys. Point hostnames at Warden, describe your services in YAML, and reach everything through a single passkey-protected front door — with native WebAuthn, HTTP/WebSocket proxying, TLS and certificate renewal, and optional WireGuard mesh management.
There is no account database to populate and no password to invent. On first run
Warden prints a one-time setup code, you create the admin account with a passkey,
and every service behind auth: true shares that sign-in.
Why
- Passkey-native. WebAuthn registration and login are implemented directly, not bolted onto a password login. One admin, many passkeys.
- Host-based routing. Each service gets its own hostname. Warden matches the
Hostheader and streams the request upstream, including WebSockets. - Quiet by default. Protected routes redirect browsers to a polished sign-in page. Warden cookies and spoofed identity headers never reach upstreams.
- Everything editable. Routes, access, activity, YAML config, and mesh peers are managed from the dashboard. YAML remains the source of truth.
- No build step. The dashboard is server-rendered HTML with a little vanilla JavaScript and one CSS file.
Quick start (local trial)
Passkeys require a secure context. Browsers treat localhost as secure, so use
it for a first look without TLS.
python3 -m venv .venv
. .venv/bin/activate
pip install -e .
warden init --admin-host localhost
warden serve
Open http://localhost:8080/. The startup banner prints a setup code such as
Setup code · 7K4M-9Q2X-.... Open /_warden/setup, enter the code, choose a
username, and register a passkey.
To pick the setup code yourself, set WARDEN_SETUP_TOKEN before starting:
WARDEN_SETUP_TOKEN='my-long-one-time-code' warden serve
The code must be at least 12 characters after removing punctuation. If no code is active (for example after it expires), restart Warden to generate a fresh one.
Observability
- OpenAPI documentation: The admin API OpenAPI schema is available at
/_warden/openapi.jsonand an interactive Swagger UI is served at/_warden/docs. Both endpoints are protected by admin‑host authentication. - Prometheus metrics: Request metrics, authentication counters, OTP counters, role changes, backup code usage, and service latency are exposed at
/_warden/metricsand also at/metrics. These endpoints require an authenticated admin session and the correct host.
-
Give Warden a name.
warden.home.lanis the dashboard;home.lanis the WebAuthn RP ID. Create DNS records (or/etc/hostsentries) for the admin host and every route, all pointing at the Warden machine. -
Create a config.
warden init -o /etc/warden/warden.yaml \ --admin-host warden.home.lan \ --rp-id home.lan
Then edit it. See
configs/warden.yaml.examplefor every option. -
Enable TLS. Passkeys only work over HTTPS on a real hostname. Point
server.tls.certfileandserver.tls.keyfileat a PEM certificate and key. The certificate should cover the admin host and any protected route hosts, for example with a wildcard or SAN certificate for*.home.lan.server: admin_host: warden.home.lan tls: enabled: true certfile: /etc/letsencrypt/live/warden.home.lan/fullchain.pem keyfile: /etc/letsencrypt/live/warden.home.lan/privkey.pem acme_webroot: /var/www/warden-acme renew_command: certbot renew --quiet
-
Validate and run.
warden check warden serve
The banner shows the HTTPS URL and a setup code. Open it, claim the instance, and register your first passkey.
-
Add services in the dashboard, or edit
routesin the YAML file and restart. Protected routes (auth: true) must be under the WebAuthn RP ID.
Important:
server.admin_hostmust equal the RP ID or be a subdomain of it. Each protected route host must also be under the RP ID. Routes outside it must useauth: false.
How requests flow
- Warden reads the normalized
Hostheader. - If it is the admin host, the request goes to the dashboard. The dashboard
requires a passkey session; internal routes live under
/_warden/. - Otherwise Warden finds an exact route, then the longest matching wildcard. Unknown or disabled hosts get a friendly 404 page.
- If the route is protected and the path is not in
public_paths, Warden checks the session cookie. Browsers are redirected to/_warden/login?next=...; API-style requests get401JSON. - Warden strips hop-by-hop headers, its own cookies, client-supplied
X-Forwarded-*/identity headers, and proxies the request. Authenticated requests can carry the username inX-Warden-User(configurable) and always carryX-Warden-User-Id. - Responses stream back. Upstream redirects can be rewritten to the public host,
and cookies with unreachable
Domainattributes can be cleaned up.
PATH and query strings are preserved. The upstream path in the route (for
example http://10.0.0.5:3000/api) is used as a prefix. WebSocket upgrades are
relayed in both directions when websocket: true.
Public paths
public_paths skips the passkey gate for matching request paths and WebSocket
paths. It uses Python fnmatch patterns:
routes:
- host: app.home.lan
upstream: http://192.168.1.20:3000
public_paths:
- /health
- /webhooks/*
Only expose what needs to be public. A route with auth: false is fully public.
Configuration reference
Warden validates the whole file on load. Unknown keys are rejected, so typos fail
fast. All relative paths are resolved from the directory containing
warden.yaml.
server
| Key | Default | Purpose |
|---|---|---|
admin_host |
localhost |
Dashboard hostname. Must be the RP ID or a subdomain of it. |
bind |
0.0.0.0 |
Listen address. |
http_port |
8080 |
HTTP port. With TLS enabled this is the ACME/redirect companion. |
https_port |
8443 |
HTTPS port when TLS is enabled. |
session_ttl_hours |
168 |
Lifetime of a passkey session. |
cookie_domain |
unset | Parent domain for the session cookie (SSO across subdomains). |
secure_cookies |
auto | Force Secure on the session cookie; auto means "when HTTPS". |
trust_proxy_headers |
false |
Trust inbound X-Forwarded-*. Only behind a proxy you control. |
forward_user_header |
X-Warden-User |
Header carrying the username to authenticated upstreams (null disables). |
external_scheme |
unset | Force http/https in generated URLs. |
public_url |
unset | Canonical admin origin, e.g. https://warden.home.lan. |
request_timeout_seconds |
60 |
Default upstream timeout. |
health_interval_seconds |
30 |
Dashboard health-check interval. |
log_level |
info |
debug, info, warning, error, or critical. |
rate_limiter |
unset | Configuration for request rate limiting. Optional; if omitted, a memory limiter is used. |
server.tls
| Key | Default | Purpose |
|---|---|---|
enabled |
false |
Serve HTTPS and the HTTP redirect/ACME companion. |
certfile / keyfile |
required | PEM certificate chain and private key. |
acme_webroot |
unset | Directory served at /.well-known/acme-challenge/. |
renew_command |
unset | Shell command run when renewal is due. |
renew_days_before |
30 |
Run the hook when the certificate has this many days left. |
check_interval_hours |
12 |
How often the renewal loop checks. |
Warden never reloads a certificate in place: after renewal, restart the process (or use a certbot deploy hook that restarts it). The HTTP companion stays up during renewal so webroot challenges keep working.
webauthn
| Key | Default | Purpose |
|---|---|---|
rp_id |
localhost |
WebAuthn relying-party domain. Changing it invalidates passkeys. |
rp_name |
Warden |
Name shown by the authenticator. |
require_user_verification |
false |
Require PIN/biometric verification when authenticating. |
challenge_ttl_seconds |
300 |
Lifetime of a ceremony challenge. |
allowed_origins |
[] |
Extra origins allowed to run passkey ceremonies. |
database
| Key | Default | Purpose |
|---|---|---|
path |
warden.db |
SQLite database file. |
The database stores the admin account, passkey public keys, hashed sessions, mesh private keys, challenges, settings, and the audit log. Back it up and keep it private.
mesh and routes
See the fully commented example in
configs/warden.yaml.example. Route fields:
| Key | Default | Purpose |
|---|---|---|
host |
required | Exact hostname or *.suffix wildcard. |
upstream |
required | http:// or https:// base URL, optional path prefix. |
name |
host label | Display name. |
auth |
true |
Require a passkey session (outside public_paths). |
enabled |
true |
Pause the route without deleting it. |
websocket |
true |
Allow WebSocket upgrades. |
preserve_host |
false |
Send the public Host upstream. |
verify_tls |
true |
Verify upstream TLS certificates. |
rewrite_redirects |
true |
Rewrite upstream redirects that point at the upstream itself. |
rewrite_cookies |
true |
Drop unreachable upstream cookie domains and downgrade Secure on HTTP. |
timeout_seconds |
server default | Per-route upstream timeout. |
public_paths |
[] |
Auth bypass patterns using fnmatch. |
headers |
{} |
Extra upstream request headers. |
health_path |
/ |
Path used by the health monitor. |
icon |
unset | Emoji or short label for the dashboard. |
Role‑Based Access Control (RBAC)
Warden now supports fine‑grained role‑based access control.
- Roles – defined via the admin API (
/_warden/api/roles). Each role has a name and optional description. - User‑role assignments – assign roles to users with
POST /_warden/api/users/{user_id}/roles/{role_id}and remove them with the correspondingDELETEendpoint. - Permission checks – the admin UI and API enforce that only users with the
adminrole can manage roles, users, and other privileged actions. Use the helper_require_role(request, "admin")in custom endpoints if needed. - Built‑in role – the
adminrole is created automatically on first use and should be granted to the initial administrator account.
The UI now includes a Users page where you can view users and manage their role assignments.
The Roles page (/roles) lets administrators view, create, and delete roles directly from the dashboard.
Monitoring & Metrics
Warden exposes a rich set of Prometheus metrics at GET /_warden/metrics:
warden_uptime_seconds– process uptime.- Per‑host request, error, latency, and authenticated request counters (
warden_route_*). - Authentication counters:
warden_auth_success_totalandwarden_auth_failure_total. - OTP counters:
warden_otp_success_totalandwarden_otp_failure_total. - RBAC counters:
warden_role_created_total,warden_role_deleted_total,warden_role_assigned_total,warden_role_removed_total. - Backup‑code counters:
warden_backup_code_used_total– total times a backup code was used for login.
A starter Grafana dashboard is provided in grafana/warden-dashboard.json. Import it into Grafana to visualize uptime, request volume, auth success/failure, and role activity.
- Services — add, edit, pause, and remove routes; open services; see latency, status, and traffic.
- Access — register additional passkeys, name or remove them, review and revoke sign-in sessions. Removing a passkey does not revoke existing sessions; revoke sessions separately if a device is lost.
- Activity — audit trail for setup, sign-ins, passkey changes, config edits, and mesh actions.
- Config — edit the YAML file with validation. Route and access changes apply immediately. Listener, TLS, database, RP ID, and mesh-interface changes are saved but held at their old runtime values until restart, and the page tells you a restart is required.
- Mesh — create peers, download client configs, apply the hub config, and see handshakes when WireGuard is available.
Mutations require the X-Warden-Request: 1 header and a same-origin request.
The dashboard JavaScript handles both automatically.
TLS and certificate renewal
Warden can terminate TLS itself or sit behind a TLS terminator.
Direct TLS. Enable server.tls. Warden serves HTTPS on https_port; a small
companion on http_port serves ACME challenges and redirects everything else to
HTTPS. Set acme_webroot to the same directory certbot writes to, then point the
renewal hook at certbot:
warden certs status
warden certs renew --dry-run # print the configured command
warden certs renew # run it now
The built-in renewal loop checks every check_interval_hours and runs
renew_command when fewer than renew_days_before days remain. It does not
restart Warden automatically. A deploy hook is a good place to do that:
renew_command: certbot renew --quiet --deploy-hook "systemctl restart warden"
The hook runs as the Warden service user. If certbot needs root to write
/etc/letsencrypt, either grant that user the necessary access or leave
renew_command unset and renew from a root-owned timer. In the timer case Warden
still serves the ACME challenges from acme_webroot, and you restart it after the
certificate is replaced.
The Warden service user must be able to read certfile and keyfile. Let's
Encrypt keeps private keys root-only by default, so deploy a readable copy or
grant access with a group/ACL.
Behind a TLS terminator. Leave server.tls disabled, set
external_scheme: https, secure_cookies: true, and optionally public_url.
Only enable trust_proxy_headers when the terminator is the sole way to reach
Warden and it overwrites X-Forwarded-*.
WireGuard mesh
The mesh is a hub-and-spoke WireGuard network managed alongside the proxy.
mesh:
enabled: true
interface: wg0
address: 10.44.0.1/24
listen_port: 51820
endpoint: vpn.home.lan:51820
dns: 10.44.0.1
client_allowed_ips: 10.44.0.0/24, 192.168.1.0/24
config_path: /etc/wireguard/wg0.conf
Create a peer and apply the hub config:
warden mesh peer add laptop -o warden-laptop.conf
warden mesh config > /etc/wireguard/wg0.conf # or inspect it first
warden mesh apply
warden mesh status
warden mesh apply writes the config, then uses wg syncconf if the interface is
up or wg-quick up if it is down. Peers can also be created and downloaded from
the Mesh page. Warden stores the hub private key and peer private keys in SQLite
so it can regenerate configs; protect the database accordingly.
Requirements:
wireguard-tools(wgandwg-quick) on the host.- Permission to create/configure the interface. This normally means root or
carefully scoped capabilities;
wg-quickalso invokesip,iptables, andsysctl. - A UDP port reachable from clients (
listen_port) andmesh.endpointset to the publichost:portclients should dial. mesh.config_pathwritable by the Warden process.
mesh apply is manual by design. Peer changes are saved immediately, but the
interface is only updated when you apply from the dashboard or CLI.
CLI
warden [--config PATH] COMMAND
serve run the proxy and dashboard
--host ADDR bind override
--port PORT HTTP port override
--https-port PORT HTTPS port override
--no-access-log hide per-request logs
init write a starter warden.yaml
-o/--output PATH
--admin-host HOST
--rp-id DOMAIN
--bind ADDR
--port PORT
--https-port PORT
--database PATH
--force
check [--json] validate config, TLS files, and runtime paths
certs status [--json]
certs renew [--dry-run]
auth reset [--yes] delete the account, passkeys, and sessions
(restart to generate a new setup code)
mesh status
mesh config
mesh apply
mesh peers
mesh peer add NAME [--address IP] [--endpoint HOST:PORT]
[--keepalive SECONDS] [-o FILE]
mesh peer config NAME
mesh peer remove NAME [--yes]
WARDEN_CONFIG sets the config path when --config is not given. warden --version prints the version. serve validates TLS files before binding.
Docker
The image runs Warden as an unprivileged user and installs it from the local source tree.
mkdir -p config
cp configs/warden.yaml.example config/warden.yaml
# edit config/warden.yaml: admin_host, rp_id, routes, TLS paths
docker compose up -d --build
docker compose logs -f warden
The compose file maps host 8080/8443 to the container's default ports and
keeps the database in a named volume. Edit the mappings and public_url if you
want standard ports. Mount certificates read-only and point certfile/keyfile
at the mounted paths.
WireGuard is not container-friendly by default. The example keeps it disabled. If
you enable it in Docker, the container needs host networking, /dev/net/tun,
NET_ADMIN, the WireGuard tools, and a writable config path. Running the mesh on
the host (or in a privileged sidecar) is usually simpler.
systemd
deploy/warden.service is a starting point for a
non-mesh deployment. A typical install:
sudo useradd --system --home /var/lib/warden --shell /usr/sbin/nologin warden
sudo install -d -o warden -g warden -m 750 /var/lib/warden /etc/warden
sudo install -d -o warden -g warden -m 755 /var/www/warden-acme
sudo python3 -m venv /opt/warden
sudo /opt/warden/bin/pip install /path/to/warden
sudo install -m 644 deploy/warden.service /etc/systemd/system/warden.service
sudo systemctl daemon-reload
sudo systemctl enable --now warden
sudo journalctl -u warden -f
The unit grants CAP_NET_BIND_SERVICE for ports below 1024 and uses systemd
sandboxing. Adjust ExecStart, the config path, and ReadWritePaths to match
your install. The unit deliberately targets proxy mode; a process that manages a
WireGuard interface needs broader privileges than the sandbox allows.
Security notes
- Warden is single-admin. Additional passkeys attach to the same account. There is no password fallback and no user self-registration.
- Sessions are opaque 256-bit tokens stored as SHA-256 hashes in SQLite. Cookies
are
HttpOnly,SameSite=Lax, andSecurewhen the request is HTTPS. The session cookie never reaches upstream services. - Passkey registration and login challenges are stored server-side and consumed once. Origin, content type, rate limits, and user verification are checked during ceremonies.
- CSRF protection combines same-origin/
Sec-Fetch-Sitechecks with a requiredX-Warden-Request: 1header on admin mutations. - Upstreams see sanitized
X-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto,X-Real-IP, and (for authenticated routes) identity headers. Client-supplied identity and forwarding headers are stripped. public_pathsandauth: falseare deliberate holes. Review them.- The SQLite database contains sensitive material (passkey metadata, session hashes, WireGuard private keys). Use file permissions and encrypted backups.
- Keep Warden to a single process. Sessions and rate limits are process-local; multiple workers would also fight over the same SQLite file and renewal loop.
- Run behind a network you trust. Warden's own threat model assumes the admin hostname is not exposed on the public internet without TLS.
OTP (One‑Time Password)
Warden supports optional TOTP‑based two‑factor authentication.
-
Enable OTP – An authenticated user can POST to
/_warden/auth/otp/setup. The endpoint is CSRF‑protected (requiresX-Warden-Request: 1and same‑origin). It returns a Base32 secret and anotpauth://URL that can be scanned by Google Authenticator, Authy, or any TOTP app. -
Verify OTP – After provisioning, the user POSTs to
/_warden/auth/otp/verifywith the current 6‑digit code. Successful verification increments thewarden_otp_success_totalmetric; failures incrementwarden_otp_failure_total. -
Rate limiting – OTP verification is limited to 30 attempts per minute per IP.
-
Metrics – Both success and failure counters are exposed at
/_warden/metricsand visualised in the Grafana dashboard. -
No persistent storage – The secret is stored in the database; OTP codes are time‑based and can be regenerated by the client at any time.
Backup codes UI
The admin dashboard now includes a Backup Codes page at /backup_codes. It lists all backup codes belonging to the current admin user, showing when each code was created and whether it has been used. A Generate New Backup Code button creates a fresh code via the POST /backup_codes/generate endpoint and redirects back to the list.
Warden supports one‑time backup codes for account recovery. An admin can generate a code via the API and a user can log in with it.
POST /_warden/api/backup_codes– creates a new backup code (requires an authenticated admin session).POST /_warden/auth/login/backup– logs in using a backup code.
The metric warden_backup_code_used_total records each successful backup‑code login.
Backup and recovery
Back up your configuration, database, and TLS certificates.
- the YAML config (contains secrets such as mesh settings and renewal hooks),
- the SQLite database (use
sqlite3 warden.db ".backup backup.db"or stop Warden first), - TLS certificates and keys if not managed by certbot.
To recover access when every passkey is lost:
warden auth reset --yes
# restart Warden and use the newly printed setup code
This deletes the admin account, all passkeys, sessions, and first-run settings, but leaves the audit log and mesh peers. To start the whole instance over, remove the database as well.
Development
python3 -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'
pytest
The test suite covers config validation, YAML round-tripping, SQLite persistence, TLS inspection, mesh configs, authentication ceremonies (with mocked WebAuthn verification), HTTP proxy behavior, admin APIs, WebSockets, and the CLI. It does not replace a real browser passkey ceremony.
To run against a development config:
WARDEN_CONFIG=./warden.dev.yaml warden serve --host 127.0.0.1
License
MIT. See LICENSE.
bump for auto-tag test
Metadata
Release files for warden-mesh 0.1.15
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| warden_mesh-0.1.15.tar.gz | 158.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| warden_mesh-0.1.15-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 302.4 kB
Release files / warden_mesh-0.1.15.tar.gz
| Download URL | warden_mesh-0.1.15.tar.gz |
|---|---|
| Size | 158.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
434013a0ef1c81bfe4c6ffa9b4f0111bc23ac2c3e4ea66dac21e288538fdc2b1
|
|
BLAKE2b-256 checksum How to use checksums |
143e2d570fbb5204737611b9728d6b5f57a1f2bc9f1a74a04abff8ede71f46fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / warden_mesh-0.1.15-py3-none-any.whl
| Download URL | warden_mesh-0.1.15-py3-none-any.whl |
|---|---|
| Size | 144.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5e428d8de7efa44964e008df34cb10e3e3a35c67f105bf5dd1f8993061e1c9ed
|
|
BLAKE2b-256 checksum How to use checksums |
00b3834cf9af2cd072aae14ba68e0a8bce95f3342b8b9d1ac5fc4e9f8e8ee734
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for warden-mesh 0.1.15
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for warden_mesh-0.1.15.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.