Make provider MSP evaluation activation attributable

This commit is contained in:
rcourtman
2026-08-11 14:24:47 +01:00
parent 52489e8926
commit 9449a6909e
7 changed files with 130 additions and 55 deletions
+6
View File
@@ -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:
+22 -5
View File
@@ -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
}
+26 -19
View File
@@ -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
@@ -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
@@ -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`
+26 -19
View File
@@ -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
@@ -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,