From a17882458aa06e33a8e0540862cbea6ac0ca9b97 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 10 Jun 2026 14:08:52 +0100 Subject: [PATCH] Rename CP_TRIAL_ACTIVATION_PRIVATE_KEY to CP_ENTITLEMENT_SIGNING_PRIVATE_KEY in the provider MSP bundle The variable signs hosted entitlement leases; the trial-activation name is left over from the retired trial era and reads as trial machinery to an operator generating their licensing root key. The provider MSP bundle has no installed base yet, so the canonical rename is free today and frozen the moment the first design partner installs. - Control plane reads CP_ENTITLEMENT_SIGNING_PRIVATE_KEY first and falls back to CP_TRIAL_ACTIVATION_PRIVATE_KEY, so existing Pulse-hosted cloud deployments (deploy/cloud, hibernated snapshot) keep working unchanged. - deploy/provider-msp (.env.example, compose, setup.sh), MSP.md, and the install-test pins use the canonical name; error messages name it too. - deploy/cloud intentionally keeps the legacy name: that stack historically signed hosted trial activations, and its snapshot predates the rename. --- deploy/provider-msp/.env.example | 2 +- deploy/provider-msp/docker-compose.yml | 2 +- deploy/provider-msp/setup.sh | 22 ++++++------- docs/MSP.md | 2 +- internal/cloudcp/config.go | 32 +++++++++++++------ internal/cloudcp/config_test.go | 28 ++++++++++++++++ .../installtests/provider_msp_deploy_test.go | 4 +-- 7 files changed, 67 insertions(+), 25 deletions(-) diff --git a/deploy/provider-msp/.env.example b/deploy/provider-msp/.env.example index 74b4c16f4..5fb5c1cff 100644 --- a/deploy/provider-msp/.env.example +++ b/deploy/provider-msp/.env.example @@ -32,7 +32,7 @@ CP_PROVIDER_MSP_LICENSE_FILE=./provider-msp-license.jwt # `./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_ENTITLEMENT_SIGNING_PRIVATE_KEY= CP_TENANT_MEMORY_LIMIT=536870912 CP_ALLOW_DOCKERLESS_PROVISIONING=false CP_STORAGE_GUARDRAILS_ENABLED=true diff --git a/deploy/provider-msp/docker-compose.yml b/deploy/provider-msp/docker-compose.yml index eaa671429..5bbf13bde 100644 --- a/deploy/provider-msp/docker-compose.yml +++ b/deploy/provider-msp/docker-compose.yml @@ -76,7 +76,7 @@ services: - CP_PROOF_TENANT_MAX_AGE=${CP_PROOF_TENANT_MAX_AGE:-24h} - CP_PROOF_TENANT_MATCHERS=${CP_PROOF_TENANT_MATCHERS:-proof,canary,rehearsal,msp_prod,ownerseed,owner_seed} - CP_REQUIRE_EMAIL_PROVIDER=${CP_REQUIRE_EMAIL_PROVIDER:-false} - - CP_TRIAL_ACTIVATION_PRIVATE_KEY=${CP_TRIAL_ACTIVATION_PRIVATE_KEY} + - CP_ENTITLEMENT_SIGNING_PRIVATE_KEY=${CP_ENTITLEMENT_SIGNING_PRIVATE_KEY} - RESEND_API_KEY=${RESEND_API_KEY} - PULSE_EMAIL_FROM=${PULSE_EMAIL_FROM} - PULSE_EMAIL_REPLY_TO=${PULSE_EMAIL_REPLY_TO} diff --git a/deploy/provider-msp/setup.sh b/deploy/provider-msp/setup.sh index 1319601b6..b5d1b226c 100755 --- a/deploy/provider-msp/setup.sh +++ b/deploy/provider-msp/setup.sh @@ -290,15 +290,15 @@ ensure_generated_secrets() { log "generating CP_ADMIN_KEY" set_env_value CP_ADMIN_KEY "$(openssl rand -hex 32)" "${env_path}" fi - if [[ -z "$(env_value CP_TRIAL_ACTIVATION_PRIVATE_KEY "${env_path}")" ]]; then - log "generating CP_TRIAL_ACTIVATION_PRIVATE_KEY" - set_env_value CP_TRIAL_ACTIVATION_PRIVATE_KEY "$(openssl rand -base64 32 | tr -d '\n')" "${env_path}" + if [[ -z "$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")" ]]; then + log "generating CP_ENTITLEMENT_SIGNING_PRIVATE_KEY" + set_env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "$(openssl rand -base64 32 | tr -d '\n')" "${env_path}" fi 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 +# CP_ENTITLEMENT_SIGNING_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. @@ -308,8 +308,8 @@ derive_lease_signing_public_key() { 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_b64="$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")" + [[ -n "${key_b64}" ]] || die "CP_ENTITLEMENT_SIGNING_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) @@ -328,7 +328,7 @@ derive_lease_signing_public_key() { 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)" + die "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY must decode to a 32-byte seed or 64-byte Ed25519 key (got ${key_len} bytes)" ;; esac } @@ -379,7 +379,7 @@ Edit it now and set required values: - CP_TRUSTED_PROXY_CIDRS - CP_PROVIDER_MSP_LICENSE_FILE -setup.sh will generate CP_ADMIN_KEY and CP_TRIAL_ACTIVATION_PRIVATE_KEY if they +setup.sh will generate CP_ADMIN_KEY and CP_ENTITLEMENT_SIGNING_PRIVATE_KEY if they are still blank. EOF @@ -404,7 +404,7 @@ validate_env_file() { local missing=() local k v - for k in DOMAIN ACME_EMAIL CF_DNS_API_TOKEN CP_ENV TRAEFIK_IMAGE DOCKER_SOCKET_PROXY_IMAGE CONTROL_PLANE_IMAGE CP_ADMIN_KEY CP_PULSE_IMAGE PULSE_PROVIDER_MSP_DATA_DIR PULSE_PROVIDER_MSP_DOCKER_NETWORK PULSE_PROVIDER_MSP_DOCKER_SUBNET PULSE_PROVIDER_MSP_DOCKER_SOCKET PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR CP_TRUSTED_PROXY_CIDRS CP_PROVIDER_MSP_LICENSE_FILE CP_TRIAL_ACTIVATION_PRIVATE_KEY CP_TENANT_MEMORY_LIMIT CP_ALLOW_DOCKERLESS_PROVISIONING CP_STORAGE_GUARDRAILS_ENABLED CP_STORAGE_MIN_ROOT_AVAILABLE CP_STORAGE_MIN_DATA_AVAILABLE CP_STORAGE_MIN_DOCKER_AVAILABLE CP_STORAGE_MAX_DOCKER_BUILD_CACHE CP_PROOF_TENANT_MAX_AGE CP_PROOF_TENANT_MATCHERS CP_REQUIRE_EMAIL_PROVIDER PULSE_EMAIL_FROM PULSE_EMAIL_REPLY_TO; do + for k in DOMAIN ACME_EMAIL CF_DNS_API_TOKEN CP_ENV TRAEFIK_IMAGE DOCKER_SOCKET_PROXY_IMAGE CONTROL_PLANE_IMAGE CP_ADMIN_KEY CP_PULSE_IMAGE PULSE_PROVIDER_MSP_DATA_DIR PULSE_PROVIDER_MSP_DOCKER_NETWORK PULSE_PROVIDER_MSP_DOCKER_SUBNET PULSE_PROVIDER_MSP_DOCKER_SOCKET PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR CP_TRUSTED_PROXY_CIDRS CP_PROVIDER_MSP_LICENSE_FILE CP_ENTITLEMENT_SIGNING_PRIVATE_KEY CP_TENANT_MEMORY_LIMIT CP_ALLOW_DOCKERLESS_PROVISIONING CP_STORAGE_GUARDRAILS_ENABLED CP_STORAGE_MIN_ROOT_AVAILABLE CP_STORAGE_MIN_DATA_AVAILABLE CP_STORAGE_MIN_DOCKER_AVAILABLE CP_STORAGE_MAX_DOCKER_BUILD_CACHE CP_PROOF_TENANT_MAX_AGE CP_PROOF_TENANT_MATCHERS CP_REQUIRE_EMAIL_PROVIDER PULSE_EMAIL_FROM PULSE_EMAIL_REPLY_TO; do v="$(env_value "${k}" "${env_path}")" if [[ -z "${v}" ]]; then missing+=("${k}") @@ -458,9 +458,9 @@ validate_env_file() { if [[ "${#admin_key}" -lt 32 ]]; then die "CP_ADMIN_KEY must be at least 32 characters" fi - trial_key="$(env_value CP_TRIAL_ACTIVATION_PRIVATE_KEY "${env_path}")" + trial_key="$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")" if ! printf '%s' "${trial_key}" | base64 -d >/dev/null 2>&1; then - die "CP_TRIAL_ACTIVATION_PRIVATE_KEY must be valid base64" + die "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY must be valid base64" fi docker_subnet="$(env_value PULSE_PROVIDER_MSP_DOCKER_SUBNET "${env_path}")" trusted_cidrs="$(env_value CP_TRUSTED_PROXY_CIDRS "${env_path}" | tr -d '[:space:]')" diff --git a/docs/MSP.md b/docs/MSP.md index 5ab85c6d0..0df96312f 100644 --- a/docs/MSP.md +++ b/docs/MSP.md @@ -182,7 +182,7 @@ 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 +1. `setup.sh` generates `CP_ENTITLEMENT_SIGNING_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. diff --git a/internal/cloudcp/config.go b/internal/cloudcp/config.go index 27aa8474b..d3e68d4e7 100644 --- a/internal/cloudcp/config.go +++ b/internal/cloudcp/config.go @@ -262,11 +262,14 @@ func LoadConfig() (*CPConfig, error) { ProviderMSPLicenseEmail: providerMSPLicenseEmail, LicenseServerURL: envOrDefault("PULSE_LICENSE_SERVER_URL", "https://license.pulserelay.pro"), LicenseAdminToken: strings.TrimSpace(os.Getenv("PULSE_LICENSE_ADMIN_TOKEN")), - TrialActivationPrivateKey: strings.TrimSpace(os.Getenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY")), - RequireEmailProvider: envOrDefaultBool("CP_REQUIRE_EMAIL_PROVIDER", true), - ResendAPIKey: strings.TrimSpace(os.Getenv("RESEND_API_KEY")), - EmailFrom: envOrDefault("PULSE_EMAIL_FROM", "noreply@pulserelay.pro"), - EmailReplyTo: envOrDefault("PULSE_EMAIL_REPLY_TO", "support@pulserelay.pro"), + // Canonical name first; CP_TRIAL_ACTIVATION_PRIVATE_KEY is the + // legacy spelling from the retired trial-activation era, kept as a + // fallback for already-deployed control planes. + TrialActivationPrivateKey: envFirstNonEmpty("CP_ENTITLEMENT_SIGNING_PRIVATE_KEY", "CP_TRIAL_ACTIVATION_PRIVATE_KEY"), + RequireEmailProvider: envOrDefaultBool("CP_REQUIRE_EMAIL_PROVIDER", true), + ResendAPIKey: strings.TrimSpace(os.Getenv("RESEND_API_KEY")), + EmailFrom: envOrDefault("PULSE_EMAIL_FROM", "noreply@pulserelay.pro"), + EmailReplyTo: envOrDefault("PULSE_EMAIL_REPLY_TO", "support@pulserelay.pro"), } if strings.TrimSpace(cfg.TrialActivationPrivateKey) != "" { publicKey, err := deriveTrialActivationPublicKey(cfg.TrialActivationPrivateKey) @@ -300,7 +303,7 @@ func (c *CPConfig) validateProviderMSPLeaseSigningBinding() error { } boundKey := strings.TrimSpace(c.ProviderMSPLeaseSigningPublicKey) if boundKey == "" { - return fmt.Errorf("CP_PROVIDER_MSP_LICENSE_FILE does not bind an entitlement lease signing key (entitlement_signing_public_key); request an updated provider MSP license bound to this control plane's CP_TRIAL_ACTIVATION_PRIVATE_KEY public key %q", c.TrialActivationPublicKey) + return fmt.Errorf("CP_PROVIDER_MSP_LICENSE_FILE does not bind an entitlement lease signing key (entitlement_signing_public_key); request an updated provider MSP license bound to this control plane's CP_ENTITLEMENT_SIGNING_PRIVATE_KEY public key %q", c.TrialActivationPublicKey) } bound, err := pkglicensing.DecodePublicKey(boundKey) if err != nil { @@ -311,7 +314,7 @@ func (c *CPConfig) validateProviderMSPLeaseSigningBinding() error { return fmt.Errorf("derive trial activation public key: %w", err) } if !bound.Equal(derived) { - return fmt.Errorf("CP_TRIAL_ACTIVATION_PRIVATE_KEY does not match the entitlement lease signing key bound in CP_PROVIDER_MSP_LICENSE_FILE (license binds %q, control plane derives %q); use the keypair the license was issued for, or request a license bound to this key", boundKey, c.TrialActivationPublicKey) + return fmt.Errorf("CP_ENTITLEMENT_SIGNING_PRIVATE_KEY does not match the entitlement lease signing key bound in CP_PROVIDER_MSP_LICENSE_FILE (license binds %q, control plane derives %q); use the keypair the license was issued for, or request a license bound to this key", boundKey, c.TrialActivationPublicKey) } return nil } @@ -411,7 +414,7 @@ func (c *CPConfig) validate() error { return fmt.Errorf("CP_MSP_*_PRICE_ID must not be configured when CP_CONTROL_PLANE_MODE=%s", c.ControlPlaneMode) } if strings.TrimSpace(c.TrialActivationPrivateKey) == "" { - return fmt.Errorf("CP_TRIAL_ACTIVATION_PRIVATE_KEY is required when CP_CONTROL_PLANE_MODE=%s", c.ControlPlaneMode) + return fmt.Errorf("CP_ENTITLEMENT_SIGNING_PRIVATE_KEY is required when CP_CONTROL_PLANE_MODE=%s", c.ControlPlaneMode) } if c.Environment == "production" && strings.TrimSpace(c.ProviderMSPLicenseFile) == "" { return fmt.Errorf("CP_PROVIDER_MSP_LICENSE_FILE is required in production when CP_CONTROL_PLANE_MODE=%s", c.ControlPlaneMode) @@ -464,7 +467,7 @@ func (c *CPConfig) validate() error { return fmt.Errorf("CP_TRIAL_SIGNUP_PRICE_ID is required when STRIPE_API_KEY is configured") } if strings.TrimSpace(c.StripeAPIKey) != "" && strings.TrimSpace(c.TrialActivationPrivateKey) == "" { - return fmt.Errorf("CP_TRIAL_ACTIVATION_PRIVATE_KEY is required when STRIPE_API_KEY is configured") + return fmt.Errorf("CP_ENTITLEMENT_SIGNING_PRIVATE_KEY is required when STRIPE_API_KEY is configured") } if err := validateCloudStripePriceID(c.Environment, c.StripeAPIKey, "CP_TRIAL_SIGNUP_PRICE_ID", c.TrialSignupPriceID, "cloud_starter"); err != nil { return err @@ -659,6 +662,17 @@ func stripeSecretKeyMode(raw string) string { } } +// envFirstNonEmpty returns the first environment variable in keys with a +// non-empty trimmed value. +func envFirstNonEmpty(keys ...string) string { + for _, key := range keys { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return v + } + } + return "" +} + func envOrDefault(key, fallback string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v diff --git a/internal/cloudcp/config_test.go b/internal/cloudcp/config_test.go index 88795c08b..bff70ca73 100644 --- a/internal/cloudcp/config_test.go +++ b/internal/cloudcp/config_test.go @@ -927,3 +927,31 @@ func TestStripeSecretKeyMode(t *testing.T) { }) } } + +func TestLoadConfig_EntitlementSigningKeyNamePrecedence(t *testing.T) { + setRequiredCPEnv(t) + // Canonical name wins over the legacy trial-era spelling. + t.Setenv("CP_ENTITLEMENT_SIGNING_PRIVATE_KEY", "A8medgdNdm12GXfTXWo6+TMZ2BeHPCLg2kd0znn6ZUk=") + t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "not-even-base64") + + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig: %v", err) + } + if cfg.TrialActivationPublicKey != base64.StdEncoding.EncodeToString(trialSigningEnvPublicKey(t)) { + t.Fatalf("canonical CP_ENTITLEMENT_SIGNING_PRIVATE_KEY was not preferred; derived %q", cfg.TrialActivationPublicKey) + } +} + +func TestLoadConfig_LegacyTrialActivationKeyNameStillWorks(t *testing.T) { + setRequiredCPEnv(t) + t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "A8medgdNdm12GXfTXWo6+TMZ2BeHPCLg2kd0znn6ZUk=") + + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig: %v", err) + } + if cfg.TrialActivationPublicKey == "" { + t.Fatal("legacy CP_TRIAL_ACTIVATION_PRIVATE_KEY fallback no longer derives a key") + } +} diff --git a/scripts/installtests/provider_msp_deploy_test.go b/scripts/installtests/provider_msp_deploy_test.go index 20c520631..9e761cdcb 100644 --- a/scripts/installtests/provider_msp_deploy_test.go +++ b/scripts/installtests/provider_msp_deploy_test.go @@ -75,7 +75,7 @@ func TestProviderMSPDeployEnvExampleMatchesBootstrapPath(t *testing.T) { "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", - "CP_TRIAL_ACTIVATION_PRIVATE_KEY=", + "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY=", "sudo -E ./setup.sh", "docker compose run --rm control-plane provider-msp bootstrap", "docker compose run --rm control-plane provider-msp preflight", @@ -132,7 +132,7 @@ func TestProviderMSPSetupScriptMatchesProviderContract(t *testing.T) { "run-install-proof.sh", "upgrade.sh", "CP_PROVIDER_MSP_LICENSE_FILE", - "CP_TRIAL_ACTIVATION_PRIVATE_KEY", + "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY", "PULSE_PROVIDER_MSP_DATA_DIR", "PULSE_PROVIDER_MSP_DOCKER_NETWORK", "PULSE_PROVIDER_MSP_DOCKER_SUBNET",