Skip to content
Merged
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
44 changes: 0 additions & 44 deletions pkg/operator/config.go

This file was deleted.

63 changes: 52 additions & 11 deletions pkg/operator/rotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package operator
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"fmt"
"time"

Expand All @@ -16,16 +17,37 @@ import (
)

const (
signingCertificateLifetimeInDays = 365 // 1 year
// The minimum remaining duration of the service CA needs to exceeds the maximum
// supported upgrade interval (currently 12 months). A duration of 26 months
// (rotated at 13 months) ensures that an upgrade will occur after automated
// rotation and before the expiry of the pre-rotation CA. Since an upgrade restarts
// all services, those services will always be using valid material.
//
// Example timeline using a 26 month service CA duration:
//
// - T+0m - Cluster installed with new CA or existing CA is rotated (CA-1)
// - T+12m - Cluster is upgraded and all pods are restarted
// - T+13m - Automated rotation replaces CA-1 with CA-2 when CA-1 duration < 13m
// - T+24m - Cluster is upgraded and all pods are restarted
// - T+26m - CA-1 expires. No impact because of the restart at time of upgrade
//
signingCertificateLifetimeInDays = 790 // 26 months

// The minimum duration that a CA should be trusted is approximately half
// the default signing certificate lifetime. If a signing CA is valid for
// less than this duration, it is due for rotation. An intermediate
// certificate created by rotation (to ensure that the previous CA remains
// trusted) should be valid for at least this long.
minimumTrustDuration = 182 * 24 * time.Hour
minimumTrustDuration = 395 * 24 * time.Hour // 13 months
)

type unsupportedServiceCAConfig struct {
ForceRotation forceRotationConfig `json:"forceRotation"`
}
type forceRotationConfig struct {
Reason string `json:"reason"`
}

