Skip to content

Commit 459582c

Browse files
CLOUDP-66615: Add support for rotation of TLS certificates and keys (#122)
1 parent e6b9eb1 commit 459582c

File tree

11 files changed

+268
-65
lines changed

11 files changed

+268
-65
lines changed

.evergreen.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ task_groups:
151151
- e2e_test_replica_set_multiple
152152
- e2e_test_replica_set_tls
153153
- e2e_test_replica_set_tls_upgrade
154+
- e2e_test_replica_set_tls_rotate
154155
teardown_task:
155156
- func: upload_e2e_logs
156157

@@ -277,6 +278,12 @@ tasks:
277278
vars:
278279
test: replica_set_tls_upgrade
279280

281+
- name: e2e_test_replica_set_tls_rotate
282+
commands:
283+
- func: run_e2e_test
284+
vars:
285+
test: replica_set_tls_rotate
286+
280287
buildvariants:
281288
- name: go_unit_tests
282289
display_name: go_unit_tests

pkg/apis/mongodb/v1/mongodb_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (m MongoDB) TLSSecretNamespacedName() types.NamespacedName {
197197
// TLSOperatorSecretNamespacedName will get the namespaced name of the Secret created by the operator
198198
// containing the combined certificate and key.
199199
func (m MongoDB) TLSOperatorSecretNamespacedName() types.NamespacedName {
200-
return types.NamespacedName{Name: "mongodb-operator-server-certificate-key", Namespace: m.Namespace}
200+
return types.NamespacedName{Name: m.Name + "-server-certificate-key", Namespace: m.Namespace}
201201
}
202202

203203
func (m MongoDB) NamespacedName() types.NamespacedName {

pkg/controller/mongodb/mongodb_tls.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mongodb
22

33
import (
4+
"crypto/sha256"
45
"fmt"
56

67
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -23,8 +24,7 @@ const (
2324
tlsCAMountPath = "/var/lib/tls/ca/"
2425
tlsCACertName = "ca.crt"
2526
tlsOperatorSecretMountPath = "/var/lib/tls/server/" //nolint
26-
tlsOperatorSecretFileName = "server.pem"
27-
tlsSecretCertName = "tls.crt" //nolint
27+
tlsSecretCertName = "tls.crt" //nolint
2828
tlsSecretKeyName = "tls.key"
2929
)
3030

@@ -74,6 +74,9 @@ func (r *ReplicaSetReconciler) validateTLSConfig(mdb mdbv1.MongoDB) (bool, error
7474
return false, nil
7575
}
7676

77+
// Watch certificate-key secret to handle rotations
78+
r.secretWatcher.Watch(mdb.TLSSecretNamespacedName(), mdb.NamespacedName())
79+
7780
return true, nil
7881
}
7982

@@ -84,47 +87,72 @@ func getTLSConfigModification(getUpdateCreator secret.GetUpdateCreator, mdb mdbv
8487
return automationconfig.NOOP(), nil
8588
}
8689

87-
if err := ensureTLSSecret(getUpdateCreator, mdb); err != nil {
90+
cert, key, err := getCertAndKey(getUpdateCreator, mdb)
91+
if err != nil {
92+
return automationconfig.NOOP(), err
93+
}
94+
95+
err = ensureTLSSecret(getUpdateCreator, mdb, cert, key)
96+
if err != nil {
8897
return automationconfig.NOOP(), err
8998
}
9099

91100
// The config is only updated after the certs and keys have been rolled out to all pods.
92101
// The agent needs these to be in place before the config is updated.
93102
// Once the config is updated, the agents will gradually enable TLS in accordance with: https://docs.mongodb.com/manual/tutorial/upgrade-cluster-to-ssl/
94103
if hasRolledOutTLS(mdb) {
95-
return tlsConfigModification(mdb), nil
104+
return tlsConfigModification(mdb, cert, key), nil
96105
}
97106

98107
return automationconfig.NOOP(), nil
99108
}
100109

101-
// ensureTLSSecret will create or update the operator-managed Secret containing
102-
// the concatenated certificate and key from the user-provided Secret.
103-
func ensureTLSSecret(getUpdateCreator secret.GetUpdateCreator, mdb mdbv1.MongoDB) error {
104-
cert, err := secret.ReadKey(getUpdateCreator, tlsSecretCertName, mdb.TLSSecretNamespacedName())
110+
// getCertAndKey will fetch the certificate and key from the user-provided Secret.
111+
func getCertAndKey(getter secret.Getter, mdb mdbv1.MongoDB) (string, string, error) {
112+
cert, err := secret.ReadKey(getter, tlsSecretCertName, mdb.TLSSecretNamespacedName())
105113
if err != nil {
106-
return err
114+
return "", "", err
107115
}
108116

109-
key, err := secret.ReadKey(getUpdateCreator, tlsSecretKeyName, mdb.TLSSecretNamespacedName())
117+
key, err := secret.ReadKey(getter, tlsSecretKeyName, mdb.TLSSecretNamespacedName())
110118
if err != nil {
111-
return err
119+
return "", "", err
112120
}
113121

122+
return cert, key, nil
123+
}
124+
125+
// ensureTLSSecret will create or update the operator-managed Secret containing
126+
// the concatenated certificate and key from the user-provided Secret.
127+
func ensureTLSSecret(getUpdateCreator secret.GetUpdateCreator, mdb mdbv1.MongoDB, cert, key string) error {
128+
// Calculate file name from certificate and key
129+
fileName := tlsOperatorSecretFileName(cert, key)
130+
114131
operatorSecret := secret.Builder().
115132
SetName(mdb.TLSOperatorSecretNamespacedName().Name).
116133
SetNamespace(mdb.TLSOperatorSecretNamespacedName().Namespace).
117-
SetField(tlsOperatorSecretFileName, cert+key).
134+
SetField(fileName, cert+key).
118135
SetOwnerReferences([]metav1.OwnerReference{getOwnerReference(mdb)}).
119136
Build()
120137

121138
return secret.CreateOrUpdate(getUpdateCreator, operatorSecret)
122139
}
123140

141+
// tlsOperatorSecretFileName calculates the file name to use for the mounted
142+
// certificate-key file. The name is based on the hash of the combined cert and key.
143+
// If the certificate or key changes, the file path changes as well which will trigger
144+
// the agent to perform a restart.
145+
// The user-provided secret is being watched and will trigger a reconciliation
146+
// on changes. This enables the operator to automatically handle cert rotations.
147+
func tlsOperatorSecretFileName(cert, key string) string {
148+
hash := sha256.Sum256([]byte(cert + key))
149+
return fmt.Sprintf("%x.pem", hash)
150+
}
151+
124152
// tlsConfigModification will enable TLS in the automation config.
125-
func tlsConfigModification(mdb mdbv1.MongoDB) automationconfig.Modification {
153+
func tlsConfigModification(mdb mdbv1.MongoDB, cert, key string) automationconfig.Modification {
126154
caCertificatePath := tlsCAMountPath + tlsCACertName
127-
certificateKeyPath := tlsOperatorSecretMountPath + tlsOperatorSecretFileName
155+
certificateKeyPath := tlsOperatorSecretMountPath + tlsOperatorSecretFileName(cert, key)
128156

129157
mode := automationconfig.TLSModeRequired
130158
if mdb.Spec.Security.TLS.Optional {

pkg/controller/mongodb/mongodb_tls_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,11 @@ func TestAutomationConfig_IsCorrectlyConfiguredWithTLS(t *testing.T) {
139139
}, ac.TLS)
140140

141141
for _, process := range ac.Processes {
142+
operatorSecretFileName := tlsOperatorSecretFileName("CERT", "KEY")
143+
142144
assert.Equal(t, automationconfig.MongoDBTLS{
143145
Mode: automationconfig.TLSModeRequired,
144-
PEMKeyFile: tlsOperatorSecretMountPath + tlsOperatorSecretFileName,
146+
PEMKeyFile: tlsOperatorSecretMountPath + operatorSecretFileName,
145147
CAFile: tlsCAMountPath + tlsCACertName,
146148
AllowConnectionsWithoutCertificate: true,
147149
}, process.Args26.Net.TLS)
@@ -160,9 +162,11 @@ func TestAutomationConfig_IsCorrectlyConfiguredWithTLS(t *testing.T) {
160162
}, ac.TLS)
161163

162164
for _, process := range ac.Processes {
165+
operatorSecretFileName := tlsOperatorSecretFileName("CERT", "KEY")
166+
163167
assert.Equal(t, automationconfig.MongoDBTLS{
164168
Mode: automationconfig.TLSModePreferred,
165-
PEMKeyFile: tlsOperatorSecretMountPath + tlsOperatorSecretFileName,
169+
PEMKeyFile: tlsOperatorSecretMountPath + operatorSecretFileName,
166170
CAFile: tlsCAMountPath + tlsCACertName,
167171
AllowConnectionsWithoutCertificate: true,
168172
}, process.Args26.Net.TLS)
@@ -182,7 +186,7 @@ func TestTLSOperatorSecret(t *testing.T) {
182186

183187
// Operator-managed secret should have been created and contain the
184188
// concatenated certificate and key.
185-
certificateKey, err := secret.ReadKey(client, tlsOperatorSecretFileName, mdb.TLSOperatorSecretNamespacedName())
189+
certificateKey, err := secret.ReadKey(client, tlsOperatorSecretFileName("CERT", "KEY"), mdb.TLSOperatorSecretNamespacedName())
186190
assert.NoError(t, err)
187191
assert.Equal(t, "CERTKEY", certificateKey)
188192
})
@@ -197,16 +201,17 @@ func TestTLSOperatorSecret(t *testing.T) {
197201
s := secret.Builder().
198202
SetName(mdb.TLSOperatorSecretNamespacedName().Name).
199203
SetNamespace(mdb.TLSOperatorSecretNamespacedName().Namespace).
200-
SetField(tlsOperatorSecretFileName, "").
204+
SetField(tlsOperatorSecretFileName("", ""), "").
201205
Build()
202206
err = client.CreateSecret(s)
203207
assert.NoError(t, err)
204208

205209
_, err = getTLSConfigModification(client, mdb)
206210
assert.NoError(t, err)
207211

208-
// Operator-managed secret should have been updated the with concatenated certificate and key.
209-
certificateKey, err := secret.ReadKey(client, tlsOperatorSecretFileName, mdb.TLSOperatorSecretNamespacedName())
212+
// Operator-managed secret should have been updated with the concatenated
213+
// certificate and key.
214+
certificateKey, err := secret.ReadKey(client, tlsOperatorSecretFileName("CERT", "KEY"), mdb.TLSOperatorSecretNamespacedName())
210215
assert.NoError(t, err)
211216
assert.Equal(t, "CERTKEY", certificateKey)
212217
})

pkg/controller/mongodb/replica_set_controller.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os"
1010
"time"
1111

12+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/controller/watch"
13+
1214
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/persistentvolumeclaim"
1315

1416
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/probes"
@@ -77,19 +79,22 @@ func Add(mgr manager.Manager) error {
7779
// contains the list of all available MongoDB versions
7880
type ManifestProvider func() (automationconfig.VersionManifest, error)
7981

80-
// newReconciler returns a new reconcile.Reconciler
81-
func newReconciler(mgr manager.Manager, manifestProvider ManifestProvider) reconcile.Reconciler {
82+
func newReconciler(mgr manager.Manager, manifestProvider ManifestProvider) *ReplicaSetReconciler {
8283
mgrClient := mgr.GetClient()
84+
secretWatcher := watch.New()
85+
8386
return &ReplicaSetReconciler{
8487
client: kubernetesClient.NewClient(mgrClient),
8588
scheme: mgr.GetScheme(),
8689
manifestProvider: manifestProvider,
8790
log: zap.S(),
91+
secretWatcher: &secretWatcher,
8892
}
8993
}
9094

91-
// add adds a new Controller to mgr with r as the reconcile.Reconciler
92-
func add(mgr manager.Manager, r reconcile.Reconciler) error {
95+
// add sets up a controller for the Reconciler on the manager. It will
96+
// also configure the necessary watches.
97+
func add(mgr manager.Manager, r *ReplicaSetReconciler) error {
9398
// Create a new controller
9499
c, err := controller.New("replicaset-controller", mgr, controller.Options{Reconciler: r})
95100
if err != nil {
@@ -101,6 +106,12 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
101106
if err != nil {
102107
return err
103108
}
109+
110+
err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, r.secretWatcher)
111+
if err != nil {
112+
return err
113+
}
114+
104115
return nil
105116
}
106117

@@ -115,6 +126,7 @@ type ReplicaSetReconciler struct {
115126
scheme *runtime.Scheme
116127
manifestProvider func() (automationconfig.VersionManifest, error)
117128
log *zap.SugaredLogger
129+
secretWatcher *watch.ResourceWatcher
118130
}
119131

120132
// Reconcile reads that state of the cluster for a MongoDB object and makes changes based on the state read
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package replica_set_tls
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/mongodb/mongodb-kubernetes-operator/test/e2e/tlstests"
8+
9+
e2eutil "github.com/mongodb/mongodb-kubernetes-operator/test/e2e"
10+
"github.com/mongodb/mongodb-kubernetes-operator/test/e2e/mongodbtests"
11+
setup "github.com/mongodb/mongodb-kubernetes-operator/test/e2e/setup"
12+
f "github.com/operator-framework/operator-sdk/pkg/test"
13+
)
14+
15+
func TestMain(m *testing.M) {
16+
f.MainEntry(m)
17+
}
18+
19+
func TestReplicaSetTLSRotate(t *testing.T) {
20+
ctx, shouldCleanup := setup.InitTest(t)
21+
if shouldCleanup {
22+
defer ctx.Cleanup()
23+
}
24+
25+
mdb, user := e2eutil.NewTestMongoDB("mdb-tls")
26+
mdb.Spec.Security.TLS = e2eutil.NewTestTLSConfig(false)
27+
28+
_, err := setup.GeneratePasswordForUser(user, ctx)
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
33+
if err := setup.CreateTLSResources(mdb.Namespace, ctx); err != nil {
34+
t.Fatalf("Failed to set up TLS resources: %+v", err)
35+
}
36+
37+
t.Run("Create MongoDB Resource", mongodbtests.CreateMongoDBResource(&mdb, ctx))
38+
t.Run("Basic tests", mongodbtests.BasicFunctionality(&mdb))
39+
t.Run("Wait for TLS to be enabled", tlstests.WaitForTLSMode(&mdb, "requireSSL"))
40+
t.Run("Test Basic TLS Connectivity", tlstests.ConnectivityWithTLS(&mdb))
41+
t.Run("Test TLS required", tlstests.ConnectivityWithoutTLSShouldFail(&mdb))
42+
43+
t.Run("MongoDB is reachable while certificate is rotated", tlstests.IsReachableOverTLSDuring(&mdb, time.Second*10,
44+
func() {
45+
t.Run("Update certificate secret", tlstests.RotateCertificate(&mdb))
46+
t.Run("Wait for certificate to be rotated", tlstests.WaitForRotatedCertificate(&mdb))
47+
},
48+
))
49+
}

test/e2e/tlstests/tlstests.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ import (
66
"crypto/x509"
77
"fmt"
88
"io/ioutil"
9+
"math/big"
910
"testing"
1011
"time"
1112

13+
f "github.com/operator-framework/operator-sdk/pkg/test"
14+
15+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/secret"
16+
1217
v1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
1318
e2eutil "github.com/mongodb/mongodb-kubernetes-operator/test/e2e"
1419
"github.com/mongodb/mongodb-kubernetes-operator/test/e2e/mongodbtests"
@@ -136,6 +141,59 @@ func getAdminSetting(uri, key string) (interface{}, error) {
136141
return value, nil
137142
}
138143

144+
func RotateCertificate(mdb *v1.MongoDB) func(*testing.T) {
145+
return func(t *testing.T) {
146+
// Load new certificate and key
147+
cert, err := ioutil.ReadFile("testdata/tls/server_rotated.crt")
148+
assert.NoError(t, err)
149+
key, err := ioutil.ReadFile("testdata/tls/server_rotated.key")
150+
assert.NoError(t, err)
151+
152+
certKeySecret := secret.Builder().
153+
SetName(mdb.Spec.Security.TLS.CertificateKeySecret.Name).
154+
SetNamespace(mdb.Namespace).
155+
SetField("tls.crt", string(cert)).
156+
SetField("tls.key", string(key)).
157+
Build()
158+
159+
err = f.Global.Client.Update(context.TODO(), &certKeySecret)
160+
assert.NoError(t, err)
161+
}
162+
}
163+
164+
func WaitForRotatedCertificate(mdb *v1.MongoDB) func(*testing.T) {
165+
return func(t *testing.T) {
166+
// The rotated certificate has serial number 2
167+
expectedSerial := big.NewInt(2)
168+
169+
tlsConfig, err := getClientTLSConfig()
170+
assert.NoError(t, err)
171+
172+
// Reject all server certificates that don't have the expected serial number
173+
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
174+
cert := verifiedChains[0][0]
175+
if expectedSerial.Cmp(cert.SerialNumber) != 0 {
176+
return fmt.Errorf("expected certificate serial number %s, got %s", expectedSerial, cert.SerialNumber)
177+
}
178+
179+
return nil
180+
}
181+
182+
opts := options.Client().SetTLSConfig(tlsConfig).ApplyURI(mdb.MongoURI())
183+
mongoClient, err := mongo.Connect(context.TODO(), opts)
184+
assert.NoError(t, err)
185+
186+
// Ping the cluster until it succeeds. The ping will only suceed with the right certificate.
187+
err = wait.Poll(5*time.Second, 5*time.Minute, func() (done bool, err error) {
188+
if err := mongoClient.Ping(context.TODO(), nil); err != nil {
189+
return false, nil
190+
}
191+
return true, nil
192+
})
193+
assert.NoError(t, err)
194+
}
195+
}
196+
139197
func getClientTLSConfig() (*tls.Config, error) {
140198
// Read the CA certificate from test data
141199
caPEM, err := ioutil.ReadFile("testdata/tls/ca.crt")

0 commit comments

Comments
 (0)