Skip to content

Commit 5acd701

Browse files
authored
K8SPG-1017 replication certificate was not covered by cert manager path (#1599)
1 parent 00705dc commit 5acd701

6 files changed

Lines changed: 256 additions & 0 deletions

File tree

e2e-tests/tests/cert-manager-tls/10-verify-cert-manager-resources.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ commands:
4949
exit 1
5050
fi
5151
52+
replication_cert_ready=$(kubectl -n "$NAMESPACE" get certificate cert-manager-tls-replication-cert -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')
53+
if [[ "$replication_cert_ready" != "True" ]]; then
54+
echo "Replication Certificate is not ready: $replication_cert_ready"
55+
exit 1
56+
fi
57+
5258
instance_sts=$(kubectl -n "$NAMESPACE" get sts -l postgres-operator.crunchydata.com/cluster=cert-manager-tls,postgres-operator.crunchydata.com/instance-set=instance1 -o jsonpath='{.items[*].metadata.name}')
5359
for sts_name in $instance_sts; do
5460
cert_name="${sts_name}-cert"

e2e-tests/tests/cert-manager-tls/11-verify-tls-secrets.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ commands:
6262
exit 1
6363
fi
6464
65+
retry 12 5 verify_secret_data cert-manager-tls-replication-cert tls.crt tls.key ca.crt
66+
67+
repl_secret_type=$(kubectl -n "$NAMESPACE" get secret cert-manager-tls-replication-cert -o jsonpath='{.type}')
68+
if [[ "$repl_secret_type" != "kubernetes.io/tls" ]]; then
69+
echo "Replication TLS secret type is incorrect: $repl_secret_type (expected kubernetes.io/tls)"
70+
exit 1
71+
fi
72+
73+
repl_issuer_name=$(kubectl -n "$NAMESPACE" get secret cert-manager-tls-replication-cert -o jsonpath='{.metadata.annotations.cert-manager\.io/issuer-name}')
74+
if [[ "$repl_issuer_name" != "cert-manager-tls-tls-issuer" ]]; then
75+
echo "Replication TLS secret issuer annotation is incorrect: $repl_issuer_name"
76+
exit 1
77+
fi
78+
6579
retry 12 5 verify_secret_data cert-manager-tls-pgbackrest-client-tls tls.crt tls.key
6680
6781
pgbr_client_issuer=$(kubectl -n "$NAMESPACE" get secret cert-manager-tls-pgbackrest-client-tls -o jsonpath='{.metadata.annotations.cert-manager\.io/issuer-name}')

internal/controller/postgrescluster/patroni.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,23 @@ func (r *Reconciler) reconcileReplicationSecret(
379379
return custom, err
380380
}
381381

382+
certManagerManaged, err := r.isRootCACertManagerManaged(ctx, cluster)
383+
if err != nil {
384+
return nil, errors.Wrap(err, "failed to check if cert-manager manages root CA")
385+
}
386+
387+
if certManagerManaged {
388+
return r.reconcileCertManagerReplicationSecret(ctx, cluster)
389+
}
390+
391+
return r.reconcileInternalReplicationSecret(ctx, cluster, root)
392+
}
393+
394+
// reconcileInternalReplicationSecret creates a replication certificate using internal PKI.
395+
func (r *Reconciler) reconcileInternalReplicationSecret(
396+
ctx context.Context, cluster *v1beta1.PostgresCluster,
397+
root *pki.RootCertificateAuthority,
398+
) (*corev1.Secret, error) {
382399
existing := &corev1.Secret{ObjectMeta: naming.ReplicationClientCertSecret(cluster)}
383400
err := errors.WithStack(client.IgnoreNotFound(
384401
r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing)))
@@ -436,6 +453,20 @@ func (r *Reconciler) reconcileReplicationSecret(
436453
return intent, err
437454
}
438455

456+
// reconcileCertManagerReplicationSecret creates the replication certificate using cert-manager.
457+
func (r *Reconciler) reconcileCertManagerReplicationSecret(
458+
ctx context.Context, cluster *v1beta1.PostgresCluster,
459+
) (*corev1.Secret, error) {
460+
c := r.CertManagerCtrlFunc(r.Client, r.Scheme, false)
461+
462+
if err := c.ApplyReplicationCertificate(ctx, cluster); err != nil {
463+
return nil, errors.Wrap(err, "failed to apply replication certificate")
464+
}
465+
466+
secret := &corev1.Secret{ObjectMeta: naming.ReplicationClientCertSecret(cluster)}
467+
return secret, nil
468+
}
469+
439470
// replicationCertSecretProjection returns a secret projection of the postgrescluster's
440471
// client certificate and key to include in the instance configuration volume.
441472
func replicationCertSecretProjection(certificate *corev1.Secret) *corev1.SecretProjection {

internal/controller/postgrescluster/pki_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ func (m *mockCertManagerController) ApplyInstanceCertificate(context.Context, *v
412412
func (m *mockCertManagerController) ApplyPGBouncerCertificate(context.Context, *v1beta1.PostgresCluster, []string) error {
413413
panic("unexpected call")
414414
}
415+
func (m *mockCertManagerController) ApplyReplicationCertificate(context.Context, *v1beta1.PostgresCluster) error {
416+
panic("unexpected call")
417+
}
415418
func (m *mockCertManagerController) ApplyPGBackRestClientCertificate(context.Context, *v1beta1.PostgresCluster) error {
416419
panic("unexpected call")
417420
}

percona/certmanager/certmanager.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Controller interface {
3232
ApplyClusterCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster, dnsNames []string) error
3333
ApplyInstanceCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster, instanceName string, dnsNames []string) error
3434
ApplyPGBouncerCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster, dnsNames []string) error
35+
ApplyReplicationCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster) error
3536
ApplyPGBackRestClientCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster) error
3637
ApplyPGBackRestRepoCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster, dnsNames []string) error
3738
}
@@ -590,6 +591,104 @@ func (c *controller) ApplyPGBouncerCertificate(ctx context.Context, cluster *v1b
590591
return nil
591592
}
592593

