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
|
||||
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
|
||||
|
||||
```
|
||||
|
|
|
|||
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