warden-mesh 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. warden_mesh-0.1.8/LICENSE +21 -0
  2. warden_mesh-0.1.8/PKG-INFO +572 -0
  3. warden_mesh-0.1.8/README.md +538 -0
  4. warden_mesh-0.1.8/pyproject.toml +67 -0
  5. warden_mesh-0.1.8/setup.cfg +4 -0
  6. warden_mesh-0.1.8/tests/test_app.py +527 -0
  7. warden_mesh-0.1.8/tests/test_auth.py +556 -0
  8. warden_mesh-0.1.8/tests/test_cli.py +454 -0
  9. warden_mesh-0.1.8/tests/test_cli_parser.py +19 -0
  10. warden_mesh-0.1.8/tests/test_config.py +211 -0
  11. warden_mesh-0.1.8/tests/test_db.py +245 -0
  12. warden_mesh-0.1.8/tests/test_main_helpers.py +92 -0
  13. warden_mesh-0.1.8/tests/test_mesh.py +273 -0
  14. warden_mesh-0.1.8/tests/test_otp_backup_metrics.py +92 -0
  15. warden_mesh-0.1.8/tests/test_rbac.py +46 -0
  16. warden_mesh-0.1.8/tests/test_tls.py +149 -0
  17. warden_mesh-0.1.8/tests/test_utils.py +146 -0
  18. warden_mesh-0.1.8/tests/test_ws.py +216 -0
  19. warden_mesh-0.1.8/warden/__init__.py +4 -0
  20. warden_mesh-0.1.8/warden/__main__.py +4 -0
  21. warden_mesh-0.1.8/warden/admin/routes.py +991 -0
  22. warden_mesh-0.1.8/warden/asgi.py +11 -0
  23. warden_mesh-0.1.8/warden/auth/passkeys.py +585 -0
  24. warden_mesh-0.1.8/warden/auth/routes.py +637 -0
  25. warden_mesh-0.1.8/warden/auth/sessions.py +176 -0
  26. warden_mesh-0.1.8/warden/cli.py +851 -0
  27. warden_mesh-0.1.8/warden/config.py +981 -0
  28. warden_mesh-0.1.8/warden/db.py +1161 -0
  29. warden_mesh-0.1.8/warden/i18n.py +41 -0
  30. warden_mesh-0.1.8/warden/main.py +378 -0
  31. warden_mesh-0.1.8/warden/mesh.py +440 -0
  32. warden_mesh-0.1.8/warden/middleware.py +360 -0
  33. warden_mesh-0.1.8/warden/proxy.py +744 -0
  34. warden_mesh-0.1.8/warden/stats.py +386 -0
  35. warden_mesh-0.1.8/warden/templates/activity.html +98 -0
  36. warden_mesh-0.1.8/warden/templates/auth_base.html +55 -0
  37. warden_mesh-0.1.8/warden/templates/backup_codes.html +23 -0
  38. warden_mesh-0.1.8/warden/templates/base.html +71 -0
  39. warden_mesh-0.1.8/warden/templates/config.html +125 -0
  40. warden_mesh-0.1.8/warden/templates/dashboard.html +341 -0
  41. warden_mesh-0.1.8/warden/templates/error.html +37 -0
  42. warden_mesh-0.1.8/warden/templates/login.html +53 -0
  43. warden_mesh-0.1.8/warden/templates/mesh.html +245 -0
  44. warden_mesh-0.1.8/warden/templates/passkeys.html +203 -0
  45. warden_mesh-0.1.8/warden/templates/roles.html +76 -0
  46. warden_mesh-0.1.8/warden/templates/setup.html +56 -0
  47. warden_mesh-0.1.8/warden/templates/static/admin.js +781 -0
  48. warden_mesh-0.1.8/warden/templates/static/auth.js +96 -0
  49. warden_mesh-0.1.8/warden/templates/static/favicon.svg +6 -0
  50. warden_mesh-0.1.8/warden/templates/static/warden.css +1823 -0
  51. warden_mesh-0.1.8/warden/templates/static/warden.js +367 -0
  52. warden_mesh-0.1.8/warden/templates/tls.html +24 -0
  53. warden_mesh-0.1.8/warden/templates/users.html +124 -0
  54. warden_mesh-0.1.8/warden/templating.py +39 -0
  55. warden_mesh-0.1.8/warden/tls.py +411 -0
  56. warden_mesh-0.1.8/warden/utils.py +526 -0
  57. warden_mesh-0.1.8/warden_mesh.egg-info/PKG-INFO +572 -0
  58. warden_mesh-0.1.8/warden_mesh.egg-info/SOURCES.txt +60 -0
  59. warden_mesh-0.1.8/warden_mesh.egg-info/dependency_links.txt +1 -0
  60. warden_mesh-0.1.8/warden_mesh.egg-info/entry_points.txt +2 -0
  61. warden_mesh-0.1.8/warden_mesh.egg-info/requires.txt +15 -0
  62. warden_mesh-0.1.8/warden_mesh.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Warden contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,572 @@
