Skip to content

Commit 14f4c3b

Browse files
authored
Merge pull request #181 from arangodb/feature/renew-tls-ca-certificate
Added renewal of deployment TLS CA certificate
2 parents ae6136a + 134c795 commit 14f4c3b

15 files changed

+461
-104
lines changed

pkg/apis/deployment/v1alpha/plan.go

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const (
4747
ActionTypeWaitForMemberUp ActionType = "WaitForMemberUp"
4848
// ActionTypeRenewTLSCertificate causes the TLS certificate of a member to be renewed.
4949
ActionTypeRenewTLSCertificate ActionType = "RenewTLSCertificate"
50+
// ActionTypeRenewTLSCACertificate causes the TLS CA certificate of the entire deployment to be renewed.
51+
ActionTypeRenewTLSCACertificate ActionType = "RenewTLSCACertificate"
5052
)
5153

5254
// Action represents a single action to be taken to update a deployment.

pkg/deployment/access_package.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (d *Deployment) ensureAccessPackage(apSecretName string) error {
108108

109109
// Fetch client authentication CA
110110
clientAuthSecretName := spec.Sync.Authentication.GetClientCASecretName()
111-
clientAuthCert, clientAuthKey, err := k8sutil.GetCASecret(d.deps.KubeCli.CoreV1(), clientAuthSecretName, ns)
111+
clientAuthCert, clientAuthKey, _, err := k8sutil.GetCASecret(d.deps.KubeCli.CoreV1(), clientAuthSecretName, ns, nil)
112112
if err != nil {
113113
log.Debug().Err(err).Msg("Failed to get client-auth CA secret")
114114
return maskAny(err)

pkg/deployment/context_impl.go

+42
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,25 @@ func (d *Deployment) CleanupPod(p v1.Pod) error {
237237
return nil
238238
}
239239

240+
// RemovePodFinalizers removes all the finalizers from the Pod with given name in the namespace
241+
// of the deployment. If the pod does not exist, the error is ignored.
242+
func (d *Deployment) RemovePodFinalizers(podName string) error {
243+
log := d.deps.Log
244+
ns := d.GetNamespace()
245+
kubecli := d.deps.KubeCli
246+
p, err := kubecli.CoreV1().Pods(ns).Get(podName, metav1.GetOptions{})
247+
if err != nil {
248+
if k8sutil.IsNotFound(err) {
249+
return nil
250+
}
251+
return maskAny(err)
252+
}
253+
if err := k8sutil.RemovePodFinalizers(log, d.deps.KubeCli, p, p.GetFinalizers(), true); err != nil {
254+
return maskAny(err)
255+
}
256+
return nil
257+
}
258+
240259
// DeletePvc deletes a persistent volume claim with given name in the namespace
241260
// of the deployment. If the pvc does not exist, the error is ignored.
242261
func (d *Deployment) DeletePvc(pvcName string) error {
@@ -307,3 +326,26 @@ func (d *Deployment) DeleteTLSKeyfile(group api.ServerGroup, member api.MemberSt
307326
}
308327
return nil
309328
}
329+
330+
// GetTLSCA returns the TLS CA certificate in the secret with given name.
331+
// Returns: publicKey, privateKey, ownerByDeployment, error
332+
func (d *Deployment) GetTLSCA(secretName string) (string, string, bool, error) {
333+
ns := d.apiObject.GetNamespace()
334+
owner := d.apiObject.AsOwner()
335+
cert, priv, isOwned, err := k8sutil.GetCASecret(d.deps.KubeCli.CoreV1(), secretName, ns, &owner)
336+
if err != nil {
337+
return "", "", false, maskAny(err)
338+
}
339+
return cert, priv, isOwned, nil
340+
341+
}
342+
343+
// DeleteSecret removes the Secret with given name.
344+
// If the secret does not exist, the error is ignored.
345+
func (d *Deployment) DeleteSecret(secretName string) error {
346+
ns := d.apiObject.GetNamespace()
347+
if err := d.deps.KubeCli.CoreV1().Secrets(ns).Delete(secretName, &metav1.DeleteOptions{}); err != nil && !k8sutil.IsNotFound(err) {
348+
return maskAny(err)
349+
}
350+
return nil
351+
}

pkg/deployment/reconcile/action_context.go

+39
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ type ActionContext interface {
7070
// DeletePvc deletes a persistent volume claim with given name in the namespace
7171
// of the deployment. If the pvc does not exist, the error is ignored.
7272
DeletePvc(pvcName string) error
73+
// RemovePodFinalizers removes all the finalizers from the Pod with given name in the namespace
74+
// of the deployment. If the pod does not exist, the error is ignored.
75+
RemovePodFinalizers(podName string) error
7376
// DeleteTLSKeyfile removes the Secret containing the TLS keyfile for the given member.
7477
// If the secret does not exist, the error is ignored.
7578
DeleteTLSKeyfile(group api.ServerGroup, member api.MemberStatus) error
79+
// DeleteTLSCASecret removes the Secret containing the TLS CA certificate.
80+
DeleteTLSCASecret() error
7681
}
7782

7883
// newActionContext creates a new ActionContext implementation.
@@ -212,6 +217,15 @@ func (ac *actionContext) DeletePvc(pvcName string) error {
212217
return nil
213218
}
214219

220+
// RemovePodFinalizers removes all the finalizers from the Pod with given name in the namespace
221+
// of the deployment. If the pod does not exist, the error is ignored.
222+
func (ac *actionContext) RemovePodFinalizers(podName string) error {
223+
if err := ac.context.RemovePodFinalizers(podName); err != nil {
224+
return maskAny(err)
225+
}
226+
return nil
227+
}
228+
215229
// DeleteTLSKeyfile removes the Secret containing the TLS keyfile for the given member.
216230
// If the secret does not exist, the error is ignored.
217231
func (ac *actionContext) DeleteTLSKeyfile(group api.ServerGroup, member api.MemberStatus) error {
@@ -220,3 +234,28 @@ func (ac *actionContext) DeleteTLSKeyfile(group api.ServerGroup, member api.Memb
220234
}
221235
return nil
222236
}
237+
238+
// DeleteTLSCASecret removes the Secret containing the TLS CA certificate.
239+
func (ac *actionContext) DeleteTLSCASecret() error {
240+
spec := ac.context.GetSpec().TLS
241+
if !spec.IsSecure() {
242+
return nil
243+
}
244+
secretName := spec.GetCASecretName()
245+
if secretName == "" {
246+
return nil
247+
}
248+
// Remove secret hash, since it is going to change
249+
status, lastVersion := ac.context.GetStatus()
250+
if status.SecretHashes != nil {
251+
status.SecretHashes.TLSCA = ""
252+
if err := ac.context.UpdateStatus(status, lastVersion); err != nil {
253+
return maskAny(err)
254+
}
255+
}
256+
// Do delete the secret
257+
if err := ac.context.DeleteSecret(secretName); err != nil {
258+
return maskAny(err)
259+
}
260+
return nil
261+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Ewout Prangsma
21+
//
22+
23+
package reconcile
24+
25+
import (
26+
"context"
27+
"time"
28+
29+
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
30+
"github.com/rs/zerolog"
31+
)
32+
33+
// NewRenewTLSCACertificateAction creates a new Action that implements the given
34+
// planned RenewTLSCACertificate action.
35+
func NewRenewTLSCACertificateAction(log zerolog.Logger, action api.Action, actionCtx ActionContext) Action {
36+
return &renewTLSCACertificateAction{
37+
log: log,
38+
action: action,
39+
actionCtx: actionCtx,
40+
}
41+
}
42+
43+
// renewTLSCACertificateAction implements a RenewTLSCACertificate action.
44+
type renewTLSCACertificateAction struct {
45+
log zerolog.Logger
46+
action api.Action
47+
actionCtx ActionContext
48+
}
49+
50+
// Start performs the start of the action.
51+
// Returns true if the action is completely finished, false in case
52+
// the start time needs to be recorded and a ready condition needs to be checked.
53+
func (a *renewTLSCACertificateAction) Start(ctx context.Context) (bool, error) {
54+
// Just delete the secret.
55+
// It will be re-created.
56+
if err := a.actionCtx.DeleteTLSCASecret(); err != nil {
57+
return false, maskAny(err)
58+
}
59+
return true, nil
60+
}
61+
62+
// CheckProgress checks the progress of the action.
63+
// Returns true if the action is completely finished, false otherwise.
64+
func (a *renewTLSCACertificateAction) CheckProgress(ctx context.Context) (bool, bool, error) {
65+
return true, false, nil
66+
}
67+
68+
// Timeout returns the amount of time after which this action will timeout.
69+
func (a *renewTLSCACertificateAction) Timeout() time.Duration {
70+
return renewTLSCACertificateTimeout
71+
}

pkg/deployment/reconcile/action_rotate_member.go

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func (a *actionRotateMember) Start(ctx context.Context) (bool, error) {
5757
if !ok {
5858
log.Error().Msg("No such member")
5959
}
60+
// Remove finalizers, so Kubernetes will quickly terminate the pod
61+
if err := a.actionCtx.RemovePodFinalizers(m.PodName); err != nil {
62+
return false, maskAny(err)
63+
}
6064
if group.IsArangod() {
6165
// Invoke shutdown endpoint
6266
c, err := a.actionCtx.GetServerClient(ctx, group, a.action.MemberID)

pkg/deployment/reconcile/context.go

+9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ type Context interface {
6969
// DeletePvc deletes a persistent volume claim with given name in the namespace
7070
// of the deployment. If the pvc does not exist, the error is ignored.
7171
DeletePvc(pvcName string) error
72+
// RemovePodFinalizers removes all the finalizers from the Pod with given name in the namespace
73+
// of the deployment. If the pod does not exist, the error is ignored.
74+
RemovePodFinalizers(podName string) error
7275
// GetOwnedPods returns a list of all pods owned by the deployment.
7376
GetOwnedPods() ([]v1.Pod, error)
7477
// GetTLSKeyfile returns the keyfile encoded TLS certificate+key for
@@ -77,4 +80,10 @@ type Context interface {
7780
// DeleteTLSKeyfile removes the Secret containing the TLS keyfile for the given member.
7881
// If the secret does not exist, the error is ignored.
7982
DeleteTLSKeyfile(group api.ServerGroup, member api.MemberStatus) error
83+
// GetTLSCA returns the TLS CA certificate in the secret with given name.
84+
// Returns: publicKey, privateKey, ownerByDeployment, error
85+
GetTLSCA(secretName string) (string, string, bool, error)
86+
// DeleteSecret removes the Secret with given name.
87+
// If the secret does not exist, the error is ignored.
88+
DeleteSecret(secretName string) error
8089
}

pkg/deployment/reconcile/plan_builder.go

+10-78
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@
2323
package reconcile
2424

2525
import (
26-
"crypto/x509"
27-
"encoding/pem"
28-
"time"
29-
3026
"github.com/rs/zerolog"
3127
"github.com/rs/zerolog/log"
3228
"k8s.io/api/core/v1"
@@ -58,7 +54,7 @@ func (d *Reconciler) CreatePlan() error {
5854
apiObject := d.context.GetAPIObject()
5955
spec := d.context.GetSpec()
6056
status, lastVersion := d.context.GetStatus()
61-
newPlan, changed := createPlan(d.log, apiObject, status.Plan, spec, status, pods, d.context.GetTLSKeyfile)
57+
newPlan, changed := createPlan(d.log, apiObject, status.Plan, spec, status, pods, d.context.GetTLSKeyfile, d.context.GetTLSCA)
6258

6359
// If not change, we're done
6460
if !changed {
@@ -83,7 +79,8 @@ func (d *Reconciler) CreatePlan() error {
8379
func createPlan(log zerolog.Logger, apiObject metav1.Object,
8480
currentPlan api.Plan, spec api.DeploymentSpec,
8581
status api.DeploymentStatus, pods []v1.Pod,
86-
getTLSKeyfile func(group api.ServerGroup, member api.MemberStatus) (string, error)) (api.Plan, bool) {
82+
getTLSKeyfile func(group api.ServerGroup, member api.MemberStatus) (string, error),
83+
getTLSCA func(string) (string, string, bool, error)) (api.Plan, bool) {
8784
if len(currentPlan) > 0 {
8885
// Plan already exists, complete that first
8986
return currentPlan, false
@@ -178,41 +175,14 @@ func createPlan(log zerolog.Logger, apiObject metav1.Object,
178175
})
179176
}
180177

178+
// Check for the need to rotate TLS CA certificate and all members
179+
if len(plan) == 0 {
180+
plan = createRotateTLSCAPlan(log, spec, status, getTLSCA)
181+
}
182+
181183
// Check for the need to rotate TLS certificate of a members
182-
if len(plan) == 0 && spec.TLS.IsSecure() {
183-
status.Members.ForeachServerGroup(func(group api.ServerGroup, members api.MemberStatusList) error {
184-
for _, m := range members {
185-
if len(plan) > 0 {
186-
// Only 1 change at a time
187-
continue
188-
}
189-
if m.Phase != api.MemberPhaseCreated {
190-
// Only make changes when phase is created
191-
continue
192-
}
193-
if group == api.ServerGroupSyncWorkers {
194-
// SyncWorkers have no externally created TLS keyfile
195-
continue
196-
}
197-
// Load keyfile
198-
keyfile, err := getTLSKeyfile(group, m)
199-
if err != nil {
200-
log.Warn().Err(err).
201-
Str("role", group.AsRole()).
202-
Str("id", m.ID).
203-
Msg("Failed to get TLS secret")
204-
continue
205-
}
206-
renewalNeeded := tlsKeyfileNeedsRenewal(log, keyfile)
207-
if renewalNeeded {
208-
plan = append(append(plan,
209-
api.NewAction(api.ActionTypeRenewTLSCertificate, group, m.ID)),
210-
createRotateMemberPlan(log, m, group, "TLS certificate renewal")...,
211-
)
212-
}
213-
}
214-
return nil
215-
})
184+
if len(plan) == 0 {
185+
plan = createRotateTLSServerCertificatePlan(log, spec, status, getTLSKeyfile)
216186
}
217187

218188
// Return plan
@@ -304,44 +274,6 @@ func normalizeServiceAccountName(name string) string {
304274
return ""
305275
}
306276

307-
// tlsKeyfileNeedsRenewal decides if the certificate in the given keyfile
308-
// should be renewed.
309-
func tlsKeyfileNeedsRenewal(log zerolog.Logger, keyfile string) bool {
310-
raw := []byte(keyfile)
311-
for {
312-
var derBlock *pem.Block
313-
derBlock, raw = pem.Decode(raw)
314-
if derBlock == nil {
315-
break
316-
}
317-
if derBlock.Type == "CERTIFICATE" {
318-
cert, err := x509.ParseCertificate(derBlock.Bytes)
319-
if err != nil {
320-
// We do not understand the certificate, let's renew it
321-
log.Warn().Err(err).Msg("Failed to parse x509 certificate. Renewing it")
322-
return true
323-
}
324-
if cert.IsCA {
325-
// Only look at the server certificate, not CA or intermediate
326-
continue
327-
}
328-
// Check expiration date. Renewal at 2/3 of lifetime.
329-
ttl := cert.NotAfter.Sub(cert.NotBefore)
330-
expirationDate := cert.NotBefore.Add((ttl / 3) * 2)
331-
if expirationDate.Before(time.Now()) {
332-
// We should renew now
333-
log.Debug().
334-
Str("not-before", cert.NotBefore.String()).
335-
Str("not-after", cert.NotAfter.String()).
336-
Str("expiration-date", expirationDate.String()).
337-
Msg("TLS certificate renewal needed")
338-
return true
339-
}
340-
}
341-
}
342-
return false
343-
}
344-
345277
// createScalePlan creates a scaling plan for a single server group
346278
func createScalePlan(log zerolog.Logger, members api.MemberStatusList, group api.ServerGroup, count int) api.Plan {
347279
var plan api.Plan

0 commit comments

Comments
 (0)