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.
- warden_mesh-0.1.8/LICENSE +21 -0
- warden_mesh-0.1.8/PKG-INFO +572 -0
- warden_mesh-0.1.8/README.md +538 -0
- warden_mesh-0.1.8/pyproject.toml +67 -0
- warden_mesh-0.1.8/setup.cfg +4 -0
- warden_mesh-0.1.8/tests/test_app.py +527 -0
- warden_mesh-0.1.8/tests/test_auth.py +556 -0
- warden_mesh-0.1.8/tests/test_cli.py +454 -0
- warden_mesh-0.1.8/tests/test_cli_parser.py +19 -0
- warden_mesh-0.1.8/tests/test_config.py +211 -0
- warden_mesh-0.1.8/tests/test_db.py +245 -0
- warden_mesh-0.1.8/tests/test_main_helpers.py +92 -0
- warden_mesh-0.1.8/tests/test_mesh.py +273 -0
- warden_mesh-0.1.8/tests/test_otp_backup_metrics.py +92 -0
- warden_mesh-0.1.8/tests/test_rbac.py +46 -0
- warden_mesh-0.1.8/tests/test_tls.py +149 -0
- warden_mesh-0.1.8/tests/test_utils.py +146 -0
- warden_mesh-0.1.8/tests/test_ws.py +216 -0
- warden_mesh-0.1.8/warden/__init__.py +4 -0
- warden_mesh-0.1.8/warden/__main__.py +4 -0
- warden_mesh-0.1.8/warden/admin/routes.py +991 -0
- warden_mesh-0.1.8/warden/asgi.py +11 -0
- warden_mesh-0.1.8/warden/auth/passkeys.py +585 -0
- warden_mesh-0.1.8/warden/auth/routes.py +637 -0
- warden_mesh-0.1.8/warden/auth/sessions.py +176 -0
- warden_mesh-0.1.8/warden/cli.py +851 -0
- warden_mesh-0.1.8/warden/config.py +981 -0
- warden_mesh-0.1.8/warden/db.py +1161 -0
- warden_mesh-0.1.8/warden/i18n.py +41 -0
- warden_mesh-0.1.8/warden/main.py +378 -0
- warden_mesh-0.1.8/warden/mesh.py +440 -0
- warden_mesh-0.1.8/warden/middleware.py +360 -0
- warden_mesh-0.1.8/warden/proxy.py +744 -0
- warden_mesh-0.1.8/warden/stats.py +386 -0
- warden_mesh-0.1.8/warden/templates/activity.html +98 -0
- warden_mesh-0.1.8/warden/templates/auth_base.html +55 -0
- warden_mesh-0.1.8/warden/templates/backup_codes.html +23 -0
- warden_mesh-0.1.8/warden/templates/base.html +71 -0
- warden_mesh-0.1.8/warden/templates/config.html +125 -0
- warden_mesh-0.1.8/warden/templates/dashboard.html +341 -0
- warden_mesh-0.1.8/warden/templates/error.html +37 -0
- warden_mesh-0.1.8/warden/templates/login.html +53 -0
- warden_mesh-0.1.8/warden/templates/mesh.html +245 -0
- warden_mesh-0.1.8/warden/templates/passkeys.html +203 -0
- warden_mesh-0.1.8/warden/templates/roles.html +76 -0
- warden_mesh-0.1.8/warden/templates/setup.html +56 -0
- warden_mesh-0.1.8/warden/templates/static/admin.js +781 -0
- warden_mesh-0.1.8/warden/templates/static/auth.js +96 -0
- warden_mesh-0.1.8/warden/templates/static/favicon.svg +6 -0
- warden_mesh-0.1.8/warden/templates/static/warden.css +1823 -0
- warden_mesh-0.1.8/warden/templates/static/warden.js +367 -0
- warden_mesh-0.1.8/warden/templates/tls.html +24 -0
- warden_mesh-0.1.8/warden/templates/users.html +124 -0
- warden_mesh-0.1.8/warden/templating.py +39 -0
- warden_mesh-0.1.8/warden/tls.py +411 -0
- warden_mesh-0.1.8/warden/utils.py +526 -0
- warden_mesh-0.1.8/warden_mesh.egg-info/PKG-INFO +572 -0
- warden_mesh-0.1.8/warden_mesh.egg-info/SOURCES.txt +60 -0
- warden_mesh-0.1.8/warden_mesh.egg-info/dependency_links.txt +1 -0
- warden_mesh-0.1.8/warden_mesh.egg-info/entry_points.txt +2 -0
- warden_mesh-0.1.8/warden_mesh.egg-info/requires.txt +15 -0
- 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
|
+
[](https://github.com/patpadgett/warden/actions/workflows/auto-tag.yml)
|
|
38
|
+
[](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).
|