Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/templates/cockroachdb-legacy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ tls:
# Image for the selfSigner utility.
image:
repository: cockroachdb/cockroach-self-signer-cert
tag: "1.11"
tag: "1.12"
pullPolicy: IfNotPresent
registry: docker.io
credentials: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ cockroachdb:
# repository defines the image repository for the self-signer container.
repository: cockroachdb/cockroach-self-signer-cert
# tag defines the image tag for the self-signer container.
tag: "1.11"
tag: "1.12"
# pullPolicy defines the image pull policy for the self-signer container.
pullPolicy: IfNotPresent
# registry defines the container registry host (e.g., docker.io).
Expand Down
17 changes: 12 additions & 5 deletions cmd/migrate/cockroachdb-operator/migrate-certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ var migrateCertsCmd = &cobra.Command{
This command performs the following operations:
1. Moves the existing CA certificate from a Kubernetes Secret to a ConfigMap because the operator
expects the CA certificate to be in a ConfigMap.
2. Regenerates node certificates to include Subject Alternative Names (SAN) for the CockroachDB join service
3. Generates client certificates if they are not already present in the cluster
2. Regenerates node certificates to include Subject Alternative Names (SAN) for the CockroachDB join service,
keeping any additional SANs the existing node certificate carries
3. Reissues the client certificate

The command supports customization of certificate durations and expiry windows for:
- Node certificates (default: 1 year duration, 7 days expiry window)
- Client certificates (default: 28 days duration, 2 days expiry window)
- Client certificates (default: same duration as node certificates, 2 days expiry window)

The migration process ensures all certificates are properly configured for use with the CockroachDB Operator.`,
RunE: migrateCertsForCockroachDbOperator,
Expand All @@ -48,17 +49,23 @@ func init() {

migrateCertsCmd.PersistentFlags().StringVar(&caSecret, "ca-secret", "", "name of user provided CA secret")

migrateCertsCmd.PersistentFlags().StringVar(&nodeDuration, "node-duration", "8760h", "duration of Node cert. Defaults to 365h (1 year)")
migrateCertsCmd.PersistentFlags().StringVar(&nodeDuration, "node-duration", "8760h", "duration of Node cert. Defaults to 8760h (1 year)")
migrateCertsCmd.PersistentFlags().StringVar(&nodeExpiry, "node-expiry", "168h", "expiry window for Node cert. Defaults to 7 days")

migrateCertsCmd.PersistentFlags().StringVar(&clientDuration, "client-duration", "672h", "duration of Client cert. Defaults to 28 days")
migrateCertsCmd.PersistentFlags().StringVar(&clientDuration, "client-duration", "", "duration of Client cert. Defaults to --node-duration, because nothing rotates the client cert after migration")
migrateCertsCmd.PersistentFlags().StringVar(&clientExpiry, "client-expiry", "48h", "expiry window for Client(root) cert. Defaults to 2 days")

migrateCertsCmd.PersistentFlags().StringVar(&clusterDomain, "cluster-domain", "cluster.local", "cluster domain")
_ = migrateCertsCmd.MarkFlagRequired("statefulset-name")
}

func migrateCertsForCockroachDbOperator(cmd *cobra.Command, args []string) error {
// Without the self-signer nothing rotates the client certificate, so give
// it the node certificate's lifetime unless the user chose otherwise.
if clientDuration == "" {
clientDuration = nodeDuration
}

runtimeScheme := runtime.NewScheme()
if err := certv1.AddToScheme(runtimeScheme); err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions cockroachdb-legacy/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ All notable changes to the CockroachDB Helm chart will be documented in this fil

### Changed
- Updated the self-signer image to `1.11` for operator-managed certificate rotation fixes.
- Updated the self-signer image to `1.12`: `migrate-certs` now carries the node certificate's
additional SANs over to the operator-managed secrets and reissues the root client certificate.

## [22.0.4] 2026-09-23
### Changed
Expand Down
2 changes: 1 addition & 1 deletion cockroachdb-legacy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ tls:
# Image for the selfSigner utility.
image:
repository: cockroachdb/cockroach-self-signer-cert
tag: "1.11"
tag: "1.12"
pullPolicy: IfNotPresent
registry: docker.io
credentials: {}
Expand Down
2 changes: 2 additions & 0 deletions cockroachdb-operator/charts/cockroachdb/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
leaving the CA ConfigMap stale, and attempting StatefulSet restarts in deployments
that do not use StatefulSets.
- Updated the self-signer image to `1.11`, which includes these fixes.
- Updated the self-signer image to `1.12`: `migrate-certs` now carries the node certificate's
additional SANs over to the operator-managed secrets and reissues the root client certificate.

### Upgrade notes
- Node certificates rotated by affected self-signer versions may be missing join-service
Expand Down
2 changes: 1 addition & 1 deletion cockroachdb-operator/charts/cockroachdb/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ cockroachdb:
# repository defines the image repository for the self-signer container.
repository: cockroachdb/cockroach-self-signer-cert
# tag defines the image tag for the self-signer container.
tag: "1.11"
tag: "1.12"
# pullPolicy defines the image pull policy for the self-signer container.
pullPolicy: IfNotPresent
# registry defines the container registry host (e.g., docker.io).
Expand Down
8 changes: 8 additions & 0 deletions docs/migration/helm/manual_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ uploads the required resources:
bin/migration-helper migrate-certs --statefulset-name $STS_NAME --namespace $NAMESPACE
```

`migrate-certs` regenerates the node certificate with the join-service names the operator requires and
carries over any additional SANs the existing node certificate already had; it logs the names it carried
over. It also reissues the root client certificate for the node certificate's duration (`--client-duration`
defaults to `--node-duration`), because the migrated cluster's values disable the self-signer and nothing
rotates certificates until you re-enable it. Before you enable `cockroachdb.tls.selfSigner.enabled: true`,
copy the carried-over names into `cockroachdb.tls.selfSigner.additionalSANs`; the self-signer rebuilds
certificates from configuration, not from the previous certificate.