594+
// ApplyReplicationCertificate creates a cert-manager Certificate resource for the replication client.
595+
func (c *controller) ApplyReplicationCertificate(ctx context.Context, cluster *v1beta1.PostgresCluster) error {
596+
secretMeta := naming.ReplicationClientCertSecret(cluster)
597+
certName := cluster.Name + "-replication-cert"
598+
commonName := "_crunchyrepl"
599+
600+
certDuration := DefaultCertDuration
601+
if cluster.Spec.TLS != nil && cluster.Spec.TLS.CertValidityDuration != nil {
602+
certDuration = cluster.Spec.TLS.CertValidityDuration.Duration
603+
}
604+
605+
existing := &v1.Certificate{}
606+
err := c.cl.Get(ctx, types.NamespacedName{Name: certName, Namespace: cluster.Namespace}, existing)
607+
if err == nil {
608+
needsUpdate := false
609+
610+
hasOwnerRef, err := controllerutil.HasOwnerReference(existing.OwnerReferences, cluster, c.scheme)
611+
if err != nil {
612+
return errors.Wrap(err, "check owner reference")
613+
}
614+
615+
if !hasOwnerRef {
616+
gvk := v1beta1.SchemeBuilder.GroupVersion.WithKind("PostgresCluster")
617+
existing.OwnerReferences = []metav1.OwnerReference{{
618+
APIVersion: gvk.GroupVersion().String(),
619+
Kind: gvk.Kind,
620+
Name: cluster.GetName(),
621+
UID: cluster.GetUID(),
622+
BlockOwnerDeletion: ptr.To(true),
623+
Controller: ptr.To(true),
624+
}}
625+
needsUpdate = true
626+
}
627+
628+
if existing.Spec.Duration != nil && existing.Spec.Duration.Duration != certDuration {
629+
existing.Spec.Duration = &metav1.Duration{Duration: certDuration}
630+
needsUpdate = true
631+
}
632+
633+
if !needsUpdate {
634+
return nil
635+
}
636+
637+
return errors.Wrap(c.cl.Update(ctx, existing), "failed to update replication certificate")
638+
}
639+
if !k8serrors.IsNotFound(err) {
640+
return errors.Wrap(err, "failed to get replication certificate")
641+
}
642+
643+
cert := &v1.Certificate{
644+
ObjectMeta: metav1.ObjectMeta{
645+
Name: certName,
646+
Namespace: cluster.Namespace,
647+
Labels: naming.WithPerconaLabels(map[string]string{
648+
naming.LabelCluster: cluster.Name,
649+
naming.LabelClusterCertificate: "replication-client-tls",
650+
}, cluster.Name, "", cluster.Labels[naming.LabelVersion]),
651+
},
652+
Spec: v1.CertificateSpec{
653+
SecretName: secretMeta.Name,
654+
CommonName: commonName,
655+
DNSNames: []string{commonName},
656+
IssuerRef: cmmeta.IssuerReference{
657+
Name: naming.TLSIssuer(cluster).Name,
658+
Kind: v1.IssuerKind,
659+
},
660+
Duration: &metav1.Duration{Duration: certDuration},
661+
RenewBefore: &metav1.Duration{Duration: DefaultRenewBefore},
662+
PrivateKey: &v1.CertificatePrivateKey{
663+
Algorithm: v1.ECDSAKeyAlgorithm,
664+
Size: 256,
665+
RotationPolicy: v1.RotationPolicyNever,
666+
},
667+
Usages: []v1.KeyUsage{
668+
v1.UsageClientAuth,
669+
v1.UsageDigitalSignature,
670+
v1.UsageKeyEncipherment,
671+
},
672+
SecretTemplate: &v1.CertificateSecretTemplate{
673+
Labels: naming.WithPerconaLabels(map[string]string{
674+
naming.LabelCluster: cluster.Name,
675+
naming.LabelClusterCertificate: "replication-client-tls",
676+
}, cluster.Name, "", cluster.Labels[naming.LabelVersion]),
677+
},
678+
},
679+
}
680+
681+
if err := controllerutil.SetControllerReference(cluster, cert, c.scheme); err != nil {
682+
return errors.Wrap(err, "failed to set controller reference")
683+
}
684+
685+
if err := c.cl.Create(ctx, cert); err != nil {
686+
return errors.Wrap(err, "failed to create replication certificate")
687+
}
688+
689+
return nil
690+
}
691+
593692
// ApplyPGBackRestClientCertificate creates a cert-manager Certificate resource
594693
// for the pgBackRest client used by all PostgreSQL instances to connect to the
595694
// repository host.

