Files
curriculum-project-hub/hub/deploy/README.md
T

4.6 KiB

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:

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:

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:

/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:

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.