Generate a manifest for each CrdbNode and the CrdbCluster from the current StatefulSet. The
generated resources retain the pod and PVC names, allowing operator-managed pods to reuse the
existing storage without replicating data into empty volumes:
Expand Down
8 changes: 8 additions & 0 deletions docs/migration/operator/manual_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ the required resources:
bin/migration-helper migrate-certs --statefulset-name $CRDBCLUSTER --namespace $NAMESPACE
```

`migrate-certs` regenerates the node certificate with the join-service names the operator requires and
carries over any additional SANs the existing node certificate already had; it logs the names it carried
over. It also reissues the root client certificate for the node certificate's duration (`--client-duration`
defaults to `--node-duration`), because the migrated cluster's values disable the self-signer and nothing
rotates certificates until you re-enable it. Before you enable `cockroachdb.tls.selfSigner.enabled: true`,
copy the carried-over names into `cockroachdb.tls.selfSigner.additionalSANs`; the self-signer rebuilds
certificates from configuration, not from the previous certificate.

Generate a manifest for each CrdbNode and the CrdbCluster from the current StatefulSet. The
generated resources retain the pod and PVC names, allowing operator-managed pods to reuse the
existing storage without replicating data into empty volumes:
Expand Down
204 changes: 131 additions & 73 deletions pkg/generator/generate_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,86 +366,97 @@ func (rc *GenerateCert) GenerateClientCert(ctx context.Context, clientSecretName
return errors.Wrap(err, "failed to get client secret")
}

// inline func used to generate client cert and key
generate := func(rc *GenerateCert, clientSecretName, namespace string) error {
logrus.Info("Generating client certificate")
// check if the existing is ready to be consumed. If found ready, skip cert generation
if secret.Ready() && secret.ValidateAnnotations() {

// Create the user for the certificate
u := &security.SQLUsername{
U: user,
if rc.RotateClientCert {
isRequired, reason := secret.IsRotationRequired(rc.ClientCertConfig.Duration, rc.NodeAndClientCronSchedule)
if isRequired {
logrus.Infof("Client Certificate: %s", reason)
return rc.generateClientCert(ctx, user, clientSecretName, namespace)
}
}

// Create the client certificates
if err = errors.Wrap(
security.CreateClientPair(
rc.CertsDir,
rc.CAKey,
keySize,
rc.ClientCertConfig.Duration,
overwriteFiles,
*u,
generatePKCS8Key),
"failed to generate client certificate and key"); err != nil {
return err
}
logrus.Infof("Client secret [%s] is found in ready state, skipping Client cert generation", clientSecretName)
return nil
}

// Load the CA certificate into memory
ca, err := os.ReadFile(filepath.Join(rc.CertsDir, resource.CaCert))
if err != nil {
return errors.Wrap(err, "unable to read ca.crt")
}
return rc.generateClientCert(ctx, user, clientSecretName, namespace)
}

// Load the client user certificate into memory
userCertFile := fmt.Sprintf("client.%s.crt", user)
pemCert, err := os.ReadFile(filepath.Join(rc.CertsDir, userCertFile))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to read %s", userCertFile))
}
// generateClientCert issues the client certificate for user and stores it in
// clientSecretName, replacing whatever the secret held. GenerateClientCert
// calls it when no ready secret exists or rotation is due.
func (rc *GenerateCert) generateClientCert(ctx context.Context, user, clientSecretName, namespace string) error {
logrus.Info("Generating client certificate")

validFrom, validUpto, err := rc.getCertLife(pemCert)
if err != nil {
return err

}
// Create the user for the certificate
u := &security.SQLUsername{
U: user,
}

// Load the client root key into memory
userKeyFile := fmt.Sprintf("client.%s.key", user)
pemKey, err := os.ReadFile(filepath.Join(rc.CertsDir, userKeyFile))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to read %s", userKeyFile))
}
// Create the client certificates
if err := errors.Wrap(
security.CreateClientPair(
rc.CertsDir,
rc.CAKey,
keySize,
rc.ClientCertConfig.Duration,
overwriteFiles,
*u,
generatePKCS8Key),
"failed to generate client certificate and key"); err != nil {
return err
}

// add certificate info in the secret annotations
annotations := resource.GetSecretAnnotations(validFrom, validUpto, rc.ClientCertConfig.Duration.String())
// Load the CA certificate into memory
ca, err := os.ReadFile(filepath.Join(rc.CertsDir, resource.CaCert))
if err != nil {
return errors.Wrap(err, "unable to read ca.crt")
}

// create and save the TLS certificates into a secret
secret = resource.CreateTLSSecret(clientSecretName, corev1.SecretTypeTLS,
resource.NewKubeResource(ctx, rc.client, namespace, kube.DefaultPersister))
// Load the client user certificate into memory
userCertFile := fmt.Sprintf("client.%s.crt", user)
pemCert, err := os.ReadFile(filepath.Join(rc.CertsDir, userCertFile))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to read %s", userCertFile))
}

if err = secret.UpdateTLSSecret(pemCert, pemKey, ca, annotations); err != nil {
return errors.Wrap(err, "failed to update client TLS secret certs")
}
validFrom, validUpto, err := rc.getCertLife(pemCert)
if err != nil {
return err
}

logrus.Infof("Generated and saved client key and certificate in secret [%s]", clientSecretName)
return nil
// Load the client root key into memory
userKeyFile := fmt.Sprintf("client.%s.key", user)
pemKey, err := os.ReadFile(filepath.Join(rc.CertsDir, userKeyFile))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to read %s", userKeyFile))
}

// check if the existing is ready to be consumed. If found ready, skip cert generation
if secret.Ready() && secret.ValidateAnnotations() {
// add certificate info in the secret annotations
annotations := resource.GetSecretAnnotations(validFrom, validUpto, rc.ClientCertConfig.Duration.String())

if rc.RotateClientCert {
isRequired, reason := secret.IsRotationRequired(rc.ClientCertConfig.Duration, rc.NodeAndClientCronSchedule)
if isRequired {
logrus.Infof("Client Certificate: %s", reason)
return generate(rc, clientSecretName, namespace)
}
}
// create and save the TLS certificates into a secret
secret := resource.CreateTLSSecret(clientSecretName, corev1.SecretTypeTLS,
resource.NewKubeResource(ctx, rc.client, namespace, kube.DefaultPersister))

logrus.Infof("Client secret [%s] is found in ready state, skipping Client cert generation", clientSecretName)
return nil
if err = secret.UpdateTLSSecret(pemCert, pemKey, ca, annotations); err != nil {
return errors.Wrap(err, "failed to update client TLS secret certs")
}

return generate(rc, clientSecretName, namespace)
logrus.Infof("Generated and saved client key and certificate in secret [%s]", clientSecretName)
return nil
}

// RegenerateClientCert issues a new client certificate for user even when a
// ready one exists, unlike GenerateClientCert, which keeps a ready secret and
// reads the user from USER_NAME. Callers that cannot rely on later rotation
// (see pkg/migrate) use it to reset the certificate's lifetime; they name the
// user explicitly so the ambient environment cannot redirect the write to a
// different secret.
func (rc *GenerateCert) RegenerateClientCert(ctx context.Context, user, clientSecretName, namespace string) error {
return rc.generateClientCert(ctx, user, clientSecretName, namespace)
}

func (rc *GenerateCert) getCASecretName() string {
Expand Down Expand Up @@ -544,12 +555,15 @@ func (rc *GenerateCert) LoadCASecret(ctx context.Context, namespace string) erro
return nil
}

// GenerateNodeCert generates the Node key and certificate and stores them in a secret.
func (rc *GenerateCert) GenerateNodeCert(ctx context.Context, nodeSecretName, namespace string) error {
logrus.Info("Generating node certificate")

// hosts are the various DNS names and IP address that have to exist in the Node certificates
// for the database to function
// NodeCertHosts returns the DNS names and IP addresses every node certificate
// must carry for the database to function: the loopback names, the public
// service, the per-pod discovery wildcards and, for operator-managed clusters,
// the join service nodes use to find each other. It is the single definition
// of the standard host list; anything that must tell standard SANs from
// user-added ones builds on it. User-provided AdditionalSANs are appended by
// GenerateNodeCert, not here, so a caller can tell the identity the chart
// requires from the identity the user added.
func (rc *GenerateCert) NodeCertHosts(namespace string) []string {
hosts := []string{
"localhost",
"127.0.0.1",
Expand All @@ -560,16 +574,60 @@ func (rc *GenerateCert) GenerateNodeCert(ctx context.Context, nodeSecretName, na
fmt.Sprintf("*.%s.%s", rc.DiscoveryServiceName, namespace),
fmt.Sprintf("*.%s.%s.svc.%s", rc.DiscoveryServiceName, namespace, rc.ClusterDomain),
}

if rc.OperatorManaged {
operatorJoinServiceHosts := []string{
hosts = append(hosts,
fmt.Sprintf("%s-join", rc.DiscoveryServiceName),
fmt.Sprintf("%s-join.%s", rc.DiscoveryServiceName, namespace),
fmt.Sprintf("%s-join.%s.svc.%s", rc.DiscoveryServiceName, namespace, rc.ClusterDomain),
}
)
}
return hosts
}

hosts = append(hosts, operatorJoinServiceHosts...)
// InheritSANs adds to AdditionalSANs every DNS name and IP address in pemCert
// that is not in NodeCertHosts(namespace) and not already present. A path
// that reissues an existing node certificate (today, the migration helper)
// uses it to keep the identity the certificate already had without knowing
// which configuration produced it: the certificate is the record, not the
// values that generated it.
func (rc *GenerateCert) InheritSANs(pemCert []byte, namespace string) error {
cert, err := security.GetCertObj(pemCert)
if err != nil {
return errors.Wrap(err, "parsing existing node certificate")
}
known := make(map[string]struct{})
for _, h := range rc.NodeCertHosts(namespace) {
known[h] = struct{}{}
}
for _, s := range rc.AdditionalSANs {
known[s] = struct{}{}
}
var extra []string
add := func(s string) {
if _, ok := known[s]; ok {
return
}
known[s] = struct{}{}
extra = append(extra, s)
}
for _, name := range cert.DNSNames {
add(name)
}
for _, ip := range cert.IPAddresses {
add(ip.String())
}
if len(extra) > 0 {
logrus.Infof("Inheriting SANs from the existing node certificate: %v", extra)
rc.AdditionalSANs = append(rc.AdditionalSANs, extra...)
}
return nil
}

// GenerateNodeCert generates the Node key and certificate and stores them in a secret.
func (rc *GenerateCert) GenerateNodeCert(ctx context.Context, nodeSecretName, namespace string) error {
logrus.Info("Generating node certificate")

hosts := rc.NodeCertHosts(namespace)

// Append additional user-provided SANs
if len(rc.AdditionalSANs) > 0 {
Expand Down
Loading
Loading