forked from bai/curriculum-project-hub
102 lines
4.6 KiB
Markdown
102 lines
4.6 KiB
Markdown
# Hub service installation
|
|
|
|
The initial production target is a supported Linux host with systemd,
|
|
PostgreSQL, Node.js 20 or newer, `pg_isready`, `runuser`, `setpriv`, bubblewrap,
|
|
`socat` and `cph`. The Hub runs as the dedicated non-login `cph-hub` user; do not
|
|
create or run the unit as root.
|
|
|
|
Build or deploy the Hub under `/srv/curriculum-project-hub`, then run:
|
|
|
|
```sh
|
|
sudo BASE=/srv/curriculum-project-hub \
|
|
HUB_DIR=/srv/curriculum-project-hub/hub \
|
|
bash /srv/curriculum-project-hub/hub/deploy/install_service.sh
|
|
```
|
|
|
|
On a clean host the first invocation creates
|
|
`/srv/curriculum-project-hub/.secrets/platform.env` and a random root-owned
|
|
`secret-keyring.json`, then exits with status 78. Copy the keyring to a
|
|
separately protected recovery location before continuing; it must not live only
|
|
on this host or in the ordinary database/workspace backup. Fill every required
|
|
environment blank, set `CPH_BIN` to an executable compatible `cph`, keep both
|
|
source files root-only at mode `0600`, and run the same command again. No
|
|
systemd unit is installed until configuration,
|
|
paths, Node, bubblewrap, `socat`, `cph --version`, PostgreSQL connectivity and directory
|
|
ownership all pass preflight.
|
|
|
|
The service account cannot read the keyring source. The installed unit uses
|
|
systemd `LoadCredential` to materialize a read-only `cph-secret-keyring` for the
|
|
running service. Hub refuses production startup if this credential is missing,
|
|
malformed, or does not contain its declared active key. Provider credentials
|
|
are configured through the Organization admin connection API, never through
|
|
process-global `ANTHROPIC_*` variables; preflight rejects those legacy settings.
|
|
|
|
Rotate the local KEK only from the controlled host console. The command first
|
|
atomically adds a new active key while retaining every previous key, then
|
|
authenticates and rewraps every stored Provider and Feishu Application DEK,
|
|
writes redacted org audit events, and verifies the complete set again:
|
|
|
|
```sh
|
|
sudo bash -c '
|
|
set -euo pipefail
|
|
systemctl stop cph-hub.service
|
|
set -a
|
|
. /srv/curriculum-project-hub/.secrets/platform.env
|
|
set +a
|
|
node /srv/curriculum-project-hub/hub/dist/deployment/rotate-secret-kek.js \
|
|
--keyring-file /srv/curriculum-project-hub/.secrets/secret-keyring.json
|
|
systemctl start cph-hub.service
|
|
'
|
|
```
|
|
|
|
The CLI refuses to rotate while `cph-hub.service` is active; stopping the only
|
|
writer prevents a concurrent BYOK version from being appended under the old
|
|
in-memory keyring. A PostgreSQL transaction advisory lock excludes a second
|
|
rotation and is released automatically if the CLI or host crashes. The
|
|
operation is therefore rerunnable after interruption: old and new keys remain
|
|
in the atomically replaced keyring, so partially rewrapped rows stay
|
|
decryptable. It also refuses success while any envelope still names a
|
|
non-active KEK. Back up the updated keyring to the separate recovery location
|
|
and complete a restore preflight before considering old-key retirement. The
|
|
pilot deliberately does not auto-delete previous keys.
|
|
|
|
The deployment path also runs `npm run audit:production` from the locked clean
|
|
install before building or restarting the service. A current high or critical
|
|
production advisory therefore blocks deployment instead of becoming a warning
|
|
buried in CI output.
|
|
|
|
An existing service account is accepted only when its primary group, home and
|
|
non-login shell match the requested configuration and it has no supplementary
|
|
groups. After provisioning, the installer executes path-access, built Hub,
|
|
Prisma, an authenticated database query, `cph`, `socat` and bubblewrap namespace probes
|
|
as that account with `no_new_privs` set. Inaccessible parents, UID-specific
|
|
database/network policy, or a host that forbids unprivileged user namespaces
|
|
therefore fail before unit installation.
|
|
|
|
The default persistent layout is deliberately outside every source/release
|
|
tree:
|
|
|
|
```text
|
|
/var/lib/cph-hub/home
|
|
/var/lib/cph-hub/state
|
|
/var/lib/cph-hub/workspaces
|
|
/var/cache/cph-hub
|
|
```
|
|
|
|
Custom paths are supported with `SERVICE_HOME`, `STATE_DIR`, `CACHE_DIR` and
|
|
`WORKSPACE_ROOT`, but all must be absolute. The installer rejects any persistent
|
|
path that is equal to, above, or below `BASE`; this keeps rsync, release
|
|
switching, rollback and pruning unable to traverse customer workspaces. Set the
|
|
same custom workspace in `HUB_PROJECT_WORKSPACE_ROOT` inside `platform.env`.
|
|
|
|
After a successful installation:
|
|
|
|
```sh
|
|
sudo systemctl start cph-hub.service
|
|
sudo systemctl status cph-hub.service
|
|
```
|
|
|
|
`HOST` and `PORT` in `platform.env` are passed to the real HTTP listener. The
|
|
default `127.0.0.1:8788` expects a local TLS reverse proxy; the public OAuth URL
|
|
must be an HTTPS `HUB_PUBLIC_BASE_URL`.
|