1
+ Metadata-Version: 2.4
2
+ Name: warden-mesh
3
+ Version: 0.1.8
4
+ Summary: A zero-config, passkey-protected reverse proxy and homelab dashboard.
5
+ Author: Warden contributors
6
+ License-Expression: MIT
7
+ Keywords: reverse-proxy,homelab,webauthn,passkeys,wireguard,self-hosted
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Internet :: Proxy Servers
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: fastapi>=0.115
20
+ Requires-Dist: uvicorn[standard]>=0.30
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: webauthn>=2.0
23
+ Requires-Dist: jinja2>=3.1
24
+ Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: ruamel.yaml>=0.18
26
+ Requires-Dist: websockets>=13
27
+ Requires-Dist: cryptography>=42
28
+ Requires-Dist: acme>=2.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
32
+ Requires-Dist: asgi-lifespan>=2.1; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Warden
36
+
37
+ [![auto-tag](https://github.com/patpadgett/warden/actions/workflows/auto-tag.yml/badge.svg)](https://github.com/patpadgett/warden/actions/workflows/auto-tag.yml)
38
+ [![CI](https://github.com/patpadgett/warden/actions/workflows/ci.yml/badge.svg)](https://github.com/patpadgett/warden/actions/workflows/ci.yml)
39
+
40
+ **One door to your homelab.**
41
+
42
+ Warden is a self-hosted reverse proxy and dashboard built around passkeys. Point
43
+ hostnames at Warden, describe your services in YAML, and reach everything through
44
+ a single passkey-protected front door — with native WebAuthn, HTTP/WebSocket
45
+ proxying, TLS and certificate renewal, and optional WireGuard mesh management.
46
+
47
+ There is no account database to populate and no password to invent. On first run
48
+ Warden prints a one-time setup code, you create the admin account with a passkey,
49
+ and every service behind `auth: true` shares that sign-in.
50
+
51
+ ## Why
52
+
53
+ - **Passkey-native.** WebAuthn registration and login are implemented directly,
54
+ not bolted onto a password login. One admin, many passkeys.
55
+ - **Host-based routing.** Each service gets its own hostname. Warden matches the
56
+ `Host` header and streams the request upstream, including WebSockets.
57
+ - **Quiet by default.** Protected routes redirect browsers to a polished sign-in
58
+ page. Warden cookies and spoofed identity headers never reach upstreams.
59
+ - **Everything editable.** Routes, access, activity, YAML config, and mesh peers
60
+ are managed from the dashboard. YAML remains the source of truth.
61
+ - **No build step.** The dashboard is server-rendered HTML with a little vanilla
62
+ JavaScript and one CSS file.
63
+
64
+ ## Quick start (local trial)
65
+
66
+ Passkeys require a secure context. Browsers treat `localhost` as secure, so use
67
+ it for a first look without TLS.
68
+
69
+ ```bash
70
+ python3 -m venv .venv
71
+ . .venv/bin/activate
72
+ pip install -e .
73
+
74
+ warden init --admin-host localhost
75
+ warden serve
76
+ ```
77
+
78
+ Open <http://localhost:8080/>. The startup banner prints a setup code such as
79
+ `Setup code · 7K4M-9Q2X-...`. Open `/_warden/setup`, enter the code, choose a
80
+ username, and register a passkey.
81
+
82
+ To pick the setup code yourself, set `WARDEN_SETUP_TOKEN` before starting:
83
+
84
+ ```bash
85
+ WARDEN_SETUP_TOKEN='my-long-one-time-code' warden serve
86
+ ```
87
+
88
+ The code must be at least 12 characters after removing punctuation. If no code
89
+ is active (for example after it expires), restart Warden to generate a fresh one.
90
+
91
+ ## Observability
92
+
93
+ - **OpenAPI documentation**: The admin API OpenAPI schema is available at `/_warden/openapi.json` and an interactive Swagger UI is served at `/_warden/docs`. Both endpoints are protected by admin‑host authentication.
94
+ - **Prometheus metrics**: Request metrics, authentication counters, OTP counters, role changes, backup code usage, and service latency are exposed at `/_warden/metrics` and also at `/metrics`. These endpoints require an authenticated admin session and the correct host.
95
+
96
+ 1. **Give Warden a name.** `warden.home.lan` is the dashboard; `home.lan` is the
97
+ WebAuthn RP ID. Create DNS records (or `/etc/hosts` entries) for the admin host
98
+ and every route, all pointing at the Warden machine.
99
+
100
+ 2. **Create a config.**
101
+
102
+ ```bash
103
+ warden init -o /etc/warden/warden.yaml \
104
+ --admin-host warden.home.lan \
105
+ --rp-id home.lan
106
+ ```
107
+
108
+ Then edit it. See [`configs/warden.yaml.example`](configs/warden.yaml.example)
109
+ for every option.
110
+
111
+ 3. **Enable TLS.** Passkeys only work over HTTPS on a real hostname. Point
112
+ `server.tls.certfile` and `server.tls.keyfile` at a PEM certificate and key.
113
+ The certificate should cover the admin host and any protected route hosts, for
114
+ example with a wildcard or SAN certificate for `*.home.lan`.
115
+
116
+ ```yaml
117
+ server:
118
+ admin_host: warden.home.lan
119
+ tls:
120
+ enabled: true
121
+ certfile: /etc/letsencrypt/live/warden.home.lan/fullchain.pem
122
+ keyfile: /etc/letsencrypt/live/warden.home.lan/privkey.pem
123
+ acme_webroot: /var/www/warden-acme
124
+ renew_command: certbot renew --quiet
125
+ ```
126
+
127
+ 4. **Validate and run.**
128
+
129
+ ```bash
130
+ warden check
131
+ warden serve
132
+ ```
133
+
134
+ The banner shows the HTTPS URL and a setup code. Open it, claim the instance,
135
+ and register your first passkey.
136
+
137
+ 5. **Add services** in the dashboard, or edit `routes` in the YAML file and
138
+ restart. Protected routes (`auth: true`) must be under the WebAuthn RP ID.
139
+
140
+ > **Important:** `server.admin_host` must equal the RP ID or be a subdomain of it.
141
+ > Each protected route host must also be under the RP ID. Routes outside it must
142
+ > use `auth: false`.
143
+
144
+ ## How requests flow
145
+
146
+ 1. Warden reads the normalized `Host` header.
147
+ 2. If it is the admin host, the request goes to the dashboard. The dashboard
148
+ requires a passkey session; internal routes live under `/_warden/`.
149
+ 3. Otherwise Warden finds an exact route, then the longest matching wildcard.
150
+ Unknown or disabled hosts get a friendly 404 page.
151
+ 4. If the route is protected and the path is not in `public_paths`, Warden checks
152
+ the session cookie. Browsers are redirected to `/_warden/login?next=...`;
153
+ API-style requests get `401` JSON.
154
+ 5. Warden strips hop-by-hop headers, its own cookies, client-supplied
155
+ `X-Forwarded-*`/identity headers, and proxies the request. Authenticated
156
+ requests can carry the username in `X-Warden-User` (configurable) and always
157
+ carry `X-Warden-User-Id`.
158
+ 6. Responses stream back. Upstream redirects can be rewritten to the public host,
159
+ and cookies with unreachable `Domain` attributes can be cleaned up.
160
+
161
+ `PATH` and query strings are preserved. The upstream path in the route (for
162
+ example `http://10.0.0.5:3000/api`) is used as a prefix. WebSocket upgrades are
163
+ relayed in both directions when `websocket: true`.
164
+
165
+ ### Public paths
166
+
167
+ `public_paths` skips the passkey gate for matching request paths and WebSocket
168
+ paths. It uses Python `fnmatch` patterns:
169
+
170
+ ```yaml
171
+ routes:
172
+ - host: app.home.lan
173
+ upstream: http://192.168.1.20:3000
174
+ public_paths:
175
+ - /health
176
+ - /webhooks/*
177
+ ```
178
+
179
+ Only expose what needs to be public. A route with `auth: false` is fully public.
180
+
181
+ ## Configuration reference
182
+
183
+ Warden validates the whole file on load. Unknown keys are rejected, so typos fail
184
+ fast. All relative paths are resolved from the directory containing
185
+ `warden.yaml`.
186
+
187
+ ### `server`
188
+
189
+ | Key | Default | Purpose |
190
+ | --- | --- | --- |
191
+ | `admin_host` | `localhost` | Dashboard hostname. Must be the RP ID or a subdomain of it. |
192
+ | `bind` | `0.0.0.0` | Listen address. |
193
+ | `http_port` | `8080` | HTTP port. With TLS enabled this is the ACME/redirect companion. |
194
+ | `https_port` | `8443` | HTTPS port when TLS is enabled. |
195
+ | `session_ttl_hours` | `168` | Lifetime of a passkey session. |
196
+ | `cookie_domain` | unset | Parent domain for the session cookie (SSO across subdomains). |
197
+ | `secure_cookies` | auto | Force `Secure` on the session cookie; auto means "when HTTPS". |
198
+ | `trust_proxy_headers` | `false` | Trust inbound `X-Forwarded-*`. Only behind a proxy you control. |
199
+ | `forward_user_header` | `X-Warden-User` | Header carrying the username to authenticated upstreams (`null` disables). |
200
+ | `external_scheme` | unset | Force `http`/`https` in generated URLs. |
201
+ | `public_url` | unset | Canonical admin origin, e.g. `https://warden.home.lan`. |
202
+ | `request_timeout_seconds` | `60` | Default upstream timeout. |
203
+ | `health_interval_seconds` | `30` | Dashboard health-check interval. |
204
+ | `log_level` | `info` | `debug`, `info`, `warning`, `error`, or `critical`. |
205
+ | `rate_limiter` | unset | Configuration for request rate limiting. Optional; if omitted, a memory limiter is used. |
206
+
207
+ ### `server.tls`
208
+
209
+ | Key | Default | Purpose |
210
+ | --- | --- | --- |
211
+ | `enabled` | `false` | Serve HTTPS and the HTTP redirect/ACME companion. |
212
+ | `certfile` / `keyfile` | required | PEM certificate chain and private key. |
213
+ | `acme_webroot` | unset | Directory served at `/.well-known/acme-challenge/`. |
214
+ | `renew_command` | unset | Shell command run when renewal is due. |
215
+ | `renew_days_before` | `30` | Run the hook when the certificate has this many days left. |
216
+ | `check_interval_hours` | `12` | How often the renewal loop checks. |
217
+
218
+ Warden never reloads a certificate in place: after renewal, restart the process
219
+ (or use a certbot deploy hook that restarts it). The HTTP companion stays up
220
+ during renewal so webroot challenges keep working.
221
+
222
+ ### `webauthn`
223
+
224
+ | Key | Default | Purpose |
225
+ | --- | --- | --- |
226
+ | `rp_id` | `localhost` | WebAuthn relying-party domain. Changing it invalidates passkeys. |
227
+ | `rp_name` | `Warden` | Name shown by the authenticator. |
228
+ | `require_user_verification` | `false` | Require PIN/biometric verification when authenticating. |
229
+ | `challenge_ttl_seconds` | `300` | Lifetime of a ceremony challenge. |
230
+ | `allowed_origins` | `[]` | Extra origins allowed to run passkey ceremonies. |
231
+
232
+ ### `database`
233
+
234
+ | Key | Default | Purpose |
235
+ | --- | --- | --- |
236
+ | `path` | `warden.db` | SQLite database file. |
237
+
238
+ The database stores the admin account, passkey public keys, hashed sessions,
239
+ mesh private keys, challenges, settings, and the audit log. Back it up and keep
240
+ it private.
241
+
242
+ ### `mesh` and `routes`
243
+
244
+ See the fully commented example in
245
+ [`configs/warden.yaml.example`](configs/warden.yaml.example). Route fields:
246
+
247
+ | Key | Default | Purpose |
248
+ | --- | --- | --- |
249
+ | `host` | required | Exact hostname or `*.suffix` wildcard. |
250
+ | `upstream` | required | `http://` or `https://` base URL, optional path prefix. |
251
+ | `name` | host label | Display name. |
252
+ | `auth` | `true` | Require a passkey session (outside `public_paths`). |
253
+ | `enabled` | `true` | Pause the route without deleting it. |
254
+ | `websocket` | `true` | Allow WebSocket upgrades. |
255
+ | `preserve_host` | `false` | Send the public `Host` upstream. |
256
+ | `verify_tls` | `true` | Verify upstream TLS certificates. |
257
+ | `rewrite_redirects` | `true` | Rewrite upstream redirects that point at the upstream itself. |
258
+ | `rewrite_cookies` | `true` | Drop unreachable upstream cookie domains and downgrade `Secure` on HTTP. |
259
+ | `timeout_seconds` | server default | Per-route upstream timeout. |
260
+ | `public_paths` | `[]` | Auth bypass patterns using `fnmatch`. |
261
+ | `headers` | `{}` | Extra upstream request headers. |
262
+ | `health_path` | `/` | Path used by the health monitor. |
263
+ | `icon` | unset | Emoji or short label for the dashboard. |
264
+
265
+ ## Role‑Based Access Control (RBAC)
266
+
267
+ Warden now supports fine‑grained role‑based access control.
268
+
269
+ * **Roles** – defined via the admin API (`/_warden/api/roles`). Each role has a name and optional description.
270
+ * **User‑role assignments** – assign roles to users with `POST /_warden/api/users/{user_id}/roles/{role_id}` and remove them with the corresponding `DELETE` endpoint.
271
+ * **Permission checks** – the admin UI and API enforce that only users with the `admin` role can manage roles, users, and other privileged actions. Use the helper `_require_role(request, "admin")` in custom endpoints if needed.
272
+ * **Built‑in role** – the `admin` role is created automatically on first use and should be granted to the initial administrator account.
273
+
274
+ The UI now includes a **Users** page where you can view users and manage their role assignments.
275
+
276
+ The **Roles** page (`/roles`) lets administrators view, create, and delete roles directly from the dashboard.
277
+
278
+ ---
279
+
280
+ ## Monitoring & Metrics
281
+
282
+ Warden exposes a rich set of Prometheus metrics at `GET /_warden/metrics`:
283
+
284
+ * `warden_uptime_seconds` – process uptime.
285
+ * Per‑host request, error, latency, and authenticated request counters (`warden_route_*`).
286
+ * Authentication counters: `warden_auth_success_total` and `warden_auth_failure_total`.
287
+ * OTP counters: `warden_otp_success_total` and `warden_otp_failure_total`.
288
+ * RBAC counters: `warden_role_created_total`, `warden_role_deleted_total`, `warden_role_assigned_total`, `warden_role_removed_total`.
289
+ * Backup‑code counters: `warden_backup_code_used_total` – total times a backup code was used for login.
290
+
291
+ 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.
292
+
293
+ ---
294
+
295
+ - **Services** — add, edit, pause, and remove routes; open services; see latency,
296
+ status, and traffic.
297
+ - **Access** — register additional passkeys, name or remove them, review and
298
+ revoke sign-in sessions. Removing a passkey does not revoke existing sessions;
299
+ revoke sessions separately if a device is lost.
300
+ - **Activity** — audit trail for setup, sign-ins, passkey changes, config edits,
301
+ and mesh actions.
302
+ - **Config** — edit the YAML file with validation. Route and access changes apply
303
+ immediately. Listener, TLS, database, RP ID, and mesh-interface changes are
304
+ saved but held at their old runtime values until restart, and the page tells
305
+ you a restart is required.
306
+ - **Mesh** — create peers, download client configs, apply the hub config, and see
307
+ handshakes when WireGuard is available.
308
+
309
+ Mutations require the `X-Warden-Request: 1` header and a same-origin request.
310
+ The dashboard JavaScript handles both automatically.
311
+
312
+ ## TLS and certificate renewal
313
+
314
+ Warden can terminate TLS itself or sit behind a TLS terminator.
315
+
316
+ **Direct TLS.** Enable `server.tls`. Warden serves HTTPS on `https_port`; a small
317
+ companion on `http_port` serves ACME challenges and redirects everything else to
318
+ HTTPS. Set `acme_webroot` to the same directory certbot writes to, then point the
319
+ renewal hook at certbot:
320
+
321
+ ```bash
322
+ warden certs status
323
+ warden certs renew --dry-run # print the configured command
324
+ warden certs renew # run it now
325
+ ```
326
+
327
+ The built-in renewal loop checks every `check_interval_hours` and runs
328
+ `renew_command` when fewer than `renew_days_before` days remain. It does not
329
+ restart Warden automatically. A deploy hook is a good place to do that:
330
+
331
+ ```yaml
332
+ renew_command: certbot renew --quiet --deploy-hook "systemctl restart warden"
333
+ ```
334
+
335
+ The hook runs as the Warden service user. If certbot needs root to write
336
+ `/etc/letsencrypt`, either grant that user the necessary access or leave
337
+ `renew_command` unset and renew from a root-owned timer. In the timer case Warden
338
+ still serves the ACME challenges from `acme_webroot`, and you restart it after the
339
+ certificate is replaced.
340
+
341
+ The Warden service user must be able to read `certfile` and `keyfile`. Let's
342
+ Encrypt keeps private keys root-only by default, so deploy a readable copy or
343
+ grant access with a group/ACL.
344
+
345
+ **Behind a TLS terminator.** Leave `server.tls` disabled, set
346
+ `external_scheme: https`, `secure_cookies: true`, and optionally `public_url`.
347
+ Only enable `trust_proxy_headers` when the terminator is the sole way to reach
348
+ Warden and it overwrites `X-Forwarded-*`.
349
+
350
+ ## WireGuard mesh
351
+
352
+ The mesh is a hub-and-spoke WireGuard network managed alongside the proxy.
353
+
354
+ ```yaml
355
+ mesh:
356
+ enabled: true
357
+ interface: wg0
358
+ address: 10.44.0.1/24
359
+ listen_port: 51820
360
+ endpoint: vpn.home.lan:51820
361
+ dns: 10.44.0.1
362
+ client_allowed_ips: 10.44.0.0/24, 192.168.1.0/24
363
+ config_path: /etc/wireguard/wg0.conf
364
+ ```
365
+
366
+ Create a peer and apply the hub config:
367
+
368
+ ```bash
369
+ warden mesh peer add laptop -o warden-laptop.conf
370
+ warden mesh config > /etc/wireguard/wg0.conf # or inspect it first
371
+ warden mesh apply
372
+ warden mesh status
373
+ ```
374
+
375
+ `warden mesh apply` writes the config, then uses `wg syncconf` if the interface is
376
+ up or `wg-quick up` if it is down. Peers can also be created and downloaded from
377
+ the Mesh page. Warden stores the hub private key and peer private keys in SQLite
378
+ so it can regenerate configs; protect the database accordingly.
379
+
380
+ Requirements:
381
+
382
+ - `wireguard-tools` (`wg` and `wg-quick`) on the host.
383
+ - Permission to create/configure the interface. This normally means root or
384
+ carefully scoped capabilities; `wg-quick` also invokes `ip`, `iptables`, and
385
+ `sysctl`.
386
+ - A UDP port reachable from clients (`listen_port`) and `mesh.endpoint` set to
387
+ the public `host:port` clients should dial.
388
+ - `mesh.config_path` writable by the Warden process.
389
+
390
+ `mesh apply` is manual by design. Peer changes are saved immediately, but the
391
+ interface is only updated when you apply from the dashboard or CLI.
392
+
393
+ ## CLI
394
+
395
+ ```text
396
+ warden [--config PATH] COMMAND
397
+
398
+ serve run the proxy and dashboard
399
+ --host ADDR bind override
400
+ --port PORT HTTP port override
401
+ --https-port PORT HTTPS port override
402
+ --no-access-log hide per-request logs
403
+
404
+ init write a starter warden.yaml
405
+ -o/--output PATH
406
+ --admin-host HOST
407
+ --rp-id DOMAIN
408
+ --bind ADDR
409
+ --port PORT
410
+ --https-port PORT
411
+ --database PATH
412
+ --force
413
+
414
+ check [--json] validate config, TLS files, and runtime paths
415
+
416
+ certs status [--json]
417
+ certs renew [--dry-run]
418
+
419
+ auth reset [--yes] delete the account, passkeys, and sessions
420
+ (restart to generate a new setup code)
421
+
422
+ mesh status
423
+ mesh config
424
+ mesh apply
425
+ mesh peers
426
+ mesh peer add NAME [--address IP] [--endpoint HOST:PORT]
427
+ [--keepalive SECONDS] [-o FILE]
428
+ mesh peer config NAME
429
+ mesh peer remove NAME [--yes]
430
+ ```
431
+
432
+ `WARDEN_CONFIG` sets the config path when `--config` is not given. `warden
433
+ --version` prints the version. `serve` validates TLS files before binding.
434
+
435
+ ## Docker
436
+
437
+ The image runs Warden as an unprivileged user and installs it from the local
438
+ source tree.
439
+
440
+ ```bash
441
+ mkdir -p config
442
+ cp configs/warden.yaml.example config/warden.yaml
443
+ # edit config/warden.yaml: admin_host, rp_id, routes, TLS paths
444
+ docker compose up -d --build
445
+ docker compose logs -f warden
446
+ ```
447
+
448
+ The compose file maps host `8080`/`8443` to the container's default ports and
449
+ keeps the database in a named volume. Edit the mappings and `public_url` if you
450
+ want standard ports. Mount certificates read-only and point `certfile`/`keyfile`
451
+ at the mounted paths.
452
+
453
+ WireGuard is not container-friendly by default. The example keeps it disabled. If
454
+ you enable it in Docker, the container needs host networking, `/dev/net/tun`,
455
+ `NET_ADMIN`, the WireGuard tools, and a writable config path. Running the mesh on
456
+ the host (or in a privileged sidecar) is usually simpler.
457
+
458
+ ## systemd
459
+
460
+ [`deploy/warden.service`](deploy/warden.service) is a starting point for a
461
+ non-mesh deployment. A typical install:
462
+
463
+ ```bash
464
+ sudo useradd --system --home /var/lib/warden --shell /usr/sbin/nologin warden
465
+ sudo install -d -o warden -g warden -m 750 /var/lib/warden /etc/warden
466
+ sudo install -d -o warden -g warden -m 755 /var/www/warden-acme
467
+
468
+ sudo python3 -m venv /opt/warden
469
+ sudo /opt/warden/bin/pip install /path/to/warden
470
+ sudo install -m 644 deploy/warden.service /etc/systemd/system/warden.service
471
+
472
+ sudo systemctl daemon-reload
473
+ sudo systemctl enable --now warden
474
+ sudo journalctl -u warden -f
475
+ ```
476
+
477
+ The unit grants `CAP_NET_BIND_SERVICE` for ports below 1024 and uses systemd
478
+ sandboxing. Adjust `ExecStart`, the config path, and `ReadWritePaths` to match
479
+ your install. The unit deliberately targets proxy mode; a process that manages a
480
+ WireGuard interface needs broader privileges than the sandbox allows.
481
+
482
+ ## Security notes
483
+
484
+ - Warden is **single-admin**. Additional passkeys attach to the same account.
485
+ There is no password fallback and no user self-registration.
486
+ - Sessions are opaque 256-bit tokens stored as SHA-256 hashes in SQLite. Cookies
487
+ are `HttpOnly`, `SameSite=Lax`, and `Secure` when the request is HTTPS. The
488
+ session cookie never reaches upstream services.
489
+ - Passkey registration and login challenges are stored server-side and consumed
490
+ once. Origin, content type, rate limits, and user verification are checked
491
+ during ceremonies.
492
+ - CSRF protection combines same-origin/`Sec-Fetch-Site` checks with a required
493
+ `X-Warden-Request: 1` header on admin mutations.
494
+ - Upstreams see sanitized `X-Forwarded-For`, `X-Forwarded-Host`,
495
+ `X-Forwarded-Proto`, `X-Real-IP`, and (for authenticated routes) identity
496
+ headers. Client-supplied identity and forwarding headers are stripped.
497
+ - `public_paths` and `auth: false` are deliberate holes. Review them.
498
+ - The SQLite database contains sensitive material (passkey metadata, session
499
+ hashes, WireGuard private keys). Use file permissions and encrypted backups.
500
+ - Keep Warden to a single process. Sessions and rate limits are process-local;
501
+ multiple workers would also fight over the same SQLite file and renewal loop.
502
+ - Run behind a network you trust. Warden's own threat model assumes the admin
503
+ hostname is not exposed on the public internet without TLS.
504
+
505
+ ## OTP (One‑Time Password)
506
+
507
+ Warden supports optional TOTP‑based two‑factor authentication.
508
+
509
+ * **Enable OTP** – An authenticated user can POST to `/_warden/auth/otp/setup`. The endpoint is CSRF‑protected (requires `X-Warden-Request: 1` and same‑origin). It returns a Base32 secret and an `otpauth://` URL that can be scanned by Google Authenticator, Authy, or any TOTP app.
510
+
511
+ * **Verify OTP** – After provisioning, the user POSTs to `/_warden/auth/otp/verify` with the current 6‑digit code. Successful verification increments the `warden_otp_success_total` metric; failures increment `warden_otp_failure_total`.
512
+
513
+ * **Rate limiting** – OTP verification is limited to 30 attempts per minute per IP.
514
+
515
+ * **Metrics** – Both success and failure counters are exposed at `/_warden/metrics` and visualised in the Grafana dashboard.
516
+
517
+ * **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.
518
+
519
+ ## Backup codes UI
520
+
521
+ 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.
522
+
523
+ 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.
524
+
525
+ - `POST /_warden/api/backup_codes` – creates a new backup code (requires an authenticated admin session).
526
+ - `POST /_warden/auth/login/backup` – logs in using a backup code.
527
+
528
+ The metric `warden_backup_code_used_total` records each successful backup‑code login.
529
+
530
+ ## Backup and recovery
531
+
532
+ Back up your configuration, database, and TLS certificates.
533
+
534
+ - the YAML config (contains secrets such as mesh settings and renewal hooks),
535
+ - the SQLite database (use `sqlite3 warden.db ".backup backup.db"` or stop Warden
536
+ first),
537
+ - TLS certificates and keys if not managed by certbot.
538
+
539
+ To recover access when every passkey is lost:
540
+
541
+ ```bash
542
+ warden auth reset --yes
543
+ # restart Warden and use the newly printed setup code
544
+ ```
545
+
546
+ This deletes the admin account, all passkeys, sessions, and first-run settings,
547
+ but leaves the audit log and mesh peers. To start the whole instance over, remove
548
+ the database as well.
549
+
550
+ ## Development
551
+
552
+ ```bash
553
+ python3 -m venv .venv
554
+ . .venv/bin/activate
555
+ pip install -e '.[dev]'
556
+ pytest
557
+ ```
558
+
559
+ The test suite covers config validation, YAML round-tripping, SQLite persistence,
560
+ TLS inspection, mesh configs, authentication ceremonies (with mocked WebAuthn
561
+ verification), HTTP proxy behavior, admin APIs, WebSockets, and the CLI. It does
562
+ not replace a real browser passkey ceremony.
563
+
564
+ To run against a development config:
565
+
566
+ ```bash
567
+ WARDEN_CONFIG=./warden.dev.yaml warden serve --host 127.0.0.1
568
+ ```
569
+
570
+ ## License
571
+
572
+ MIT. See [LICENSE](LICENSE).