refactor(security): per-tenant JWT secret instead of shared global secret
Generate a unique JWT secret per tenant at provision time, stored on TenantEntity (same pattern as dbPassword). On upgrade, the existing secret is reused so agent tokens survive container recreation. - V005 migration: add jwt_secret column to tenants table - TenantEntity: add jwtSecret field - TenantProvisionRequest: add jwtSecret field - VendorTenantService: generate secret in provisionAsync(), reuse on upgrade - DockerTenantProvisioner: read from req.jwtSecret() not props - ProvisioningProperties: remove jwtSecret (no longer global config) Installer team: CAMELEER_SERVER_SECURITY_JWTSECRET and CAMELEER_SAAS_PROVISIONING_JWTSECRET can be removed from compose templates and .env — no longer consumed by the SaaS app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,6 @@ public class DockerTenantProvisioner implements TenantProvisioner {
|
||||
.responseTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
this.docker = DockerClientImpl.getInstance(config, httpClient);
|
||||
if (props.jwtSecret() == null || props.jwtSecret().isBlank()) {
|
||||
log.warn("CAMELEER_SAAS_PROVISIONING_JWTSECRET is not set — provisioned servers will fail to start");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -223,7 +220,7 @@ public class DockerTenantProvisioner implements TenantProvisioner {
|
||||
"CAMELEER_SERVER_CLICKHOUSE_PASSWORD=" + props.clickhousePassword(),
|
||||
"CAMELEER_SERVER_TENANT_ID=" + slug,
|
||||
"CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN=" + req.licenseToken(),
|
||||
"CAMELEER_SERVER_SECURITY_JWTSECRET=" + props.jwtSecret(),
|
||||
"CAMELEER_SERVER_SECURITY_JWTSECRET=" + req.jwtSecret(),
|
||||
"CAMELEER_SERVER_SECURITY_OIDC_ISSUERURI=" + props.oidcIssuerUri(),
|
||||
"CAMELEER_SERVER_SECURITY_OIDC_JWKSETURI=" + props.oidcJwkSetUri(),
|
||||
"CAMELEER_SERVER_SECURITY_OIDC_AUDIENCE=https://api.cameleer.local",
|
||||
|
||||
@@ -19,6 +19,5 @@ public record ProvisioningProperties(
|
||||
String clickhousePassword,
|
||||
String oidcIssuerUri,
|
||||
String oidcJwkSetUri,
|
||||
String corsOrigins,
|
||||
String jwtSecret
|
||||
String corsOrigins
|
||||
) {}
|
||||
|
||||
@@ -7,5 +7,6 @@ public record TenantProvisionRequest(
|
||||
String slug,
|
||||
String tier,
|
||||
String licenseToken,
|
||||
String dbPassword
|
||||
String dbPassword,
|
||||
String jwtSecret
|
||||
) {}
|
||||
|
||||
@@ -61,6 +61,9 @@ public class TenantEntity {
|
||||
@Column(name = "db_password")
|
||||
private String dbPassword;
|
||||
|
||||
@Column(name = "jwt_secret")
|
||||
private String jwtSecret;
|
||||
|
||||
@Column(name = "admin_email")
|
||||
private String adminEmail;
|
||||
|
||||
@@ -108,6 +111,8 @@ public class TenantEntity {
|
||||
public void setProvisionError(String provisionError) { this.provisionError = provisionError; }
|
||||
public String getDbPassword() { return dbPassword; }
|
||||
public void setDbPassword(String dbPassword) { this.dbPassword = dbPassword; }
|
||||
public String getJwtSecret() { return jwtSecret; }
|
||||
public void setJwtSecret(String jwtSecret) { this.jwtSecret = jwtSecret; }
|
||||
public String getAdminEmail() { return adminEmail; }
|
||||
public void setAdminEmail(String adminEmail) { this.adminEmail = adminEmail; }
|
||||
public Instant getCaAppliedAt() { return caAppliedAt; }
|
||||
|
||||
@@ -190,16 +190,23 @@ public class VendorTenantService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store DB password on entity
|
||||
// Store DB password + generate JWT secret on entity
|
||||
TenantEntity tenantForDb = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenantForDb == null) {
|
||||
log.error("Tenant {} disappeared during provisioning", slug);
|
||||
return;
|
||||
}
|
||||
tenantForDb.setDbPassword(dbPassword);
|
||||
// Reuse existing JWT secret on upgrade, generate on first provision
|
||||
String jwtSecret = tenantForDb.getJwtSecret();
|
||||
if (jwtSecret == null || jwtSecret.isBlank()) {
|
||||
jwtSecret = java.util.UUID.randomUUID().toString().replace("-", "")
|
||||
+ java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
tenantForDb.setJwtSecret(jwtSecret);
|
||||
}
|
||||
tenantRepository.save(tenantForDb);
|
||||
|
||||
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword);
|
||||
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword, jwtSecret);
|
||||
ProvisionResult result = tenantProvisioner.provision(provisionRequest);
|
||||
|
||||
TenantEntity tenant = tenantRepository.findById(tenantId).orElse(null);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE tenants ADD COLUMN jwt_secret VARCHAR(255);
|
||||
Reference in New Issue
Block a user