feat(relay): Phase1 waves A2-E — server entry, shared-store data plane, agent runtime, deploy artifacts
RELAY-PHASE1 Wave A2/B/C/D/E (12-agent workflow, all tsc-clean, 314/314 tests pass): - A2: control-plane server.ts entry + boot/redis.ts revocation-bus wiring + start script. - B1: relay-run shared-store EnforceDeps (relay-auth ports over the SAME Postgres+Redis as P3). - B2: registry-backed MtlsVerifier (verifyAgentCert, fail-closed, INV14). - B3: store-backed RouteResolver (subdomain->hostId). - B4: Redis relay:revocations subscriber -> tunnel teardown (INV12). - B5: main-phase1.ts production entry (public bind, real TLS, async-mTLS prefetch bridge) + staging /auth/mint. - B6 (PARTIAL): relay-web operator login + browser DPoP; proof offered via term.dpop.<b64u> subprotocol. - C: agent dist/cli.js build (esbuild) + runTunnel run-loop + CliDeps. - D1: same-origin static serve of relay-web/public from the browser WSS. - E: systemd units + gen-ca/gen-capability-key/issue-tls-cert scripts + deploy/RUNBOOK.md. Adversarial review: all hard invariants PASS. Follow-ups (B7): close DPoP-subprotocol read on browser-server (blocks browser connect); rate-limit /auth/mint (F1); wire activeSessionCount (F2); scrub error logs (F5). Excludes unrelated public/style.css (concurrent iOS job).
This commit is contained in:
277
deploy/RUNBOOK.md
Normal file
277
deploy/RUNBOOK.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# RUNBOOK — deploy the rendezvous-relay to VPS `8.138.1.192` (Alibaba Cloud, mainland)
|
||||
|
||||
> **Staging, durable single-tenant** (PLAN_RELAY_PHASE1 §0). Real Postgres + Redis (Docker Compose on
|
||||
> the VPS), real Let's Encrypt TLS on `:443` against an **already ICP-filed** domain, agent dials OUT
|
||||
> from the operator's laptop. Dev in-process CA signer is accepted for staging (KMS → Phase 2).
|
||||
>
|
||||
> Everything below is a **staging template** — replace every `<placeholder>` and confirm each path on
|
||||
> the box. Config is **env-only**; no host/port/secret is hardcoded in code or units.
|
||||
|
||||
Two independent trust chains — never cross-wire them:
|
||||
|
||||
| Chain | Issued by | Protects | Script |
|
||||
|---|---|---|---|
|
||||
| **Public web PKI** | Let's Encrypt | browser `:443` WSS (`TLS_CERT_PATH`) | `issue-tls-cert.sh` |
|
||||
| **Private enrollment CA** | your offline root | agent mTLS + relay agent-server cert (`AGENT_CA_*`) | `gen-agent-ca.sh` |
|
||||
|
||||
And one shared key: the **P5 capability keypair** — control-plane **signs**, relay + admin API
|
||||
**verify** (`gen-capability-key.sh`). `CAPABILITY_SIGN_PUBKEY_B64` (CP) and `RELAY_AUTH_VERIFY_PUBKEY`
|
||||
(relay) are the **same public key**, two encodings.
|
||||
|
||||
---
|
||||
|
||||
## 0. Prerequisites (on `8.138.1.192`)
|
||||
|
||||
```bash
|
||||
# Docker Engine + Compose plugin
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo systemctl enable --now docker
|
||||
|
||||
# Node 20 (satisfies control-plane>=18, relay-run>=20, agent>=18) via NodeSource
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
||||
sudo apt-get install -y nodejs git openssl
|
||||
|
||||
# Clone (this runbook assumes /opt/web-terminal; adjust the systemd --prefix if you change it)
|
||||
sudo git clone <REPO_URL> /opt/web-terminal
|
||||
cd /opt/web-terminal
|
||||
npm ci # root deps
|
||||
npm --prefix control-plane ci
|
||||
npm --prefix relay-run ci
|
||||
npm --prefix relay-web ci
|
||||
|
||||
# Dedicated non-root service user + secret dir
|
||||
sudo useradd --system --home /opt/web-terminal relay || true
|
||||
sudo mkdir -p /etc/relay
|
||||
```
|
||||
|
||||
**DNS (do this first — LE HTTP-01 and the browser both need it):** create an A-record
|
||||
`<SUBDOMAIN>.<BASE_DOMAIN>` → `8.138.1.192`. The domain must be **ICP-filed** (mainland Aliyun blocks
|
||||
`:80/:443` on unfiled domains; if unfiled, you must use DNS-01 in step 2).
|
||||
|
||||
---
|
||||
|
||||
## 1. Bring up Postgres + Redis (Docker Compose, loopback-only)
|
||||
|
||||
```bash
|
||||
cd /opt/web-terminal
|
||||
cp deploy/.env.example deploy/.env # gitignored; fill POSTGRES_PASSWORD (strong random)
|
||||
docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d
|
||||
docker compose -f deploy/docker-compose.yml ps # both healthy; bound to 127.0.0.1 only
|
||||
```
|
||||
|
||||
Both services publish to `127.0.0.1` only — never the public interface (they are the relay's private
|
||||
state).
|
||||
|
||||
---
|
||||
|
||||
## 2. Generate the CA, the capability key, and the TLS cert
|
||||
|
||||
```bash
|
||||
# (a) PRIVATE enrollment CA (agent mTLS) — NOT Let's Encrypt.
|
||||
sudo AGENT_FACING_HOST="<SUBDOMAIN>.<BASE_DOMAIN>" \
|
||||
AGENT_FACING_IP="8.138.1.192" \
|
||||
RELAY_TRUST_DOMAIN="<RELAY_TRUST_DOMAIN>" \
|
||||
bash deploy/scripts/gen-agent-ca.sh
|
||||
# writes /etc/relay/ca/{root,intermediate,agent-ca.*,relay-agent-server.*}
|
||||
|
||||
# (b) P5 capability keypair — prints BOTH public-key encodings; private key -> 0600 file.
|
||||
sudo bash deploy/scripts/gen-capability-key.sh
|
||||
# -> note CAPABILITY_SIGN_PUBKEY_B64 (base64) and RELAY_AUTH_VERIFY_PUBKEY (base64url); same key.
|
||||
|
||||
# (c) PUBLIC TLS cert for the browser :443 (Let's Encrypt).
|
||||
# http-01 needs inbound :80 open DURING issuance (step 4); dns-01 needs no inbound port.
|
||||
sudo BASE_DOMAIN="<BASE_DOMAIN>" SUBDOMAIN="<SUBDOMAIN>" ACME_EMAIL="<you@example.com>" \
|
||||
ACME_METHOD="http-01" \
|
||||
TLS_CERT_PATH="/etc/relay/tls/fullchain.pem" TLS_KEY_PATH="/etc/relay/tls/privkey.pem" \
|
||||
bash deploy/scripts/issue-tls-cert.sh
|
||||
|
||||
sudo chown -R relay:relay /etc/relay
|
||||
```
|
||||
|
||||
> **Staging CA seam (A-wave, not this task):** the control-plane's dev in-process signer must sign
|
||||
> agent leaves with `/etc/relay/ca/intermediate.key.pem` so they chain to `agent-ca.bundle.pem`.
|
||||
> `CA_INTERMEDIATE_KMS_KEY_REF=dev-local` selects the dev signer; point it at that key. Phase 2 = KMS.
|
||||
> After first boot, move `/etc/relay/ca/root.key.pem` **off** the host (offline anchor).
|
||||
|
||||
---
|
||||
|
||||
## 3. Fill the two env files
|
||||
|
||||
Copy the relevant blocks of `deploy/.env.example` into two 0600 files. **The linchpin:**
|
||||
`CAPABILITY_SIGN_PUBKEY_B64` (control-plane) and `RELAY_AUTH_VERIFY_PUBKEY` (relay) are the SAME key
|
||||
from step 2b — paste both.
|
||||
|
||||
```bash
|
||||
sudo install -m 600 -o relay -g relay /dev/null /etc/relay/control-plane.env
|
||||
sudo install -m 600 -o relay -g relay /dev/null /etc/relay/relay.env
|
||||
```
|
||||
|
||||
`/etc/relay/control-plane.env` (P3):
|
||||
|
||||
```ini
|
||||
PG_URL=postgres://relay:<POSTGRES_PASSWORD>@127.0.0.1:5432/relay
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
CAPABILITY_SIGN_PUBKEY_B64=<base64 from step 2b>
|
||||
CA_INTERMEDIATE_KMS_KEY_REF=dev-local
|
||||
CA_INTERMEDIATE_CERT_PATH=/etc/relay/ca/intermediate.cert.pem
|
||||
NODE_MTLS_TRUST_BUNDLE_PATH=/etc/relay/ca/agent-ca.bundle.pem
|
||||
BASE_DOMAIN=<BASE_DOMAIN>
|
||||
CP_BIND_HOST=127.0.0.1
|
||||
CP_BIND_PORT=8080
|
||||
# HEARTBEAT_TTL_SEC / PAIRING_TTL_SEC / PAIRING_MAX_REDEEM_ATTEMPTS use defaults if unset
|
||||
```
|
||||
|
||||
`/etc/relay/relay.env` (P1 data-plane):
|
||||
|
||||
```ini
|
||||
BASE_DOMAIN=<BASE_DOMAIN>
|
||||
BIND_HOST=0.0.0.0
|
||||
BIND_PORT=443
|
||||
TLS_CERT_PATH=/etc/relay/tls/fullchain.pem
|
||||
TLS_KEY_PATH=/etc/relay/tls/privkey.pem
|
||||
AGENT_BIND_PORT=8444
|
||||
AGENT_CA_CERT_PATH=/etc/relay/ca/agent-ca.cert.pem
|
||||
AGENT_CA_CHAIN_PATH=/etc/relay/ca/agent-ca.bundle.pem
|
||||
RELAY_NODE_ID=relay-1
|
||||
RELAY_AUTH_VERIFY_PUBKEY=<base64url from step 2b — SAME key as CAPABILITY_SIGN_PUBKEY_B64>
|
||||
RELAY_TRUST_DOMAIN=<RELAY_TRUST_DOMAIN>
|
||||
PG_URL=postgres://relay:<POSTGRES_PASSWORD>@127.0.0.1:5432/relay
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Open the Aliyun security group (inbound)
|
||||
|
||||
Open **only** these, in the ECS instance's security group:
|
||||
|
||||
| Port | Who | Note |
|
||||
|---|---|---|
|
||||
| `443/tcp` | browsers (WSS) | permanent |
|
||||
| `8444/tcp` (`AGENT_BIND_PORT`) | agents (mTLS) | permanent |
|
||||
| `80/tcp` | Let's Encrypt HTTP-01 | **temporary** — only during cert issue/renew; skip entirely if using DNS-01 |
|
||||
|
||||
**Keep loopback-only (never open):** Postgres `5432`, Redis `6379`, control-plane admin
|
||||
`8080`. Reach the admin API from your laptop via SSH tunnel: `ssh -L 8080:127.0.0.1:8080 root@8.138.1.192`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Build the browser bundle
|
||||
|
||||
```bash
|
||||
cd /opt/web-terminal
|
||||
npm --prefix relay-web run build # emits relay-web/public/build/, served same-origin by D1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Enable + start both units
|
||||
|
||||
```bash
|
||||
sudo cp deploy/systemd/relay-control-plane.service deploy/systemd/relay-data-plane.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now relay-control-plane.service # migrates PG, listens on 127.0.0.1:8080
|
||||
sudo systemctl enable --now relay-data-plane.service # :443 browser WSS + :8444 agent mTLS
|
||||
sudo systemctl status relay-control-plane.service relay-data-plane.service
|
||||
journalctl -u relay-data-plane.service -f # watch for bind + verify-key load
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Create an account + pairing code (control-plane admin)
|
||||
|
||||
The admin API is **deny-by-default**: `POST /accounts` and `POST /accounts/:id/pairing-codes` require a
|
||||
**capability token** with the `manage` right whose **`aud` equals `BASE_DOMAIN`** (control-plane
|
||||
`authz.ts` / `main.ts` `expectedAud: env.baseDomain`). `accountId` is taken ONLY from the verified
|
||||
token (INV3) — never from the body.
|
||||
|
||||
Mint the token with the **capability PRIVATE key** from step 2b (the B6 token-mint owns the signing
|
||||
helper; it imports `/etc/relay/capability/capability-sign.key.pem`). Token claims:
|
||||
`rights=['manage']`, `aud=<BASE_DOMAIN>`. Send it as `Authorization: Bearer <token>`.
|
||||
|
||||
Bootstrap has a two-token nuance (grounded in `provision.ts`):
|
||||
|
||||
1. `POST /accounts` checks `manage` only (no account-ownership check) → mint a `manage` token, create
|
||||
the account, read back its `id`.
|
||||
2. `POST /accounts/:id/pairing-codes` additionally enforces `token.sub == :id` (own-account). Mint a
|
||||
second `manage` token with `sub=<the new accountId>`, then request the pairing code.
|
||||
|
||||
Over the SSH tunnel from step 4:
|
||||
|
||||
```bash
|
||||
# 1) create account
|
||||
curl -sS -X POST http://127.0.0.1:8080/accounts \
|
||||
-H "Authorization: Bearer <MANAGE_TOKEN aud=BASE_DOMAIN>" \
|
||||
-H 'content-type: application/json' -d '{"plan":"personal"}'
|
||||
# -> { "id": "<ACCOUNT_ID>", ... }
|
||||
|
||||
# 2) pairing code (token.sub must equal <ACCOUNT_ID>)
|
||||
curl -sS -X POST http://127.0.0.1:8080/accounts/<ACCOUNT_ID>/pairing-codes \
|
||||
-H "Authorization: Bearer <MANAGE_TOKEN sub=ACCOUNT_ID aud=BASE_DOMAIN>"
|
||||
# -> { "code": "<PAIRING_CODE>", ... } (single-use, TTL 600s default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. On the LAPTOP — build the agent, pair, run
|
||||
|
||||
The agent dials OUT; nothing inbound is opened on the laptop.
|
||||
|
||||
```bash
|
||||
cd <repo>/agent
|
||||
npm ci && npm run build # emits dist/cli.js (C1)
|
||||
export RELAY_URL="wss://<SUBDOMAIN>.<BASE_DOMAIN>:8444" # AGENT_BIND_PORT
|
||||
export ENROLL_URL="https://<SUBDOMAIN>.<BASE_DOMAIN>/enroll"
|
||||
export HOST_ID="<pick-a-host-id>"
|
||||
export SUBDOMAIN="<SUBDOMAIN>"
|
||||
export LOCAL_TARGET_URL="ws://127.0.0.1:3000" # the base web-terminal app
|
||||
export STATE_DIR="$HOME/.web-terminal-agent"
|
||||
|
||||
node dist/cli.js pair <PAIRING_CODE> # redeems the code -> SPIFFE mTLS cert + hostContentSecret
|
||||
node dist/cli.js run # dials the relay, holds the mux tunnel
|
||||
```
|
||||
|
||||
`pair` redeems the single-use code and stores the enrolled SPIFFE cert under `STATE_DIR`; `run` opens
|
||||
the outbound mTLS tunnel and keeps it alive (heartbeat/backoff).
|
||||
|
||||
---
|
||||
|
||||
## 9. Browser — log in and click through to the shell
|
||||
|
||||
1. Open `https://<SUBDOMAIN>.<BASE_DOMAIN>` (served same-origin as the WSS endpoint).
|
||||
2. Operator login (B6) mints a short-lived connect capability token.
|
||||
3. Pick the host → the relay splices browser ↔ agent. The relay only sees **ciphertext** (INV2); the
|
||||
E2E is browser ↔ agent.
|
||||
|
||||
---
|
||||
|
||||
## 10. Verify restart-safety + revocation teardown
|
||||
|
||||
```bash
|
||||
# Restart-safety (INV7): bounce the relay while a shell is open — the PTY and host registration survive.
|
||||
sudo systemctl restart relay-data-plane.service
|
||||
# the browser auto-reconnects; the session is still there (state is in PG/Redis, not the process).
|
||||
|
||||
# Revocation teardown (INV12): revoke the host and watch the tunnel drop within budget.
|
||||
curl -sS -X DELETE http://127.0.0.1:8080/hosts/<HOST_ID> \
|
||||
-H "Authorization: Bearer <MANAGE_TOKEN sub=ACCOUNT_ID aud=BASE_DOMAIN>"
|
||||
# -> control-plane publishes a KillSignal on Redis relay:revocations; the data-plane subscriber
|
||||
# closes the spliced stream (browser WS closes 4403). Measure revoke -> close latency.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Meaning | Fix |
|
||||
|---|---|---|
|
||||
| Browser WS closes **1013** | `WS_TRY_LATER` — **no agent tunnel** for that host (agent offline / not dialed) | Start the agent (`node dist/cli.js run`); check `RELAY_URL` host+port; confirm `:8444` open; watch `journalctl -u relay-data-plane`. |
|
||||
| Browser WS closes **401** | **bad Origin** (CSWSH exact-match failed) — the `Origin` header is not in the relay's `allowedOrigins` | Open via `https://<SUBDOMAIN>.<BASE_DOMAIN>` (not the raw IP, not `http:`, no stray port). `allowedOrigins` is the port-less `https://<sub>.<BASE_DOMAIN>` on `:443`; must match EXACTLY. Verify `BASE_DOMAIN` in `relay.env`. |
|
||||
| Agent mTLS closes **4401** | cert chains to the CA but the **pubkey is not enrolled** in the host registry (INV14 registry-gated) | The host was never enrolled or was revoked. Re-run `pair <CODE>` with a fresh code; confirm the CP signed the leaf with `/etc/relay/ca/intermediate.key.pem` (chains to `agent-ca.bundle.pem`). |
|
||||
| Browser WS closes **4403** | `WS_REVOKED` — the host/session was revoked (expected after step 10) | Re-enroll the host to reconnect. |
|
||||
| CP fails to boot: "CAPABILITY_SIGN_PUBKEY_B64 must decode to 32 bytes" | wrong key encoding in `control-plane.env` | Use the **base64** value (not base64url) from step 2b for the CP; base64url is the relay's `RELAY_AUTH_VERIFY_PUBKEY`. |
|
||||
| Admin API returns **401** on `POST /accounts` | missing/expired/foreign-`aud` capability token | Mint a `manage` token with `aud=<BASE_DOMAIN>`, signed by the step-2b private key; send `Authorization: Bearer`. |
|
||||
| Admin API returns **403** on `/pairing-codes` | token lacks `manage` right, or `token.sub != :id` | Mint with `rights:['manage']` and `sub=<ACCOUNT_ID>` (own-account rule). |
|
||||
| LE issuance fails (timeout/connection) | HTTP-01 `:80` not reachable, or domain not ICP-filed | Open `:80` in the security group for issuance, or switch `ACME_METHOD=dns-01`; confirm the A-record + ICP filing. |
|
||||
| `docker compose ps` unhealthy | PG/Redis not up | `docker compose -f deploy/docker-compose.yml logs`; confirm `POSTGRES_PASSWORD` set in `deploy/.env`. |
|
||||
| Mixed-content / `ws:` blocked in browser | page is HTTPS but WS scheme resolved to `ws:` | Serve over HTTPS; the client follows page scheme (`wss:` on HTTPS, M6). Confirm the LE cert is valid for the FQDN. |
|
||||
145
deploy/scripts/gen-agent-ca.sh
Executable file
145
deploy/scripts/gen-agent-ca.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# RELAY-PHASE1 · E (TASK E) — Private ENROLLMENT CA for agent mTLS.
|
||||
#
|
||||
# STAGING TEMPLATE. This mints the PRIVATE trust chain the rendezvous-relay uses to (a) verify agent
|
||||
# client certs (SPIFFE mTLS, INV14) and (b) present the relay's agent-facing SERVER cert on
|
||||
# AGENT_BIND_PORT so the dialing agent can authenticate the relay.
|
||||
#
|
||||
# *** THIS IS NOT LET'S ENCRYPT. *** The browser-facing :443 cert is public-web PKI and is issued
|
||||
# by deploy/scripts/issue-tls-cert.sh. This CA is a SEPARATE, private trust root that must NEVER be
|
||||
# a public CA — mixing them would let any web-PKI leaf enroll as an agent.
|
||||
#
|
||||
# Hierarchy produced (Ed25519, to match the control-plane dev signer's algorithm):
|
||||
# root (self-signed, offline anchor)
|
||||
# └── intermediate (signs agent SPIFFE leaves at CP /enroll, AND the relay agent-server cert)
|
||||
# ├── relay agent-server leaf (TLS serverAuth, CN/SAN = the agent-facing host)
|
||||
# └── (agent leaves are issued at runtime by the control-plane, not here)
|
||||
#
|
||||
# Outputs (0600 keys, 0644 certs) under CA_DIR (default /etc/relay/ca):
|
||||
# root.key.pem root.cert.pem
|
||||
# intermediate.key.pem intermediate.cert.pem <- CA_INTERMEDIATE_CERT_PATH (+ its key = the CP signer)
|
||||
# agent-ca.bundle.pem = intermediate + root <- AGENT_CA_CHAIN_PATH / NODE_MTLS_TRUST_BUNDLE_PATH
|
||||
# agent-ca.cert.pem = intermediate cert <- AGENT_CA_CERT_PATH
|
||||
# relay-agent-server.key.pem relay-agent-server.cert.pem (present on AGENT_BIND_PORT)
|
||||
#
|
||||
# Config via ENV only (no hardcoded hosts/ports/secrets):
|
||||
# CA_DIR output dir (default /etc/relay/ca)
|
||||
# AGENT_FACING_HOST REQUIRED — CN/SAN for the relay agent-server cert; the host the agent dials,
|
||||
# e.g. "<sub>.<BASE_DOMAIN>" (must match RELAY_URL's host in the agent config).
|
||||
# AGENT_FACING_IP OPTIONAL — extra IP SAN (e.g. 8.138.1.192) if the agent dials the raw IP.
|
||||
# RELAY_TRUST_DOMAIN OPTIONAL — SPIFFE trust domain (recorded in a NOTE only; agent SPIFFE leaves
|
||||
# are minted by the control-plane /enroll, not by this script).
|
||||
# CA_DAYS_ROOT / CA_DAYS_INT / CA_DAYS_LEAF cert lifetimes (defaults 3650 / 1825 / 825).
|
||||
#
|
||||
# Verify (syntax): bash -n deploy/scripts/gen-agent-ca.sh
|
||||
set -euo pipefail
|
||||
|
||||
CA_DIR="${CA_DIR:-/etc/relay/ca}"
|
||||
CA_DAYS_ROOT="${CA_DAYS_ROOT:-3650}"
|
||||
CA_DAYS_INT="${CA_DAYS_INT:-1825}"
|
||||
CA_DAYS_LEAF="${CA_DAYS_LEAF:-825}"
|
||||
|
||||
if [[ -z "${AGENT_FACING_HOST:-}" ]]; then
|
||||
echo "FATAL: AGENT_FACING_HOST is required (CN/SAN of the relay agent-server cert)." >&2
|
||||
echo " e.g. AGENT_FACING_HOST=term1.example.com bash $0" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command -v openssl >/dev/null 2>&1 || { echo "FATAL: openssl not found on PATH." >&2; exit 3; }
|
||||
|
||||
umask 077
|
||||
mkdir -p "${CA_DIR}"
|
||||
cd "${CA_DIR}"
|
||||
|
||||
echo "== Enrollment CA (PRIVATE — not Let's Encrypt) into ${CA_DIR} =="
|
||||
|
||||
# ---- 1. Root CA (offline anchor) ------------------------------------------------------------------
|
||||
if [[ ! -f root.key.pem ]]; then
|
||||
openssl genpkey -algorithm ed25519 -out root.key.pem
|
||||
fi
|
||||
openssl req -x509 -new -key root.key.pem -days "${CA_DAYS_ROOT}" \
|
||||
-subj "/O=web-terminal-relay/OU=agent-enrollment/CN=web-terminal Agent Enrollment Root" \
|
||||
-addext "basicConstraints=critical,CA:TRUE" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
||||
-addext "subjectKeyIdentifier=hash" \
|
||||
-out root.cert.pem
|
||||
|
||||
# ---- 2. Intermediate CA (the control-plane's leaf-signing key in staging) --------------------------
|
||||
# The CP dev in-process signer (CA_INTERMEDIATE_KMS_KEY_REF=dev-local) signs agent SPIFFE leaves with
|
||||
# THIS intermediate key. In Phase 2 the intermediate key moves into a non-exportable KMS/HSM (§3.1).
|
||||
if [[ ! -f intermediate.key.pem ]]; then
|
||||
openssl genpkey -algorithm ed25519 -out intermediate.key.pem
|
||||
fi
|
||||
openssl req -new -key intermediate.key.pem \
|
||||
-subj "/O=web-terminal-relay/OU=agent-enrollment/CN=web-terminal Agent Enrollment Intermediate" \
|
||||
-out intermediate.csr.pem
|
||||
cat > intermediate.ext <<'EXT'
|
||||
basicConstraints = critical,CA:TRUE,pathlen:0
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
EXT
|
||||
openssl x509 -req -in intermediate.csr.pem \
|
||||
-CA root.cert.pem -CAkey root.key.pem -CAcreateserial \
|
||||
-days "${CA_DAYS_INT}" -extfile intermediate.ext \
|
||||
-out intermediate.cert.pem
|
||||
|
||||
# ---- 3. Trust bundle (intermediate + root) --------------------------------------------------------
|
||||
# The relay walks leaf -> intermediate -> root, so the anchor (root) MUST be in the bundle
|
||||
# (relay-auth verifyChain terminates at a self-signed root present in the set, INV14).
|
||||
cat intermediate.cert.pem root.cert.pem > agent-ca.bundle.pem
|
||||
cp intermediate.cert.pem agent-ca.cert.pem
|
||||
|
||||
# ---- 4. Relay agent-facing SERVER cert (presented on AGENT_BIND_PORT) -----------------------------
|
||||
# The agent dials wss://<AGENT_FACING_HOST>:<AGENT_BIND_PORT> and pins THIS CA to authenticate the
|
||||
# relay server. CN/SAN must equal the host the agent dials.
|
||||
if [[ ! -f relay-agent-server.key.pem ]]; then
|
||||
openssl genpkey -algorithm ed25519 -out relay-agent-server.key.pem
|
||||
fi
|
||||
openssl req -new -key relay-agent-server.key.pem \
|
||||
-subj "/O=web-terminal-relay/OU=agent-data-plane/CN=${AGENT_FACING_HOST}" \
|
||||
-out relay-agent-server.csr.pem
|
||||
{
|
||||
echo "basicConstraints = critical,CA:FALSE"
|
||||
echo "keyUsage = critical,digitalSignature"
|
||||
echo "extendedKeyUsage = serverAuth"
|
||||
echo "subjectKeyIdentifier = hash"
|
||||
echo "authorityKeyIdentifier = keyid,issuer"
|
||||
if [[ -n "${AGENT_FACING_IP:-}" ]]; then
|
||||
echo "subjectAltName = DNS:${AGENT_FACING_HOST},IP:${AGENT_FACING_IP}"
|
||||
else
|
||||
echo "subjectAltName = DNS:${AGENT_FACING_HOST}"
|
||||
fi
|
||||
} > relay-agent-server.ext
|
||||
openssl x509 -req -in relay-agent-server.csr.pem \
|
||||
-CA intermediate.cert.pem -CAkey intermediate.key.pem -CAcreateserial \
|
||||
-days "${CA_DAYS_LEAF}" -extfile relay-agent-server.ext \
|
||||
-out relay-agent-server.cert.pem
|
||||
|
||||
# ---- 5. Permissions + summary ---------------------------------------------------------------------
|
||||
chmod 600 ./*.key.pem
|
||||
chmod 644 ./*.cert.pem ./*.bundle.pem
|
||||
rm -f ./*.csr.pem ./*.ext
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
Enrollment CA ready in ${CA_DIR}. Wire these env paths (relay.env / control-plane.env):
|
||||
|
||||
CA_INTERMEDIATE_CERT_PATH = ${CA_DIR}/intermediate.cert.pem
|
||||
NODE_MTLS_TRUST_BUNDLE_PATH = ${CA_DIR}/agent-ca.bundle.pem
|
||||
AGENT_CA_CERT_PATH = ${CA_DIR}/agent-ca.cert.pem
|
||||
AGENT_CA_CHAIN_PATH = ${CA_DIR}/agent-ca.bundle.pem
|
||||
|
||||
relay agent-server cert = ${CA_DIR}/relay-agent-server.cert.pem
|
||||
relay agent-server key = ${CA_DIR}/relay-agent-server.key.pem (0600)
|
||||
intermediate signing key = ${CA_DIR}/intermediate.key.pem (0600 — the CP dev signer)
|
||||
|
||||
NOTE (staging integration seam, owned by the A-wave CP boot/ca-wiring, NOT by this script):
|
||||
The control-plane's dev in-process signer must sign agent leaves with intermediate.key.pem so the
|
||||
leaves chain to this bundle. CA_INTERMEDIATE_KMS_KEY_REF=dev-local selects the dev signer; point it
|
||||
at ${CA_DIR}/intermediate.key.pem. Phase 2 replaces this with a KMS/HSM ref (§3.1).
|
||||
SPIFFE trust domain for agent leaves = ${RELAY_TRUST_DOMAIN:-<RELAY_TRUST_DOMAIN unset>}.
|
||||
|
||||
*** root.key.pem is the offline trust anchor — after first use, move it OFF this host. ***
|
||||
SUMMARY
|
||||
67
deploy/scripts/gen-capability-key.sh
Executable file
67
deploy/scripts/gen-capability-key.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# RELAY-PHASE1 · E (TASK E) — P5 capability-token keypair (Ed25519).
|
||||
#
|
||||
# STAGING TEMPLATE. One Ed25519 keypair underpins the whole authz split:
|
||||
#
|
||||
# CONTROL-PLANE SIGNS ──(mints capability tokens with the PRIVATE key, B6 token-mint)──▶
|
||||
# RELAY VERIFIES
|
||||
# The CP admin API (POST /accounts, /pairing-codes) ALSO verifies operator "manage" tokens with
|
||||
# the SAME public key. So the public key is deployed to BOTH processes; the private key lives ONLY
|
||||
# where tokens are minted (the B6 mint), never on the relay, never committed.
|
||||
#
|
||||
# The public key is printed in TWO encodings of the SAME raw 32 bytes (the split is encoding-only):
|
||||
# CAPABILITY_SIGN_PUBKEY_B64 raw 32B, standard base64 -> control-plane env (env.ts base64ToBytes)
|
||||
# RELAY_AUTH_VERIFY_PUBKEY raw 32B, base64url (no pad) -> relay env (loadVerifyKeyFromEnv)
|
||||
# These MUST be the SAME key (PLAN §3 linchpin). This script prints both from one keypair so they
|
||||
# cannot drift.
|
||||
#
|
||||
# The PRIVATE key is written PKCS#8 PEM to a 0600 file for the B6 mint to import (WebCrypto Ed25519).
|
||||
# It is NEVER printed and NEVER placed in relay.env / control-plane.env.
|
||||
#
|
||||
# Config via ENV only:
|
||||
# KEY_DIR output dir for the private key (default /etc/relay/capability)
|
||||
#
|
||||
# Verify (syntax): bash -n deploy/scripts/gen-capability-key.sh
|
||||
set -euo pipefail
|
||||
|
||||
KEY_DIR="${KEY_DIR:-/etc/relay/capability}"
|
||||
PRIV="${KEY_DIR}/capability-sign.key.pem"
|
||||
|
||||
command -v openssl >/dev/null 2>&1 || { echo "FATAL: openssl not found on PATH." >&2; exit 3; }
|
||||
|
||||
umask 077
|
||||
mkdir -p "${KEY_DIR}"
|
||||
|
||||
if [[ -f "${PRIV}" ]]; then
|
||||
echo "WARN: ${PRIV} already exists — reusing it (delete it to rotate). Rotating INVALIDATES every" >&2
|
||||
echo " minted token AND requires re-deploying the public key to BOTH processes." >&2
|
||||
else
|
||||
openssl genpkey -algorithm ed25519 -out "${PRIV}"
|
||||
fi
|
||||
chmod 600 "${PRIV}"
|
||||
|
||||
# Raw 32-byte Ed25519 public key = last 32 bytes of the SPKI DER.
|
||||
PUB_STD_B64="$(openssl pkey -in "${PRIV}" -pubout -outform DER | tail -c 32 | openssl base64 -A)"
|
||||
# base64url of the SAME bytes, padding stripped (decodeBase64UrlBytes tolerates missing padding).
|
||||
PUB_B64URL="$(printf '%s' "${PUB_STD_B64}" | tr '+/' '-_' | tr -d '=')"
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
P5 capability keypair ready.
|
||||
|
||||
PRIVATE key (B6 token-mint only; 0600; NEVER commit / NEVER on the relay):
|
||||
${PRIV}
|
||||
|
||||
Put the SAME public key in BOTH env files (they are one key, two encodings):
|
||||
|
||||
# control-plane.env
|
||||
CAPABILITY_SIGN_PUBKEY_B64=${PUB_STD_B64}
|
||||
|
||||
# relay.env
|
||||
RELAY_AUTH_VERIFY_PUBKEY=${PUB_B64URL}
|
||||
|
||||
Sanity check they decode to the same 32 bytes:
|
||||
diff <(openssl pkey -in "${PRIV}" -pubout -outform DER | tail -c 32 | xxd -p) \\
|
||||
<(printf '%s' "${PUB_STD_B64}" | openssl base64 -d -A | xxd -p) && echo OK
|
||||
SUMMARY
|
||||
115
deploy/scripts/issue-tls-cert.sh
Executable file
115
deploy/scripts/issue-tls-cert.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# RELAY-PHASE1 · E (TASK E) — PUBLIC-web TLS cert for the browser :443 endpoint (Let's Encrypt).
|
||||
#
|
||||
# STAGING TEMPLATE. This is the ONLY public-CA cert in the deployment: the browser hits
|
||||
# https://<SUBDOMAIN>.<BASE_DOMAIN> (WSS same-origin), so it must chain to a public root. It is a
|
||||
# DIFFERENT trust chain from the private agent-enrollment CA (deploy/scripts/gen-agent-ca.sh) — do
|
||||
# NOT cross-wire them.
|
||||
#
|
||||
# ICP-備案 ASSUMPTION (mainland Alibaba Cloud): BASE_DOMAIN is ALREADY ICP-filed and its A-record
|
||||
# <SUBDOMAIN>.<BASE_DOMAIN> -> 8.138.1.192 resolves. Serving :80/:443 on an unfiled domain in
|
||||
# mainland CN is blocked upstream; HTTP-01 will then fail. If unfiled, use DNS-01 (no inbound :80).
|
||||
#
|
||||
# Two challenge methods (pick with ACME_METHOD):
|
||||
# http-01 — a one-shot listener on :80 answers the challenge. REQUIRES inbound :80 open in the
|
||||
# Aliyun security group DURING issuance (you can close it again after; renewals reopen).
|
||||
# dns-01 — a TXT record under _acme-challenge.<SUBDOMAIN>.<BASE_DOMAIN>. No inbound :80. Needs the
|
||||
# acme.sh DNS-API creds for your provider (e.g. Ali_Key/Ali_Secret for the Aliyun DNS API).
|
||||
#
|
||||
# Installs to TLS_CERT_PATH (fullchain) + TLS_KEY_PATH, matching relay.env.
|
||||
#
|
||||
# Config via ENV only:
|
||||
# BASE_DOMAIN REQUIRED (e.g. term.example.com)
|
||||
# SUBDOMAIN REQUIRED (the tenant sub; FQDN = <SUBDOMAIN>.<BASE_DOMAIN>)
|
||||
# ACME_EMAIL REQUIRED account/registration email
|
||||
# ACME_METHOD http-01 | dns-01 (default http-01)
|
||||
# ACME_CLIENT acme.sh | certbot (default acme.sh)
|
||||
# ACME_DNS_PROVIDER acme.sh dnsapi id for dns-01 (e.g. dns_ali) (dns-01 only)
|
||||
# TLS_CERT_PATH install target for fullchain (default /etc/relay/tls/fullchain.pem)
|
||||
# TLS_KEY_PATH install target for privkey (default /etc/relay/tls/privkey.pem)
|
||||
# ACME_STAGING 1 to use the LE staging CA (untrusted, avoids rate limits while testing)
|
||||
#
|
||||
# Verify (syntax): bash -n deploy/scripts/issue-tls-cert.sh
|
||||
set -euo pipefail
|
||||
|
||||
: "${BASE_DOMAIN:?FATAL: BASE_DOMAIN is required (your ICP-filed domain)}"
|
||||
: "${SUBDOMAIN:?FATAL: SUBDOMAIN is required}"
|
||||
: "${ACME_EMAIL:?FATAL: ACME_EMAIL is required}"
|
||||
ACME_METHOD="${ACME_METHOD:-http-01}"
|
||||
ACME_CLIENT="${ACME_CLIENT:-acme.sh}"
|
||||
TLS_CERT_PATH="${TLS_CERT_PATH:-/etc/relay/tls/fullchain.pem}"
|
||||
TLS_KEY_PATH="${TLS_KEY_PATH:-/etc/relay/tls/privkey.pem}"
|
||||
|
||||
FQDN="${SUBDOMAIN}.${BASE_DOMAIN}"
|
||||
echo "== Issuing Let's Encrypt cert for ${FQDN} via ${ACME_CLIENT} (${ACME_METHOD}) =="
|
||||
|
||||
mkdir -p "$(dirname "${TLS_CERT_PATH}")" "$(dirname "${TLS_KEY_PATH}")"
|
||||
|
||||
case "${ACME_CLIENT}" in
|
||||
acme.sh)
|
||||
command -v acme.sh >/dev/null 2>&1 || { echo "FATAL: acme.sh not installed. curl https://get.acme.sh | sh" >&2; exit 3; }
|
||||
acme.sh --register-account -m "${ACME_EMAIL}" >/dev/null 2>&1 || true
|
||||
STAGING_FLAG=""
|
||||
[[ "${ACME_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging"
|
||||
|
||||
case "${ACME_METHOD}" in
|
||||
http-01)
|
||||
echo ">> Ensure inbound :80 is OPEN in the Aliyun security group for the duration of issuance."
|
||||
acme.sh --issue ${STAGING_FLAG} -d "${FQDN}" --standalone --httpport 80
|
||||
;;
|
||||
dns-01)
|
||||
: "${ACME_DNS_PROVIDER:?FATAL: ACME_DNS_PROVIDER required for dns-01 (e.g. dns_ali); export the DNS-API creds too}"
|
||||
acme.sh --issue ${STAGING_FLAG} -d "${FQDN}" --dns "${ACME_DNS_PROVIDER}"
|
||||
;;
|
||||
*)
|
||||
echo "FATAL: ACME_METHOD must be http-01 or dns-01" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
# --install-cert copies the current material AND registers the renew-reload hook.
|
||||
acme.sh --install-cert -d "${FQDN}" \
|
||||
--key-file "${TLS_KEY_PATH}" \
|
||||
--fullchain-file "${TLS_CERT_PATH}" \
|
||||
--reloadcmd "systemctl try-reload-or-restart relay-data-plane.service"
|
||||
;;
|
||||
|
||||
certbot)
|
||||
command -v certbot >/dev/null 2>&1 || { echo "FATAL: certbot not installed (apt-get install certbot)." >&2; exit 3; }
|
||||
STAGING_FLAG=""
|
||||
[[ "${ACME_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging"
|
||||
case "${ACME_METHOD}" in
|
||||
http-01)
|
||||
echo ">> Ensure inbound :80 is OPEN in the Aliyun security group for the duration of issuance."
|
||||
certbot certonly ${STAGING_FLAG} --standalone --non-interactive --agree-tos \
|
||||
-m "${ACME_EMAIL}" -d "${FQDN}"
|
||||
;;
|
||||
dns-01)
|
||||
echo "FATAL: certbot dns-01 needs a provider plugin; prefer ACME_CLIENT=acme.sh for Aliyun DNS." >&2
|
||||
exit 2 ;;
|
||||
*)
|
||||
echo "FATAL: ACME_METHOD must be http-01 or dns-01" >&2; exit 2 ;;
|
||||
esac
|
||||
install -m 644 "/etc/letsencrypt/live/${FQDN}/fullchain.pem" "${TLS_CERT_PATH}"
|
||||
install -m 600 "/etc/letsencrypt/live/${FQDN}/privkey.pem" "${TLS_KEY_PATH}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "FATAL: ACME_CLIENT must be acme.sh or certbot" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
chmod 600 "${TLS_KEY_PATH}" || true
|
||||
chmod 644 "${TLS_CERT_PATH}" || true
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
Installed:
|
||||
TLS_CERT_PATH = ${TLS_CERT_PATH}
|
||||
TLS_KEY_PATH = ${TLS_KEY_PATH}
|
||||
|
||||
RENEWAL (LE certs last ~90 days):
|
||||
- acme.sh installs its OWN cron on install ('acme.sh --cron'); --install-cert registered the
|
||||
reload hook above, so renewals auto-reload the data-plane. Verify: 'crontab -l | grep acme'.
|
||||
- certbot: add a cron/timer, e.g.
|
||||
0 3 * * * certbot renew --quiet --deploy-hook 'systemctl try-reload-or-restart relay-data-plane.service'
|
||||
- http-01 renewals need :80 reachable at renew time; dns-01 does not.
|
||||
SUMMARY
|
||||
52
deploy/systemd/relay-control-plane.service
Normal file
52
deploy/systemd/relay-control-plane.service
Normal file
@@ -0,0 +1,52 @@
|
||||
# RELAY-PHASE1 · E (TASK E) — control-plane (P3) systemd unit. STAGING TEMPLATE.
|
||||
#
|
||||
# Runs the P3 admin/registry/CA/pairing API (control-plane/src/server.ts, added in A2) on the
|
||||
# loopback admin port (CP_BIND_HOST/CP_BIND_PORT in the env file). It is NEVER exposed publicly —
|
||||
# the security group keeps 8080 loopback-only; operators reach it via SSH tunnel.
|
||||
#
|
||||
# INSTALL (adjust the placeholder paths to your VPS):
|
||||
# 1) clone the repo to /opt/web-terminal (or edit --prefix below)
|
||||
# 2) install: sudo cp deploy/systemd/relay-control-plane.service /etc/systemd/system/
|
||||
# 3) put the filled env at /etc/relay/control-plane.env (0600, owned by the service user)
|
||||
# 4) sudo systemctl daemon-reload && sudo systemctl enable --now relay-control-plane.service
|
||||
#
|
||||
# The env file supplies PG_URL/REDIS_URL/CAPABILITY_SIGN_PUBKEY_B64/CA_* /BASE_DOMAIN (see .env.example).
|
||||
|
||||
[Unit]
|
||||
Description=web-terminal rendezvous-relay CONTROL PLANE (P3)
|
||||
Documentation=file:///opt/web-terminal/docs/PLAN_RELAY_PHASE1.md
|
||||
# Postgres + Redis run under Docker Compose (deploy/docker-compose.yml); wait for docker + network.
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Dedicated non-root service user (create once: sudo useradd --system --home /opt/web-terminal relay).
|
||||
User=relay
|
||||
Group=relay
|
||||
WorkingDirectory=/opt/web-terminal
|
||||
# Secrets/config come ONLY from the env file (INV9 — no secrets inline in the unit).
|
||||
EnvironmentFile=/etc/relay/control-plane.env
|
||||
# /usr/bin/env resolves npm via PATH (works for NodeSource /usr/bin/npm and nvm shims alike).
|
||||
# Runs the "start" script (control-plane/package.json -> tsx src/server.ts).
|
||||
ExecStart=/usr/bin/env npm --prefix /opt/web-terminal/control-plane start
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
# Give in-flight requests / graceful SIGTERM (server.ts closes the http listener + pg/redis) time.
|
||||
TimeoutStopSec=20
|
||||
KillSignal=SIGTERM
|
||||
|
||||
# --- hardening (INV9: shrink the blast radius around the CA + capability material) ---
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelTunables=true
|
||||
RestrictSUIDSGID=true
|
||||
# The CA/capability material under /etc/relay is READ-ONLY to this service.
|
||||
ReadOnlyPaths=/etc/relay
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
53
deploy/systemd/relay-data-plane.service
Normal file
53
deploy/systemd/relay-data-plane.service
Normal file
@@ -0,0 +1,53 @@
|
||||
# RELAY-PHASE1 · E (TASK E) — relay data-plane (P1) systemd unit. STAGING TEMPLATE.
|
||||
#
|
||||
# Runs the Phase-1 relay entry (relay-run/src/main-phase1.ts via `start:phase1`, added in B5): browser
|
||||
# WSS on :443 (BIND_PORT) and agent mTLS on AGENT_BIND_PORT. Stateless / restart-safe (INV7) — all
|
||||
# state is in Postgres/Redis, so bouncing this unit does NOT drop host registrations or kill PTYs.
|
||||
#
|
||||
# INSTALL (adjust the placeholder paths to your VPS):
|
||||
# 1) clone the repo to /opt/web-terminal (or edit --prefix below)
|
||||
# 2) install: sudo cp deploy/systemd/relay-data-plane.service /etc/systemd/system/
|
||||
# 3) put the filled env at /etc/relay/relay.env (0600, owned by the service user)
|
||||
# 4) sudo systemctl daemon-reload && sudo systemctl enable --now relay-data-plane.service
|
||||
#
|
||||
# The env file supplies BIND_HOST/BIND_PORT/TLS_*/AGENT_BIND_PORT/AGENT_CA_*/BASE_DOMAIN/
|
||||
# RELAY_NODE_ID/RELAY_AUTH_VERIFY_PUBKEY/RELAY_TRUST_DOMAIN/PG_URL/REDIS_URL (see .env.example).
|
||||
|
||||
[Unit]
|
||||
Description=web-terminal rendezvous-relay DATA PLANE (P1, Phase 1)
|
||||
Documentation=file:///opt/web-terminal/docs/PLAN_RELAY_PHASE1.md
|
||||
# Reads the SAME Postgres/Redis as the control-plane (routes, revocations, registries).
|
||||
After=network-online.target docker.service relay-control-plane.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=relay
|
||||
Group=relay
|
||||
WorkingDirectory=/opt/web-terminal
|
||||
EnvironmentFile=/etc/relay/relay.env
|
||||
# Runs relay-run "start:phase1" (relay-run/package.json -> tsx src/main-phase1.ts).
|
||||
ExecStart=/usr/bin/env npm --prefix /opt/web-terminal/relay-run run start:phase1
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
TimeoutStopSec=20
|
||||
KillSignal=SIGTERM
|
||||
|
||||
# Bind the privileged browser port :443 WITHOUT running as root (least privilege).
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
|
||||
# --- hardening ---
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelTunables=true
|
||||
RestrictSUIDSGID=true
|
||||
# TLS keys + the pinned agent-CA bundle are read-only to the relay.
|
||||
ReadOnlyPaths=/etc/relay
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user