feat: secure organization provider credentials

This commit is contained in:
2026-07-10 22:04:43 +08:00
parent e049cb6880
commit 848f913597
51 changed files with 3280 additions and 230 deletions
+43 -4
View File
@@ -14,13 +14,52 @@ sudo BASE=/srv/curriculum-project-hub \
```
On a clean host the first invocation creates
`/srv/curriculum-project-hub/.secrets/platform.env` with every supported
production key and exits with status 78. Fill every required blank, set
`CPH_BIN` to an executable compatible `cph`, keep the file mode at `0600`, and
run the same command again. No systemd unit is installed until configuration,
`/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 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
+4
View File
@@ -7,6 +7,7 @@ Wants=network-online.target
# this start-time assertion keeps later package/host changes fail-closed.
AssertPathExists=__BWRAP_BIN__
AssertPathExists=__SOCAT_BIN__
AssertPathExists=__KEYRING_FILE__
[Service]
Type=simple
@@ -18,6 +19,9 @@ Environment=HOME=__SERVICE_HOME__
Environment=XDG_STATE_HOME=__STATE_DIR__
Environment=XDG_CACHE_HOME=__CACHE_DIR__
Environment=PATH=__RUNTIME_PATH__
# ADR-0024: the root-owned source remains unreadable by the service account;
# systemd materializes a read-only per-unit credential at runtime.
LoadCredential=cph-secret-keyring:__KEYRING_FILE__
UMask=0027
# Run prisma migrations before each start, then launch the Hub.
ExecStartPre=__NODE_BIN__ __HUB_DIR__/node_modules/prisma/build/index.js migrate deploy --schema __HUB_DIR__/prisma/schema.prisma
+37 -5
View File
@@ -21,6 +21,7 @@ WORKSPACE_ROOT="${WORKSPACE_ROOT:-/var/lib/cph-hub/workspaces}"
HOST="${HOST:-127.0.0.1}"
PORT="${PORT:-8788}"
ENV_FILE="${ENV_FILE:-$BASE/.secrets/platform.env}"
KEYRING_FILE="${KEYRING_FILE:-$BASE/.secrets/secret-keyring.json}"
SYSTEMD_UNIT_DIR="${SYSTEMD_UNIT_DIR:-/etc/systemd/system}"
NODE_BIN="${NODE_BIN:-$(command -v node || true)}"
BWRAP_BIN="${BWRAP_BIN:-$(command -v bwrap || true)}"
@@ -63,7 +64,7 @@ if [ "$SERVICE_USER" = "root" ] || [ "$SERVICE_GROUP" = "root" ]; then
fail "the Hub service identity must not be root"
fi
for command in getent groupadd useradd install stat systemctl id; do
for command in getent groupadd useradd install stat systemctl id openssl date mktemp; do
require_command "$command"
done
for pair in \
@@ -97,6 +98,7 @@ for pair in \
"CACHE_DIR:$CACHE_DIR" \
"WORKSPACE_ROOT:$WORKSPACE_ROOT" \
"ENV_FILE:$ENV_FILE" \
"KEYRING_FILE:$KEYRING_FILE" \
"SYSTEMD_UNIT_DIR:$SYSTEMD_UNIT_DIR" \
"NODE_BIN:$NODE_BIN" \
"BWRAP_BIN:$BWRAP_BIN" \
@@ -107,6 +109,32 @@ for pair in \
require_safe_unit_value "${pair%%:*}" "${pair#*:}"
done
KEYRING_CREATED=false
if [ -L "$KEYRING_FILE" ]; then
fail "local secret keyring must not be a symlink: $KEYRING_FILE"
fi
if [ ! -e "$KEYRING_FILE" ]; then
install -d -o root -g root -m 0700 "$(dirname "$KEYRING_FILE")"
key_id="local-$(date -u +%Y%m%dT%H%M%SZ)"
key_value="$(openssl rand -base64 32)"
tmp_keyring="$(mktemp "$(dirname "$KEYRING_FILE")/.secret-keyring.XXXXXX")"
trap 'rm -f "${tmp_keyring:-}"' EXIT
chmod 0600 "$tmp_keyring"
printf '{"version":1,"activeKeyId":"%s","keys":{"%s":"%s"}}\n' \
"$key_id" "$key_id" "$key_value" >"$tmp_keyring"
install -o root -g root -m 0600 "$tmp_keyring" "$KEYRING_FILE"
rm -f "$tmp_keyring"
tmp_keyring=""
trap - EXIT
unset key_value
KEYRING_CREATED=true
echo "[install] generated local secret keyring: $KEYRING_FILE" >&2
fi
[ -f "$KEYRING_FILE" ] || fail "local secret keyring is not a regular file: $KEYRING_FILE"
keyring_metadata="$(stat -c '%u:%g:%a' -- "$KEYRING_FILE")"
[ "$keyring_metadata" = "0:0:600" ] || \
fail "local secret keyring must be owned by root:root with mode 0600; found $keyring_metadata"
if [ -L "$ENV_FILE" ]; then
fail "environment file must not be a symlink: $ENV_FILE"
fi
@@ -118,9 +146,6 @@ if [ ! -e "$ENV_FILE" ]; then
# rerunning install_service.sh; failed preflight never installs the unit.
NODE_ENV=production
DATABASE_URL=
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=
ANTHROPIC_API_KEY=
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_BOT_OPEN_ID=
@@ -132,18 +157,24 @@ HUB_PUBLIC_BASE_URL=
HUB_SESSION_SECRET=
HUB_AGENT_MAX_TURNS=25
HUB_FEISHU_LISTENER_ENABLED=true
CPH_SANDBOX_EXTRA_DENY_READ=$ENV_FILE
CPH_SANDBOX_EXTRA_DENY_READ=$ENV_FILE:$KEYRING_FILE
EOF
chmod 0600 "$ENV_FILE"
echo "[install] seeded complete production configuration: $ENV_FILE" >&2
echo "[install] copy $KEYRING_FILE to the separately protected keyring backup before continuing" >&2
echo "[install] fill every required blank, install cph at CPH_BIN, then rerun" >&2
exit 78
fi
[ -f "$ENV_FILE" ] || fail "environment file is not a regular file: $ENV_FILE"
if [ "$KEYRING_CREATED" = true ]; then
echo "[install] copy $KEYRING_FILE to the separately protected keyring backup, then rerun" >&2
exit 78
fi
PREFLIGHT_ARGS=(
"$NODE_BIN" "$PREFLIGHT_JS"
--env-file "$ENV_FILE"
--keyring-file "$KEYRING_FILE"
--node-bin "$NODE_BIN"
--runuser-bin "$RUNUSER_BIN"
--setpriv-bin "$SETPRIV_BIN"
@@ -249,6 +280,7 @@ sed \
-e "s|__WORKSPACE_ROOT__|$WORKSPACE_ROOT|g" \
-e "s|__HUB_DIR__|$HUB_DIR|g" \
-e "s|__ENV_FILE__|$ENV_FILE|g" \
-e "s|__KEYRING_FILE__|$KEYRING_FILE|g" \
-e "s|__NODE_BIN__|$NODE_BIN|g" \
-e "s|__BWRAP_BIN__|$BWRAP_BIN|g" \
-e "s|__SOCAT_BIN__|$SOCAT_BIN|g" \