@@ -23,7 +23,8 @@ when the CLI starts, so reconnects keep the original time) shown in the
2323viewer's local timezone.
2424
2525Write-capable ` /source ` , ` /edit ` , and ` /batch ` routes can be protected with a
26- shared API key via the ` EDG_ACKNOWLEDG_API_KEY ` environment variable.
26+ shared API key via the ` EDG_ACKNOWLEDG_API_KEY ` environment variable or, for
27+ deployed servers, a systemd credential (see [ Server] ( #server ) ).
2728` edg-acknowledg-cli ` multiplexes many sections over ` /batch ` from a JSON
2829specification; ` edgy-review ` and ` edg-bench-review ` remain available as
2930task-focused CLIs that use the direct ` /source ` and ` /edit ` endpoints.
@@ -88,4 +89,106 @@ a reverse proxy. For Apache, this looks something like the following (with
8889</VirtualHost>
8990```
9091
92+ Hypercorn itself is best run as a systemd service. A unit along these lines
93+ (saved as e.g. ` /etc/systemd/system/acknowledg-api.service ` , with ` <USER> ` and
94+ ` <SERVER PATH> ` replaced, where ` <SERVER PATH>/venv ` has the server wheel
95+ installed) keeps it running and restarts it on failure:
96+
97+ ```
98+ [Unit]
99+ Description=AcknowlEDG API Server
100+ After=network-online.target
101+ Wants=network-online.target
102+
103+ [Service]
104+ Type=exec
105+ User=<USER>
106+ WorkingDirectory=<SERVER PATH>/wdir
107+ # Optional API key protecting the write routes (see below).
108+ LoadCredentialEncrypted=api-key:/etc/credstore.encrypted/acknowledg-api.api-key
109+ ExecStart=<SERVER PATH>/venv/bin/hypercorn --bind 127.0.0.1:2727 acknowledg_server:app
110+ Restart=on-failure
111+ RestartSec=5
112+
113+ # Review state is held in memory, so the server needs no writable paths.
114+ NoNewPrivileges=yes
115+ PrivateTmp=yes
116+ ProtectSystem=strict
117+ ProtectHome=read-only
118+
119+ [Install]
120+ WantedBy=multi-user.target
121+ ```
122+
123+ The API key is optional: without one, the write routes accept any client, which
124+ is only appropriate when the API host is reachable solely by trusted users.
125+ When a key is wanted, supply it as a [ systemd credential] [ credentials ] rather
126+ than an environment variable: systemd decrypts it at service start into a
127+ private directory readable only by the service, and the server reads it from
128+ ` $CREDENTIALS_DIRECTORY/api-key ` (falling back to ` EDG_ACKNOWLEDG_API_KEY ` when
129+ no credential is present). On disk the key is encrypted with the host's key
130+ (and TPM2, when available), so the file is useless if copied elsewhere, and
131+ neither the deploy user nor the service user can read it. Create it once as
132+ root (requires systemd 250 or later):
133+
134+ ```
135+ mkdir -p /etc/credstore.encrypted
136+ systemd-ask-password -n 'AcknowlEDG API key:' \
137+ | systemd-creds encrypt --name=api-key - \
138+ /etc/credstore.encrypted/acknowledg-api.api-key
139+ ```
140+
141+ As written, ` LoadCredentialEncrypted= ` makes systemd refuse to start the
142+ service if the credential file is missing, so a host meant to require a key
143+ fails loudly rather than quietly running without authentication. To run without
144+ a key, omit the ` LoadCredentialEncrypted= ` line. To rotate the key, re-run the
145+ command above and restart the service. On hosts with systemd 247–249, use
146+ ` LoadCredential= ` with a plain file instead (` root:root ` , mode ` 0600 ` ), which
147+ keeps the same isolation from the service and deploy users but stores the key
148+ unencrypted.
149+
150+ [ credentials ] : https://systemd.io/CREDENTIALS/
151+
152+ Enable the service with ` systemctl daemon-reload ` and `systemctl enable --now
153+ acknowledg-api`. Review sessions live only in the server's memory, so
154+ restarting the service drops any open reviews.
155+
91156Locally, ` server/run-server.sh ` starts Hypercorn on ` 127.0.0.1:2727 ` .
157+
158+ ### Automated Deployment
159+
160+ ` .github/workflows/acknowledg-deploy.yml ` builds and deploys both halves over
161+ SSH on pushes to ` main ` that touch AcknowlEDG. The client is rsynced into the
162+ web root (no restart needed). The server wheel is uploaded and handed to
163+ ` server/deploy-remote.sh ` , which installs it and restarts the systemd unit
164+ ** only** when the wheel's contents differ from the last successful deploy
165+ (tracked in ` <server path>/deployed.fingerprint ` ). The last few deployed
166+ wheels are kept under ` <server path>/wheels/ ` for manual rollback.
167+
168+ The workflow reads these secrets from the ` acknowledg ` GitHub environment:
169+
170+ | Secret | Purpose |
171+ | --- | --- |
172+ | ` ACKNOWLEDG_SSH_HOST ` | Host name or IP address |
173+ | ` ACKNOWLEDG_SSH_USER ` | Deploy user |
174+ | ` ACKNOWLEDG_SSH_PORT ` | SSH port (optional; defaults to 22) |
175+ | ` ACKNOWLEDG_SSH_PRIVATE_KEY ` | Private key for the deploy user |
176+ | ` ACKNOWLEDG_SSH_KNOWN_HOSTS ` | ` ssh-keyscan ` output for the host |
177+ | ` ACKNOWLEDG_CLIENT_PATH ` | Web root for the client files |
178+ | ` ACKNOWLEDG_SERVER_PATH ` | Server directory containing ` venv/ ` |
179+ | ` ACKNOWLEDG_SERVICE_NAME ` | systemd unit running the server (e.g. ` acknowledg-api ` ) |
180+ | ` ACKNOWLEDG_API_HOST ` | API URL baked into the client (` VITE_EDG_ACKNOWLEDG_API_HOST ` ), including the scheme, e.g. ` wss://acknowledg-api.example.com ` |
181+
182+ The host must already be set up; the workflow does not manage the unit file:
183+
184+ - The deploy user's ` authorized_keys ` holds the public key, ` rsync ` is
185+ installed, and the user owns the client and server paths.
186+ - ` <server path>/venv ` is a Python 3.11+ venv, and the unit's ` ExecStart `
187+ runs Hypercorn from it (see the example unit under [ Server] ( #server ) ).
188+ - The deploy user may restart the unit without a password, e.g. in
189+ ` /etc/sudoers.d/acknowledg ` . sudo matches arguments exactly, so the unit
190+ name here must be spelled as in ` ACKNOWLEDG_SERVICE_NAME ` :
191+
192+ ```
193+ <user> ALL=(root) NOPASSWD: /usr/bin/systemctl restart acknowledg-api
194+ ```
0 commit comments