percona/certmanager/certmanager_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,84 @@ func TestApplyPGBackRestClientCertificate(t *testing.T) {
685685
})
686686
}
687687

688+
func TestApplyReplicationCertificate(t *testing.T) {
689+
t.Run("create replication certificate successfully", func(t *testing.T) {
690+
cluster := testCluster()
691+
cluster.Name = "replication-cert-test"
692+
client := setupFakeClient(t, cluster)
693+
ctrl := NewController(client, client.Scheme(), false)
694+
695+
err := ctrl.ApplyReplicationCertificate(t.Context(), cluster)
696+
require.NoError(t, err)
697+
698+
cert := &v1.Certificate{}
699+
certName := cluster.Name + "-replication-cert"
700+
err = client.Get(t.Context(), sigs.ObjectKey{
701+
Namespace: cluster.Namespace,
702+
Name: certName,
703+
}, cert)
704+
require.NoError(t, err)
705+
706+
assert.Equal(t, certName, cert.Name)
707+
assert.Equal(t, cluster.Namespace, cert.Namespace)
708+
assert.Equal(t, naming.ReplicationClientCertSecret(cluster).Name, cert.Spec.SecretName)
709+
assert.Equal(t, "_crunchyrepl", cert.Spec.CommonName)
710+
assert.Equal(t, []string{"_crunchyrepl"}, cert.Spec.DNSNames)
711+
assert.Equal(t, naming.TLSIssuer(cluster).Name, cert.Spec.IssuerRef.Name)
712+
assert.Equal(t, v1.IssuerKind, cert.Spec.IssuerRef.Kind)
713+
assert.NotNil(t, cert.Spec.Duration)
714+
assert.Equal(t, DefaultCertDuration, cert.Spec.Duration.Duration)
715+
assert.NotNil(t, cert.Spec.RenewBefore)
716+
assert.Equal(t, DefaultRenewBefore, cert.Spec.RenewBefore.Duration)
717+
assert.NotNil(t, cert.Spec.PrivateKey)
718+
assert.Equal(t, v1.ECDSAKeyAlgorithm, cert.Spec.PrivateKey.Algorithm)
719+
assert.Equal(t, 256, cert.Spec.PrivateKey.Size)
720+
assert.Equal(t, v1.RotationPolicyNever, cert.Spec.PrivateKey.RotationPolicy)
721+
722+
assert.Contains(t, cert.Spec.Usages, v1.UsageClientAuth)
723+
assert.Contains(t, cert.Spec.Usages, v1.UsageDigitalSignature)
724+
assert.Contains(t, cert.Spec.Usages, v1.UsageKeyEncipherment)
725+
assert.NotContains(t, cert.Spec.Usages, v1.UsageServerAuth)
726+
727+
assert.NotNil(t, cert.Spec.SecretTemplate)
728+
assert.Equal(t, cluster.Name, cert.Spec.SecretTemplate.Labels[naming.LabelCluster])
729+
assert.Equal(t, "replication-client-tls", cert.Spec.SecretTemplate.Labels[naming.LabelClusterCertificate])
730+
731+
assert.Equal(t, cluster.Name, cert.Labels[naming.LabelCluster])
732+
assert.Equal(t, "replication-client-tls", cert.Labels[naming.LabelClusterCertificate])
733+
assert.NotEmpty(t, cert.Labels[naming.LabelPerconaManagedBy])
734+
735+
require.Len(t, cert.OwnerReferences, 1)
736+
assert.Equal(t, cluster.Name, cert.OwnerReferences[0].Name)
737+
738+
// return nil when certificate already exists
739+
err = ctrl.ApplyReplicationCertificate(t.Context(), cluster)
740+
require.NoError(t, err)
741+
})
742+
743+
t.Run("uses CertValidityDuration when set", func(t *testing.T) {
744+
customDuration := 4320 * time.Hour // 180 days
745+
cluster := testCluster()
746+
cluster.Name = "replication-cert-dur"
747+
cluster.Spec.TLS = &v1beta1.TLSSpec{
748+
CertValidityDuration: &metav1.Duration{Duration: customDuration},
749+
}
750+
client := setupFakeClient(t, cluster)
751+
ctrl := NewController(client, client.Scheme(), false)
752+
753+
err := ctrl.ApplyReplicationCertificate(t.Context(), cluster)
754+
require.NoError(t, err)
755+
756+
cert := &v1.Certificate{}
757+
err = client.Get(t.Context(), sigs.ObjectKey{
758+
Namespace: cluster.Namespace,
759+
Name: cluster.Name + "-replication-cert",
760+
}, cert)
761+
require.NoError(t, err)
762+
assert.Equal(t, customDuration, cert.Spec.Duration.Duration)
763+
})
764+
}
765+
688766
func TestUpdateCertificateDuration(t *testing.T) {
689767
initialDuration := 2160 * time.Hour // 90 days
690768
updatedDuration := 4320 * time.Hour // 180 days
@@ -798,6 +876,31 @@ func TestUpdateCertificateDuration(t *testing.T) {
798876
assert.Equal(t, updatedDuration, cert.Spec.Duration.Duration)
799877
})
800878

