From 0c461be698a7d966d56a1b2823652778f4a7325c Mon Sep 17 00:00:00 2001 From: Andreas Brenner Date: Thu, 11 Jun 2026 13:57:29 +0300 Subject: [PATCH] ANW-19 deployment: systemd unit and reverse-proxy example --- README.md | 6 +++ deploy/README.md | 58 ++++++++++++++++++++++++++++ deploy/anwesen.service | 79 +++++++++++++++++++++++++++++++++++++++ deploy/nginx.example.conf | 53 ++++++++++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 deploy/README.md create mode 100644 deploy/anwesen.service create mode 100644 deploy/nginx.example.conf diff --git a/README.md b/README.md index f94eb22..c9bc464 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ anwesen version Each flag has a matching `ANWESEN_` environment variable; CLI wins 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 ``` diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..a979258 --- /dev/null +++ b/deploy/README.md @@ -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". diff --git a/deploy/anwesen.service b/deploy/anwesen.service new file mode 100644 index 0000000..a60496d --- /dev/null +++ b/deploy/anwesen.service @@ -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 diff --git a/deploy/nginx.example.conf b/deploy/nginx.example.conf new file mode 100644 index 0000000..9b3d6f1 --- /dev/null +++ b/deploy/nginx.example.conf @@ -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; +}