type signingCA struct {
config *crypto.TLSCertificateConfig
bundle []*x509.Certificate
Expand Down Expand Up @@ -64,19 +86,14 @@ func (ca *signingCA) updateSigningSecret(secret *corev1.Secret) error {
// current CA is not more than half-way expired or if a forced rotation was not
// requested, and in this case an empty rotation message will be returned.
func maybeRotateSigningSecret(secret *corev1.Secret, currentCACert *x509.Certificate, rawUnsupportedServiceCAConfig []byte) (string, error) {
serviceCAConfig, err := loadUnsupportedServiceCAConfig(rawUnsupportedServiceCAConfig)
reason, err := forceRotationReason(rawUnsupportedServiceCAConfig)
if err != nil {
return "", fmt.Errorf("failed to load unsupportedConfigOverrides: %v", err)
return "", fmt.Errorf("failed to read force rotation reason: %v", err)
}

reason := serviceCAConfig.ForceRotation.Reason
forcedRotation := forcedRotationRequired(secret, reason)

timeBasedRotation := false
if serviceCAConfig.TimeBasedRotation.Enabled {
minimumExpiry := time.Now().Add(minimumTrustDuration)
timeBasedRotation = currentCACert.NotAfter.Before(minimumExpiry)
}
minimumExpiry := time.Now().Add(minimumTrustDuration)
timeBasedRotation := currentCACert.NotAfter.Before(minimumExpiry)

if !(forcedRotation || timeBasedRotation) {
return "", nil
Expand Down Expand Up @@ -193,6 +210,30 @@ func createIntermediateCACert(targetCACert, signingCACert *x509.Certificate, sig
return caCert, nil
}

// forceRotationReason attempts to retrieve a force rotation reason
// from the provided raw UnsupportedConfigOverrides.
func forceRotationReason(rawUnsupportedServiceCAConfig []byte) (string, error) {
serviceCAConfig := &unsupportedServiceCAConfig{}
if raw := rawUnsupportedServiceCAConfig; len(raw) > 0 {
if err := json.Unmarshal(raw, serviceCAConfig); err != nil {
return "", err
}
}

return serviceCAConfig.ForceRotation.Reason, nil
}

// RawUnsupportedServiceCAConfig returns the raw value of the operator field
// UnsupportedConfigOverrides for the given force rotation reason.
func RawUnsupportedServiceCAConfig(reason string) ([]byte, error) {
config := &unsupportedServiceCAConfig{
ForceRotation: forceRotationConfig{
Reason: reason,
},
}
return json.Marshal(config)
}

// forcedRotationRequired indicates whether the force rotation reason is not empty and
// does not match the annotation stored on the signing secret.
func forcedRotationRequired(secret *corev1.Secret, reason string) bool {
Expand Down
10 changes: 3 additions & 7 deletions pkg/operator/rotate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,9 @@ func TestMaybeRotateSigningSecret(t *testing.T) {

for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
var rawUnsupportedServiceCAConfig []byte
if len(tc.reason) > 0 || tc.rotationExpected {
var err error
rawUnsupportedServiceCAConfig, err = RawUnsupportedServiceCAConfig(true, tc.reason)
if err != nil {
t.Fatalf("failed to create raw unsupported config overrides: %v", err)
}
rawUnsupportedServiceCAConfig, err := RawUnsupportedServiceCAConfig(tc.reason)
if err != nil {
t.Fatalf("failed to create raw unsupported config overrides: %v", err)
}
secret := tc.secret.DeepCopy()
rotationMessage, err := maybeRotateSigningSecret(secret, tc.caCert, rawUnsupportedServiceCAConfig)
Expand Down
19 changes: 5 additions & 14 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,6 @@ func triggerTimeBasedRotation(t *testing.T, client *kubernetes.Clientset, config
Key: currentCAKey,
}

// Enable time-based rotation by updating the operator config.
timeBasedRotationEnabled := true
forceRotationReason := ""
setUnsupportedServiceCAConfig(t, config, timeBasedRotationEnabled, forceRotationReason)

// Trigger rotation by renewing the current ca with an expiry that
// is sooner than the minimum required duration.
renewedExpiry := time.Now().Add(1 * time.Hour)
Expand Down Expand Up @@ -447,13 +442,7 @@ func triggerForcedRotation(t *testing.T, client *kubernetes.Clientset, config *r
caCertPEM := secret.Data[v1.TLSCertKey]
caKeyPEM := secret.Data[v1.TLSPrivateKeyKey]

// Trigger a forced rotation by updating the operator config with a reason.
setUnsupportedServiceCAConfig(t, config, false, "42")

pollForCARotation(t, client, caCertPEM, caKeyPEM)
}

func setUnsupportedServiceCAConfig(t *testing.T, config *rest.Config, timeBasedRotationEnabled bool, forceRotationReason string) {
// Trigger a forced rotation by updating the operator config
operatorClient, err := operatorv1client.NewForConfig(config)
if err != nil {
t.Fatalf("error creating operator client: %v", err)
Expand All @@ -462,7 +451,7 @@ func setUnsupportedServiceCAConfig(t *testing.T, config *rest.Config, timeBasedR
if err != nil {
t.Fatalf("error retrieving operator config: %v", err)
}
rawUnsupportedServiceCAConfig, err := operator.RawUnsupportedServiceCAConfig(timeBasedRotationEnabled, forceRotationReason)
rawUnsupportedServiceCAConfig, err := operator.RawUnsupportedServiceCAConfig("42")
if err != nil {
t.Fatalf("failed to create raw unsupported config overrides: %v", err)
}
Expand All @@ -471,6 +460,8 @@ func setUnsupportedServiceCAConfig(t *testing.T, config *rest.Config, timeBasedR
if err != nil {
t.Fatalf("error updating operator config: %v", err)
}

pollForCARotation(t, client, caCertPEM, caKeyPEM)
}

// pollForCARotation polls for the signing secret to be changed in
Expand Down Expand Up @@ -824,7 +815,7 @@ func TestE2E(t *testing.T) {
// validates that both refreshed and unrefreshed clients and
// servers can continue to communicate in a trusted fashion.
t.Run("time-based-ca-rotation", func(t *testing.T) {
checkCARotation(t, adminClient, adminConfig, triggerTimeBasedRotation)
checkCARotation(t, adminClient, nil, triggerTimeBasedRotation)
})

// This test triggers rotation by updating the operator
Expand Down