879+
t.Run("replication certificate duration updated when spec changes", func(t *testing.T) {
880+
cluster := testCluster()
881+
cluster.Name = "update-repl-dur"
882+
cluster.Spec.TLS = &v1beta1.TLSSpec{
883+
CertValidityDuration: &metav1.Duration{Duration: initialDuration},
884+
}
885+
client := setupFakeClient(t, cluster)
886+
ctrl := NewController(client, client.Scheme(), false)
887+
888+
err := ctrl.ApplyReplicationCertificate(t.Context(), cluster)
889+
require.NoError(t, err)
890+
891+
cluster.Spec.TLS.CertValidityDuration = &metav1.Duration{Duration: updatedDuration}
892+
err = ctrl.ApplyReplicationCertificate(t.Context(), cluster)
893+
require.NoError(t, err)
894+
895+
cert := &v1.Certificate{}
896+
err = client.Get(t.Context(), sigs.ObjectKey{
897+
Namespace: cluster.Namespace,
898+
Name: cluster.Name + "-replication-cert",
899+
}, cert)
900+
require.NoError(t, err)
901+
assert.Equal(t, updatedDuration, cert.Spec.Duration.Duration)
902+
})
903+
801904
t.Run("pgbackrest client certificate duration updated when pgBackRestCertValidityDuration changes", func(t *testing.T) {
802905
cluster := testCluster()
803906
cluster.Name = "update-pgbr-client-dur"

0 commit comments

Comments
 (0)