ANW-19 deployment: systemd unit and reverse-proxy example

This commit is contained in:
Andreas Brenner 2026-06-11 13:57:29 +03:00
parent 44d6bb995e
commit 0c461be698
4 changed files with 196 additions and 0 deletions

View file

@ -18,6 +18,12 @@ anwesen version
Each flag has a matching `ANWESEN_<UPPER>` environment variable; CLI wins Each flag has a matching `ANWESEN_<UPPER>` environment variable; CLI wins
over env. over env.
## Deployment
A systemd unit and an example reverse-proxy config live in [`deploy/`](deploy/).
Anwesen binds `127.0.0.1` and ships no authentication -- the proxy is the
access boundary. See [`deploy/README.md`](deploy/README.md).
## Development ## Development
``` ```

58
deploy/README.md Normal file
View file

@ -0,0 +1,58 @@
# Deploying Anwesen
Anwesen is a single read-only daemon. It binds `127.0.0.1:8080` by default and
trusts every request it accepts -- access control lives in front of it, not
inside it ([ADR-007]). A typical host runs the `anwesen.service` systemd unit
and an nginx (or caddy, or warpgate) reverse proxy as the network boundary.
## systemd
[`anwesen.service`](anwesen.service) runs the daemon as a dedicated `anwesen`
system user, restarts it on failure, and routes its structured stderr to the
journal.
```
cp target/release/anwesen /usr/local/bin/anwesen
useradd --system --no-create-home --shell /usr/sbin/nologin anwesen
cp deploy/anwesen.service /etc/systemd/system/
systemctl edit anwesen.service # set the real vault path, see below
systemctl enable --now anwesen.service
journalctl -u anwesen -f
```
Point the unit at your vault with a drop-in (`systemctl edit anwesen.service`)
rather than editing the shipped unit:
```ini
[Service]
Environment=ANWESEN_VAULT=/srv/vault
ReadOnlyPaths=/srv/vault
```
The `anwesen` user needs read access to the vault and traverse (`x`) on its
directories -- grant it via group membership or directory permissions. Anwesen
never writes to the vault; the unit sets `ProtectSystem=strict` with no
writable paths, so any write attempt fails outright.
The unit drains in-flight requests on SIGTERM (systemd's default stop signal)
and is hardened for a service that writes nowhere and needs no privileges.
## Reverse proxy
[`nginx.example.conf`](nginx.example.conf) terminates TLS, routes by host, and
authenticates the client before any request reaches Anwesen. It offers HTTP
basic auth out of the box with mutual-TLS as a commented alternative. Adjust
`server_name`, the certificate paths, and the auth block, then reload nginx.
Anwesen never sees the proxy's auth: the proxy authenticates the client and
forwards to `127.0.0.1:8080`. Per [ADR-007] this is deliberate -- "if you reach
Anwesen, you may read everything it indexes."
### Warpgate
For off-host access the operator's reference pattern is warpgate ticketing:
a ticket-bearing client reaches warpgate, which forwards to Anwesen on
`localhost`. Anwesen does not see the ticket and needs no configuration for it
-- it is just another reverse proxy in front of the localhost bind.
[ADR-007]: the project's design vault, "ADR-007 Authentication Out of Scope".

79
deploy/anwesen.service Normal file
View file

@ -0,0 +1,79 @@
# Anwesen: read-only HTTP daemon over a markdown vault.
#
# Install:
# cp target/release/anwesen /usr/local/bin/anwesen
# useradd --system --no-create-home --shell /usr/sbin/nologin anwesen
# cp deploy/anwesen.service /etc/systemd/system/
# systemctl edit anwesen.service # set the real vault path (see below)
# systemctl enable --now anwesen.service
# journalctl -u anwesen -f # structured logs land here
#
# Set the vault path and bind address in a drop-in rather than editing this
# unit, so package updates do not clobber local config:
#
# [Service]
# Environment=ANWESEN_VAULT=/srv/vault
# ReadOnlyPaths=/srv/vault
#
# The vault must be readable (and its directories traversable) by the anwesen
# user -- grant it via group membership or directory permissions. Anwesen
# never writes to the vault; ProtectSystem=strict below makes that structural.
[Unit]
Description=Anwesen read-only HTTP daemon over a markdown vault
Documentation=https://forge.crvrs.org/carvers/anwesen
After=network.target
[Service]
Type=exec
User=anwesen
Group=anwesen
# Defaults; override ANWESEN_VAULT (and ReadOnlyPaths) in a drop-in. Anwesen
# binds 127.0.0.1 by default and trusts every request it accepts -- front it
# with a reverse proxy for any off-host access (ADR-007, see nginx.example.conf).
Environment=ANWESEN_VAULT=/srv/vault
Environment=ANWESEN_BIND=127.0.0.1:8080
Environment=ANWESEN_LOG_LEVEL=info
ExecStart=/usr/local/bin/anwesen serve
Restart=on-failure
RestartSec=2s
# The daemon drains in-flight requests on SIGTERM (systemd's default stop
# signal). Give it room before SIGKILL.
TimeoutStopSec=15s
# Structured tracing is written to stderr; route both streams to journald.
StandardOutput=journal
StandardError=journal
SyslogIdentifier=anwesen
# --- Hardening: a read-only daemon that writes nowhere ---
# ProtectSystem=strict makes the whole filesystem read-only to the service
# (no ReadWritePaths), so the vault is readable but nothing is writable.
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target

53
deploy/nginx.example.conf Normal file
View file

@ -0,0 +1,53 @@
# Example nginx reverse proxy in front of Anwesen.
#
# Anwesen binds 127.0.0.1:8080 and ships no authentication: if a request
# reaches it, it may read everything indexed (ADR-007). This proxy is the
# access boundary -- it terminates TLS, routes by host, and authenticates the
# client before anything reaches the daemon.
#
# Drop into /etc/nginx/sites-available/ (or conf.d/) and adjust the marked
# values: server_name, certificate paths, and the auth block.
upstream anwesen {
server 127.0.0.1:8080;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# --- Host-based routing: only this name reaches Anwesen ---
server_name vault.example.org;
# --- TLS termination ---
ssl_certificate /etc/ssl/anwesen/fullchain.pem;
ssl_certificate_key /etc/ssl/anwesen/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# --- Client authentication ---
# Pick one. HTTP basic auth is the simplest:
auth_basic "Anwesen";
auth_basic_user_file /etc/nginx/anwesen.htpasswd; # htpasswd -c ... anwesen
#
# For mutual TLS instead, drop the two auth_basic lines and use:
# ssl_client_certificate /etc/ssl/anwesen/clients-ca.pem;
# ssl_verify_client on;
location / {
proxy_pass http://anwesen;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Anwesen serves strong ETags and honors If-None-Match; nginx forwards
# both unchanged, so conditional requests keep working through the proxy.
}
}
# Redirect plain HTTP to HTTPS.
server {
listen 80;
listen [::]:80;
server_name vault.example.org;
return 301 https://$host$request_uri;
}