diff --git a/deploy/provider-msp/.env.example b/deploy/provider-msp/.env.example index d095e97c6..c5f271d11 100644 --- a/deploy/provider-msp/.env.example +++ b/deploy/provider-msp/.env.example @@ -48,6 +48,11 @@ CP_TRUSTED_PROXY_CIDRS=172.30.0.0/24 # PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE=1 skips the request on an air-gapped # host. The portal and client isolation still work, but client workspaces will # not carry MSP capabilities until a licence is installed. +# +# To let Pulse match this activation to your setup request, pass +# PULSE_PROVIDER_MSP_EVAL_EMAIL when running setup.sh. The setup sends that +# address, the public signing key, and a fixed signup-source label; it never +# sends client inventory or credentials. Leave it unset for an anonymous eval. CP_PROVIDER_MSP_LICENSE_FILE= # Entitlement lease signing key. setup.sh generates this; the private key # never leaves this host. Your provider MSP license must bind the derived @@ -86,6 +91,7 @@ PULSE_EMAIL_REPLY_TO=support@example.com # Optional one-command setup proof after .env is filled: # sudo -E PULSE_PROVIDER_MSP_ACCOUNT_NAME="Example MSP" \ # PULSE_PROVIDER_MSP_OWNER_EMAIL=owner@example.com \ +# PULSE_PROVIDER_MSP_EVAL_EMAIL=owner@example.com \ # ./setup.sh # # First owner bootstrap: diff --git a/deploy/provider-msp/setup.sh b/deploy/provider-msp/setup.sh index 0ec8483ce..96f04b890 100755 --- a/deploy/provider-msp/setup.sh +++ b/deploy/provider-msp/setup.sh @@ -20,6 +20,9 @@ PULSE_PROVIDER_MSP_ACCOUNT_NAME="${PULSE_PROVIDER_MSP_ACCOUNT_NAME:-}" PULSE_PROVIDER_MSP_OWNER_EMAIL="${PULSE_PROVIDER_MSP_OWNER_EMAIL:-}" PULSE_PROVIDER_MSP_LICENSE_URL="${PULSE_PROVIDER_MSP_LICENSE_URL:-https://license.pulserelay.pro}" PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE="${PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE:-0}" +PULSE_PROVIDER_MSP_EVAL_EMAIL="${PULSE_PROVIDER_MSP_EVAL_EMAIL:-}" +PULSE_PROVIDER_MSP_SIGNUP_SOURCE="${PULSE_PROVIDER_MSP_SIGNUP_SOURCE:-provider_msp_setup}" +PULSE_PROVIDER_MSP_UPGRADE_URL="${PULSE_PROVIDER_MSP_UPGRADE_URL:-https://pulserelay.pro/msp.html}" log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*" @@ -395,7 +398,13 @@ ensure_eval_license() { log "requesting a 2-client evaluation license from ${PULSE_PROVIDER_MSP_LICENSE_URL}" local body response token - body="$(printf '{"entitlement_signing_public_key":"%s"}' "${public_key}")" + body="$(jq -cn \ + --arg public_key "${public_key}" \ + --arg email "${PULSE_PROVIDER_MSP_EVAL_EMAIL}" \ + --arg signup_source "${PULSE_PROVIDER_MSP_SIGNUP_SOURCE}" \ + '{entitlement_signing_public_key: $public_key} + + (if $email == "" then {} else {email: $email} end) + + (if $signup_source == "" then {} else {signup_source: $signup_source} end)')" response="$(curl -fsS --max-time 20 \ -H 'Content-Type: application/json' \ -d "${body}" \ @@ -419,10 +428,16 @@ ensure_eval_license() { chmod 0600 "${eval_path}" set_env_value CP_PROVIDER_MSP_LICENSE_FILE "./provider-msp-eval-license.jwt" "${env_path}" - local expires + local expires license_id expires="$(printf '%s' "${response}" | jq -r '.expires_at // empty' 2>/dev/null || true)" + license_id="$(printf '%s' "${response}" | jq -r '.license_id // empty' 2>/dev/null || true)" log "evaluation license installed: 2 client workspaces${expires:+, expires ${expires}}" - log " buy a plan and set CP_PROVIDER_MSP_LICENSE_FILE to lift the cap" + if [[ "${license_id}" =~ ^lic_msp_[a-f0-9]+$ ]]; then + log " when you need a third client, request an upgrade at:" + log " ${PULSE_PROVIDER_MSP_UPGRADE_URL%/}?eval_license_id=${license_id}#request" + else + log " when you need a third client, request an upgrade at ${PULSE_PROVIDER_MSP_UPGRADE_URL%/}#request" + fi } ensure_generated_secrets() { @@ -798,8 +813,6 @@ main() { ensure_env_file ensure_dns_credentials_file ensure_generated_secrets - # After the signing key exists, since the license binds its public half. - ensure_eval_license # After install_docker_ce, which provides the buildx used to read the # registry, and before validation, which requires the pins to be set. ensure_image_pins @@ -809,6 +822,10 @@ main() { block_container_metadata_service validate_compose_config pull_provider_images + # Issue the evaluation only after the host is configured and the immutable + # images are reachable. This makes an issued evaluation a useful activation + # signal rather than a record created before setup can succeed. + ensure_eval_license run_install_proof_if_requested print_summary } diff --git a/docs/MSP.md b/docs/MSP.md index 6b88e5f4a..0b6c6dcca 100644 --- a/docs/MSP.md +++ b/docs/MSP.md @@ -258,26 +258,25 @@ reports or collect report data in the provider control plane. MSP and Enterprise capabilities (`multi_tenant`, `unlimited`, `white_label`) 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. +when the limit is reached. The 60-day, two-workspace evaluation is +self-service. Paid MSP licences are currently issued through the assisted +upgrade path so the recurring licence renewal and key binding are checked +before money changes hands. ### Evaluating without a licence -Self-service evaluation is available only from an exact release page that -includes the evaluation-capable provider MSP bundle and both integrity -sidecars described below. If the release you intend to use does not list all -three assets, stop: evaluation onboarding for that release remains -request-assisted. Do **not** download the moving `main` branch archive or run -its `setup.sh` as root; contact support or join the MSP design-partner program -instead. +Self-service evaluation is available from the signed provider bundle published +with Pulse v6.2.1. Use this exact release asset and its integrity sidecars; do +**not** download the moving `main` branch archive or run its `setup.sh` as +root. For a later release, first confirm its release page contains the +versioned provider archive, checksum, and SSH signature before changing the +version below. -Once an exact release publishes -`pulse-provider-msp-vX.Y.Z.tar.gz`, download that versioned asset and its -integrity sidecars, verify the archive with Pulse's pinned release key, and +Download the versioned asset, verify it with Pulse's pinned release key, and only then extract and run the guided setup: ```bash -export PULSE_VERSION=vX.Y.Z +export PULSE_VERSION=v6.2.1 export PULSE_MSP_BUNDLE="pulse-provider-msp-${PULSE_VERSION}.tar.gz" export PULSE_RELEASE_BASE="https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}" @@ -294,9 +293,16 @@ sha256sum -c "${PULSE_MSP_BUNDLE}.sha256" tar -xzf "${PULSE_MSP_BUNDLE}" cd "pulse-provider-msp-${PULSE_VERSION}" +export PULSE_PROVIDER_MSP_EVAL_EMAIL=you@example.com +export PULSE_PROVIDER_MSP_SIGNUP_SOURCE=msp_docs sudo -E bash ./setup.sh ``` +`PULSE_PROVIDER_MSP_EVAL_EMAIL` is optional. Set it if you want setup help and +want an eventual paid upgrade matched to this deployment; omit it for an +anonymous evaluation. The signup-source value is a fixed attribution label, +not free-form telemetry. + The host needs Ubuntu 24.04 or similar, a domain you can point at it, and ports 80 and 443 free. Install `curl`, `openssh-client`, `coreutils`, and `tar` before the verification step. The wildcard certificate is issued over DNS-01 @@ -304,12 +310,13 @@ with Cloudflare as the default provider (`CF_DNS_API_TOKEN`); any other Traefik dnsChallenge provider works by setting `ACME_DNS_PROVIDER` in `.env` and putting that provider's credential variables in `dns-credentials.env`. -In a published evaluation-capable bundle, leave -`CP_PROVIDER_MSP_LICENSE_FILE` blank and `setup.sh` self-issues a 2-client -evaluation licence. It sends only the public half of the signing key generated -on your host, exactly as the paid path does, and the private key never leaves -the machine. You can then onboard two real clients and confirm the isolation -boundary on your own infrastructure before buying. +Leave `CP_PROVIDER_MSP_LICENSE_FILE` blank and `setup.sh` self-issues a +2-client evaluation licence after configuration validation succeeds and the +immutable images are reachable. It sends only the public half of the signing +key generated on your host, the optional contact address above, and the fixed +signup-source label. The private key, client inventory, and credentials never +leave the machine. You can then onboard two real clients and confirm the +isolation boundary on your own infrastructure before buying. The evaluation licence lasts 60 days and re-running `setup.sh` reuses the one already on disk. On an air-gapped host set diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 00134e513..3f466aaa1 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -3319,6 +3319,26 @@ caps continue to come only from the signed licence file, never from the environment fallback, and `ProviderMSPPlanSourceEnvFallback` remains the recorded plan source whenever no licence is present. +The provider-hosted MSP acquisition funnel uses commercial records that already +belong to the boundary rather than reviving retired generic browser analytics +or adding client-workspace phone-home. An explicit landing-page evaluation +start is delivered to the support inbox with the fixed `msp_landing` source; a +persisted `msp_eval` licence is the activation stage and is issued only after +provider setup validates configuration and resolves the immutable images; a +persisted `msp_starter`, `msp_growth`, or `msp_scale` licence is the paid +fulfilment stage. The licence server admin surface may aggregate those licence +stages over bounded time windows. Optional evaluator email and the public +evaluation licence ID may join an assisted upgrade to its activation, but no +stage may collect client inventory, credentials, private signing material, or +free-form runtime telemetry. Anonymous evaluation must remain available. + +Provider-hosted Starter remains assisted even though the signed evaluation +bundle is now public. A generic recurring Stripe checkout is not a valid +substitute: the provider licence is a static offline file, so self-service +checkout may open only after renewal reissue, delivery, installation, refund, +and cancellation behavior are automated and proven end to end. Growth, Scale, +and Enterprise remain request-assisted under the existing tier contract. + The capability ceiling for an entitlement lease follows HOSTING, not licensing. `entitlements.Service.SetProviderHosted` bounds a provider-operated control diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 14b0113d8..90353c2e5 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1311,14 +1311,12 @@ upgrade, update, release, or artifact-selection behavior. ## Current State -Provider-hosted MSP evaluation source exists, but the active published release -line does not yet contain a signed evaluation-capable deploy bundle. Public MSP -guidance therefore fails closed instead of running the moving `main` archive. -Future release candidates now assemble a dedicated versioned provider MSP -bundle, stamp its Pulse image refs to the same exact tag, cover it with the -canonical candidate manifest/checksum/signature path, and require the asset at -the final publication barrier. The remaining dependency is publication of the -first release built from this packaging path. +Pulse v6.2.1 is the first active published release with the signed, +evaluation-capable provider MSP deploy bundle. Public MSP guidance pins that +exact version, verifies its detached SSH signature and checksum before root +execution, and continues to reject the moving `main` archive. Future release +guidance may advance the pin only after the new exact-version provider asset +and sidecars pass the same publication barrier. The shell installer's container-runtime discovery prefers a working rootful Docker daemon over any rootless socket (#1647). @@ -3316,6 +3314,17 @@ unlicensed control plane starts, but release-build client runtimes reject its unchained entitlement leases and the client workspaces run without the capabilities being evaluated. +Evaluation issuance happens only after setup has validated the operator +configuration and confirmed that every immutable provider image is reachable. +That ordering makes a stored `msp_eval` issue an activation signal rather than +a download-intent signal created before the install can succeed. Setup may +include an optional evaluator email and a fixed signup-source label so support +can match an assisted paid upgrade to the deployment. Those fields must remain +optional and bounded; the request must never contain client inventory, +credentials, private keys, or free-form runtime telemetry. The returned public +evaluation licence ID may be carried in the upgrade URL as a non-secret +correlation key. + Self-issue must degrade rather than block. A missing signing key, an unreachable licence server, or a response carrying no licence leaves the install unlicensed with an explicit warning, and `PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE` diff --git a/frontend-modern/public/docs/MSP.md b/frontend-modern/public/docs/MSP.md index 6b88e5f4a..0b6c6dcca 100644 --- a/frontend-modern/public/docs/MSP.md +++ b/frontend-modern/public/docs/MSP.md @@ -258,26 +258,25 @@ reports or collect report data in the provider control plane. MSP and Enterprise capabilities (`multi_tenant`, `unlimited`, `white_label`) 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. +when the limit is reached. The 60-day, two-workspace evaluation is +self-service. Paid MSP licences are currently issued through the assisted +upgrade path so the recurring licence renewal and key binding are checked +before money changes hands. ### Evaluating without a licence -Self-service evaluation is available only from an exact release page that -includes the evaluation-capable provider MSP bundle and both integrity -sidecars described below. If the release you intend to use does not list all -three assets, stop: evaluation onboarding for that release remains -request-assisted. Do **not** download the moving `main` branch archive or run -its `setup.sh` as root; contact support or join the MSP design-partner program -instead. +Self-service evaluation is available from the signed provider bundle published +with Pulse v6.2.1. Use this exact release asset and its integrity sidecars; do +**not** download the moving `main` branch archive or run its `setup.sh` as +root. For a later release, first confirm its release page contains the +versioned provider archive, checksum, and SSH signature before changing the +version below. -Once an exact release publishes -`pulse-provider-msp-vX.Y.Z.tar.gz`, download that versioned asset and its -integrity sidecars, verify the archive with Pulse's pinned release key, and +Download the versioned asset, verify it with Pulse's pinned release key, and only then extract and run the guided setup: ```bash -export PULSE_VERSION=vX.Y.Z +export PULSE_VERSION=v6.2.1 export PULSE_MSP_BUNDLE="pulse-provider-msp-${PULSE_VERSION}.tar.gz" export PULSE_RELEASE_BASE="https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}" @@ -294,9 +293,16 @@ sha256sum -c "${PULSE_MSP_BUNDLE}.sha256" tar -xzf "${PULSE_MSP_BUNDLE}" cd "pulse-provider-msp-${PULSE_VERSION}" +export PULSE_PROVIDER_MSP_EVAL_EMAIL=you@example.com +export PULSE_PROVIDER_MSP_SIGNUP_SOURCE=msp_docs sudo -E bash ./setup.sh ``` +`PULSE_PROVIDER_MSP_EVAL_EMAIL` is optional. Set it if you want setup help and +want an eventual paid upgrade matched to this deployment; omit it for an +anonymous evaluation. The signup-source value is a fixed attribution label, +not free-form telemetry. + The host needs Ubuntu 24.04 or similar, a domain you can point at it, and ports 80 and 443 free. Install `curl`, `openssh-client`, `coreutils`, and `tar` before the verification step. The wildcard certificate is issued over DNS-01 @@ -304,12 +310,13 @@ with Cloudflare as the default provider (`CF_DNS_API_TOKEN`); any other Traefik dnsChallenge provider works by setting `ACME_DNS_PROVIDER` in `.env` and putting that provider's credential variables in `dns-credentials.env`. -In a published evaluation-capable bundle, leave -`CP_PROVIDER_MSP_LICENSE_FILE` blank and `setup.sh` self-issues a 2-client -evaluation licence. It sends only the public half of the signing key generated -on your host, exactly as the paid path does, and the private key never leaves -the machine. You can then onboard two real clients and confirm the isolation -boundary on your own infrastructure before buying. +Leave `CP_PROVIDER_MSP_LICENSE_FILE` blank and `setup.sh` self-issues a +2-client evaluation licence after configuration validation succeeds and the +immutable images are reachable. It sends only the public half of the signing +key generated on your host, the optional contact address above, and the fixed +signup-source label. The private key, client inventory, and credentials never +leave the machine. You can then onboard two real clients and confirm the +isolation boundary on your own infrastructure before buying. The evaluation licence lasts 60 days and re-running `setup.sh` reuses the one already on disk. On an air-gapped host set diff --git a/scripts/installtests/provider_msp_deploy_test.go b/scripts/installtests/provider_msp_deploy_test.go index 0b78d6c2f..8fe198e36 100644 --- a/scripts/installtests/provider_msp_deploy_test.go +++ b/scripts/installtests/provider_msp_deploy_test.go @@ -361,7 +361,13 @@ func TestProviderMSPSetupScriptSupportsUnlicensedEvaluation(t *testing.T) { "PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE", "reusing existing evaluation license", "could not reach the license server", + "PULSE_PROVIDER_MSP_EVAL_EMAIL", + "PULSE_PROVIDER_MSP_SIGNUP_SOURCE", + "eval_license_id=", ) + if strings.LastIndex(script, "pull_provider_images\n") > strings.LastIndex(script, "ensure_eval_license\n") { + t.Fatal("evaluation must be issued only after pinned provider images are reachable") + } // The install must never abort because an evaluation licence could not be // obtained. `|| true` inside the substitution does not achieve that: the @@ -399,7 +405,7 @@ func TestProviderMSPSetupScriptSupportsUnlicensedEvaluation(t *testing.T) { } } -func TestProviderMSPEvaluationDocsFailClosedUntilSignedBundlePublication(t *testing.T) { +func TestProviderMSPEvaluationDocsUsePublishedSignedBundle(t *testing.T) { repoDocBytes, err := os.ReadFile(repoFile("docs", "MSP.md")) if err != nil { t.Fatalf("read repo MSP guide: %v", err) @@ -414,14 +420,17 @@ func TestProviderMSPEvaluationDocsFailClosedUntilSignedBundlePublication(t *test doc := string(repoDocBytes) assertContainsAll(t, doc, - "Self-service evaluation is available only from an exact release page", - "three assets, stop: evaluation onboarding for that release remains", - "request-assisted. Do **not** download the moving `main` branch archive", + "signed provider bundle published", + "with Pulse v6.2.1", + "**not** download the moving `main` branch archive", + `export PULSE_VERSION=v6.2.1`, `PULSE_MSP_BUNDLE="pulse-provider-msp-${PULSE_VERSION}.tar.gz"`, `releases/download/${PULSE_VERSION}`, "ssh-keygen -Y verify", `-s "${PULSE_MSP_BUNDLE}.sshsig" < "${PULSE_MSP_BUNDLE}"`, `sha256sum -c "${PULSE_MSP_BUNDLE}.sha256"`, + `PULSE_PROVIDER_MSP_EVAL_EMAIL=you@example.com`, + `PULSE_PROVIDER_MSP_SIGNUP_SOURCE=msp_docs`, `sudo -E bash ./setup.sh`, ) assertNotContainsAny(t, doc,