ANW-19 deployment: systemd unit and reverse-proxy example
This commit is contained in:
parent
44d6bb995e
commit
0c461be698
4 changed files with 196 additions and 0 deletions
|
|
@ -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
58
deploy/README.md
Normal 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
79
deploy/anwesen.service
Normal 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
53
deploy/nginx.example.conf
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue