fix(cloudcp): inject hosted entitlement public key

This commit is contained in:
rcourtman 2026-03-27 10:27:52 +00:00
parent c053c6c86b
commit 53154ccb8a
5 changed files with 60 additions and 18 deletions

View file

@ -1,6 +1,8 @@
package cloudcp
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"net/url"
"os"
@ -9,6 +11,7 @@ import (
"strings"
"github.com/joho/godotenv"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
)
// CPConfig holds all configuration for the control plane.
@ -39,6 +42,7 @@ type CPConfig struct {
CloudPowerPriceID string // Cloud Power tier price ID (optional)
CloudMaxPriceID string // Cloud Max tier price ID (optional)
TrialActivationPrivateKey string
TrialActivationPublicKey string
RequireEmailProvider bool
ResendAPIKey string // Resend API key (optional — if empty, emails are logged)
EmailFrom string // Sender email address (e.g. "noreply@pulserelay.pro")
@ -128,6 +132,13 @@ func LoadConfig() (*CPConfig, error) {
ResendAPIKey: strings.TrimSpace(os.Getenv("RESEND_API_KEY")),
EmailFrom: envOrDefault("PULSE_EMAIL_FROM", "noreply@pulserelay.pro"),
}
if strings.TrimSpace(cfg.TrialActivationPrivateKey) != "" {
publicKey, err := deriveTrialActivationPublicKey(cfg.TrialActivationPrivateKey)
if err != nil {
return nil, fmt.Errorf("derive trial activation public key: %w", err)
}
cfg.TrialActivationPublicKey = publicKey
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("validate control plane config: %w", err)
@ -135,6 +146,18 @@ func LoadConfig() (*CPConfig, error) {
return cfg, nil
}
func deriveTrialActivationPublicKey(encodedPrivateKey string) (string, error) {
privateKey, err := pkglicensing.DecodeEd25519PrivateKey(strings.TrimSpace(encodedPrivateKey))
if err != nil {
return "", err
}
publicKey, ok := privateKey.Public().(ed25519.PublicKey)
if !ok || len(publicKey) != ed25519.PublicKeySize {
return "", fmt.Errorf("invalid derived trial activation public key")
}
return base64.StdEncoding.EncodeToString(publicKey), nil
}
func (c *CPConfig) validate() error {
var missing []string
if c.AdminKey == "" {

View file

@ -16,7 +16,7 @@ func setRequiredCPEnv(t *testing.T) {
func setTrialSigningEnv(t *testing.T) {
t.Helper()
t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "test-activation-key")
t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "A8medgdNdm12GXfTXWo6+TMZ2BeHPCLg2kd0znn6ZUk=")
}
func TestLoadConfig_MissingRequired(t *testing.T) {
@ -94,6 +94,19 @@ func TestLoadConfig_CustomValues(t *testing.T) {
}
}
func TestLoadConfig_DerivesTrialActivationPublicKey(t *testing.T) {
setRequiredCPEnv(t)
setTrialSigningEnv(t)
cfg, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
if strings.TrimSpace(cfg.TrialActivationPublicKey) == "" {
t.Fatal("TrialActivationPublicKey = empty, want derived public key")
}
}
func TestLoadConfig_TrustedProxyCIDRs(t *testing.T) {
setRequiredCPEnv(t)
t.Setenv("CP_TRUSTED_PROXY_CIDRS", "172.18.0.0/16, 127.0.0.1/32")

View file

@ -21,13 +21,14 @@ import (
// ManagerConfig holds Docker manager settings.
type ManagerConfig struct {
Image string
Network string
BaseDomain string
TrustedProxyCIDRs []string
MemoryLimit int64 // bytes
CPUShares int64
ContainerPort int // port inside the container (default 7655)
Image string
Network string
BaseDomain string
TrialActivationPublicKey string
TrustedProxyCIDRs []string
MemoryLimit int64 // bytes
CPUShares int64
ContainerPort int // port inside the container (default 7655)
}
// Manager orchestrates Docker containers for tenant lifecycle.
@ -92,7 +93,7 @@ func (m *Manager) CreateAndStart(ctx context.Context, tenantID, tenantDataDir st
&container.Config{
Image: m.cfg.Image,
Labels: labels,
Env: tenantEnv(tenantID, m.cfg.BaseDomain, m.tenantTrustedProxyCIDRs(ctx)),
Env: tenantEnv(tenantID, m.cfg.BaseDomain, m.cfg.TrialActivationPublicKey, m.tenantTrustedProxyCIDRs(ctx)),
},
&container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
@ -142,7 +143,7 @@ func tenantRuntimeOwnershipPaths() []string {
}
}
func tenantEnv(tenantID, baseDomain string, trustedProxyCIDRs []string) []string {
func tenantEnv(tenantID, baseDomain, trialActivationPublicKey string, trustedProxyCIDRs []string) []string {
publicURL := ""
tenantID = strings.TrimSpace(tenantID)
baseDomain = strings.TrimSpace(baseDomain)
@ -162,6 +163,9 @@ func tenantEnv(tenantID, baseDomain string, trustedProxyCIDRs []string) []string
if publicURL != "" {
env = append(env, "PULSE_PUBLIC_URL="+publicURL)
}
if strings.TrimSpace(trialActivationPublicKey) != "" {
env = append(env, "PULSE_TRIAL_ACTIVATION_PUBLIC_KEY="+strings.TrimSpace(trialActivationPublicKey))
}
if len(trustedProxyCIDRs) > 0 {
env = append(env, "PULSE_TRUSTED_PROXY_CIDRS="+strings.Join(trustedProxyCIDRs, ","))
}

View file

@ -70,7 +70,7 @@ func TestTenantMountsKeepImmutableFilesReadOnly(t *testing.T) {
func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
t.Parallel()
env := tenantEnv("t-example", "cloud.pulserelay.pro", []string{"172.18.0.0/16", "127.0.0.1/32"})
env := tenantEnv("t-example", "cloud.pulserelay.pro", "pubkey-123", []string{"172.18.0.0/16", "127.0.0.1/32"})
want := map[string]bool{
"PULSE_DATA_DIR=/etc/pulse": true,
"PULSE_HOSTED_MODE=true": true,
@ -79,6 +79,7 @@ func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
"PUID=1000": true,
"PGID=1000": true,
"PULSE_PUBLIC_URL=https://t-example.cloud.pulserelay.pro": true,
"PULSE_TRIAL_ACTIVATION_PUBLIC_KEY=pubkey-123": true,
"PULSE_TRUSTED_PROXY_CIDRS=172.18.0.0/16,127.0.0.1/32": true,
immutableOwnershipPathsEnv + "=/etc/pulse/secrets/handoff.key:/etc/pulse/.cloud_handoff_key": true,
}
@ -95,7 +96,7 @@ func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
func TestTenantEnvOmitsPublicURLWithoutTenantContext(t *testing.T) {
t.Parallel()
env := tenantEnv("", "", nil)
env := tenantEnv("", "", "", nil)
sawTenantID := false
for _, item := range env {
if strings.HasPrefix(item, "PULSE_PUBLIC_URL=") {

View file

@ -53,12 +53,13 @@ func Run(ctx context.Context, version string) error {
// Initialize Docker manager (best-effort — control plane can run without Docker for dev/testing)
var dockerMgr *cpDocker.Manager
dockerMgr, err = cpDocker.NewManager(cpDocker.ManagerConfig{
Image: cfg.PulseImage,
Network: cfg.DockerNetwork,
BaseDomain: baseDomainFromURL(cfg.BaseURL),
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
MemoryLimit: cfg.TenantMemoryLimit,
CPUShares: cfg.TenantCPUShares,
Image: cfg.PulseImage,
Network: cfg.DockerNetwork,
BaseDomain: baseDomainFromURL(cfg.BaseURL),
TrialActivationPublicKey: cfg.TrialActivationPublicKey,
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
MemoryLimit: cfg.TenantMemoryLimit,
CPUShares: cfg.TenantCPUShares,
})
if err != nil {
log.Warn().Err(err).Msg("Docker unavailable — container management disabled")