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