From 2482d4acb6bc75e5a367a56d05c849963fadef46 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 10 Jun 2026 14:00:11 +0100 Subject: [PATCH] Document provider-MSP deploy bundle as canonical install; surface lease signing public key in setup.sh - MSP.md now leads with deploy/provider-msp/ (compose stack, setup.sh, upgrade.sh, run-install-proof.sh), documents the HTTPS requirement (__Host- portal session cookie) and the pulse.provider-msp.role labels workspace provisioning requires, and explains the licence/lease chain including licence-expiry behavior. - setup.sh derives and prints the lease signing public key the provider MSP licence must bind (also via --print-lease-signing-public-key), and the missing-licence error now includes it with request instructions. - .env.example documents the CP_TRIAL_ACTIVATION_PRIVATE_KEY binding. --- deploy/provider-msp/.env.example | 6 ++++ deploy/provider-msp/setup.sh | 56 +++++++++++++++++++++++++++++++- docs/MSP.md | 42 ++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/deploy/provider-msp/.env.example b/deploy/provider-msp/.env.example index 34dc41bfe..74b4c16f4 100644 --- a/deploy/provider-msp/.env.example +++ b/deploy/provider-msp/.env.example @@ -26,6 +26,12 @@ PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR=/var/lib/pulse-provider-msp/spacecheck/ro PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR=/var/lib/docker/.pulse-provider-msp-spacecheck CP_TRUSTED_PROXY_CIDRS=172.30.0.0/24 CP_PROVIDER_MSP_LICENSE_FILE=./provider-msp-license.jwt +# Entitlement lease signing key. setup.sh generates this; the private key +# never leaves this host. Your provider MSP license must bind the derived +# PUBLIC key (entitlement_signing_public_key) — print it with +# `./setup.sh --print-lease-signing-public-key` and include it in your +# license request. The control plane refuses to start if license and key +# do not match. CP_TRIAL_ACTIVATION_PRIVATE_KEY= CP_TENANT_MEMORY_LIMIT=536870912 CP_ALLOW_DOCKERLESS_PROVISIONING=false diff --git a/deploy/provider-msp/setup.sh b/deploy/provider-msp/setup.sh index 4fb4a6942..1319601b6 100755 --- a/deploy/provider-msp/setup.sh +++ b/deploy/provider-msp/setup.sh @@ -297,6 +297,42 @@ ensure_generated_secrets() { chmod 0600 "${env_path}" } +# derive_lease_signing_public_key prints the base64 Ed25519 public key for +# CP_TRIAL_ACTIVATION_PRIVATE_KEY. The provider MSP license must bind this +# exact key (entitlement_signing_public_key) or the control plane will refuse +# to start; include it when requesting your license. The private key never +# leaves this host. +derive_lease_signing_public_key() { + local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env" + [[ -f "${env_path}" ]] || die "missing ${env_path}" + have openssl || die "openssl is required to derive the lease signing public key" + + local key_b64 key_len tmp_der + key_b64="$(env_value CP_TRIAL_ACTIVATION_PRIVATE_KEY "${env_path}")" + [[ -n "${key_b64}" ]] || die "CP_TRIAL_ACTIVATION_PRIVATE_KEY is not set; run setup.sh first" + key_len="$(printf '%s' "${key_b64}" | base64 -d 2>/dev/null | wc -c | tr -d ' ')" + case "${key_len}" in + 64) + # 64-byte Ed25519 private key: the public key is the trailing 32 bytes. + printf '%s' "${key_b64}" | base64 -d | tail -c 32 | base64 | tr -d '\n' + ;; + 32) + # 32-byte seed: wrap in a PKCS#8 DER envelope and let openssl derive + # the public key (raw key = trailing 32 bytes of the SPKI DER). + tmp_der="$(mktemp)" + { + printf '\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20' + printf '%s' "${key_b64}" | base64 -d + } >"${tmp_der}" + openssl pkey -inform DER -in "${tmp_der}" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n' + rm -f "${tmp_der}" + ;; + *) + die "CP_TRIAL_ACTIVATION_PRIVATE_KEY must decode to a 32-byte seed or 64-byte Ed25519 key (got ${key_len} bytes)" + ;; + esac +} + truthy() { case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in true|1|yes|on) return 0 ;; @@ -454,7 +490,13 @@ validate_env_file() { if [[ "${license_file}" != /* ]]; then license_file="${PULSE_PROVIDER_MSP_INSTALL_DIR}/${license_file}" fi - [[ -f "${license_file}" ]] || die "CP_PROVIDER_MSP_LICENSE_FILE does not exist: ${license_file}" + if [[ ! -f "${license_file}" ]]; then + die "CP_PROVIDER_MSP_LICENSE_FILE does not exist: ${license_file} +Request your provider MSP license with this lease signing public key +(./setup.sh --print-lease-signing-public-key): + $(derive_lease_signing_public_key) +The license must bind this key or the control plane will refuse to start." + fi } validate_compose_config() { @@ -521,12 +563,24 @@ Proof: Portal: https://${domain}/ +Lease signing public key (your provider MSP license must bind this key; +re-print any time with ./setup.sh --print-lease-signing-public-key): + $(derive_lease_signing_public_key) + EOF } main() { need_root + if [[ "${1:-}" == "--print-lease-signing-public-key" ]]; then + ensure_env_file + ensure_generated_secrets + derive_lease_signing_public_key + printf '\n' + exit 0 + fi + log "starting provider MSP first-time setup" apt_install apt-transport-https install_docker_ce diff --git a/docs/MSP.md b/docs/MSP.md index 7c210bfef..5ab85c6d0 100644 --- a/docs/MSP.md +++ b/docs/MSP.md @@ -11,8 +11,23 @@ alert routing, and reporting. It assumes you have read runtime per client workspace. Alerts, webhook destinations, branded report settings, users, audit history, and metrics stay inside the client runtime; duplicate hostnames across clients never collide because they never share a -runtime namespace. The stack is operated with the `pulse-control-plane` -binary: +runtime namespace. + +The canonical install is the deploy bundle at +[`deploy/provider-msp/`](../deploy/provider-msp/): a Docker Compose stack +(Traefik ingress with wildcard TLS, a hardened Docker socket proxy, and the +control plane), a guided `setup.sh` for fresh hosts, `upgrade.sh` for +backup-gated upgrades, and `run-install-proof.sh` for an end-to-end fresh +install proof. `.env.example` in that directory doubles as the operator +runbook. Start there rather than wiring containers by hand; among other +things the compose stack provides the `pulse.provider-msp.role=traefik` and +`pulse.provider-msp.role=control-plane` container labels that client +workspace provisioning requires for isolated tenant networking, and it +terminates TLS — the management portal sets a `__Host-` (HTTPS-only) session +cookie, so the portal does not work over plain HTTP. + +Day-2 operations run through the `pulse-control-plane` binary (via +`docker compose run --rm control-plane …` in the bundle): ```bash pulse-control-plane provider-msp bootstrap --account-name "Your MSP" --owner-email you@example.com @@ -162,3 +177,26 @@ are carried on the licence key. MSP plans are sized by client workspace count (Starter 5, Growth 15, Scale 40); workspace creation is blocked, not billed, when the limit is reached. MSP and Enterprise keys are issued through sales — contact support to get set up or to join the MSP design-partner program. + +In the provider-hosted model the licence is a signed file +(`CP_PROVIDER_MSP_LICENSE_FILE`) that also binds your control plane's +entitlement lease signing key: + +1. `setup.sh` generates `CP_TRIAL_ACTIVATION_PRIVATE_KEY` locally; the + private key never leaves your host. +2. Send the derived public key + (`./setup.sh --print-lease-signing-public-key`) with your licence request. +3. The issued licence binds that key. The control plane refuses to start in + provider mode if the licence and key do not match, so a misconfigured + stack fails at startup instead of provisioning client workspaces that + silently run unlicensed. + +Client runtimes lease their entitlements from your control plane (the +control plane injects the refresh endpoint; nothing phones Pulse Cloud) and +verify each lease through the licence chain: Pulse's embedded key signs your +licence, your licence binds your signing key, your signing key signs the +lease. Leases carry the MSP capability set plus `white_label`, so branded +per-client reports work inside every client workspace. When the licence +expires, leases stop verifying after the grace period and client runtimes +fall back to Community behavior; renew and restart the control plane to +restore them.