Skip to content

Commit

Permalink
Recreate token if store configuration changes (#29)
Browse files Browse the repository at this point in the history
This re-encrypts the S3 store secrets if GPG keys change.
  • Loading branch information
bastjan authored Mar 28, 2024
1 parent c2890f5 commit 54b92b9
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 8 deletions.
16 changes: 15 additions & 1 deletion api/v1beta1/emergencyaccount_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type EmergencyAccountStatus struct {
LastTokenCreationTimestamp metav1.Time `json:"lastTokenCreationTimestamp,omitempty"`
// Tokens is a list of tokens that have been created
Tokens []TokenStatus `json:"tokens,omitempty"`
// LastTokenStoreConfigurationHashes is the hash of the last token store configuration.
// It is used to detect changes in the token store configuration.
// A change in the configuration triggers the creation of a new token.
LastTokenStoreHashes []TokenStoreHash `json:"lastTokenStoreConfigurationHashes,omitempty"`
}

// TokenStore defines the store the created tokens are stored in
Expand Down Expand Up @@ -124,7 +128,10 @@ type SecretStoreSpec struct{}

// LogStoreSpec configures the log store.
// The log store outputs the token to the log but does not store it anywhere.
type LogStoreSpec struct{}
type LogStoreSpec struct {
// AdditionalFields is a map of additional fields to log.
AdditionalFields map[string]string `json:"additionalFields,omitempty"`
}

// TokenStatus defines the observed state of the managed token
type TokenStatus struct {
Expand All @@ -147,6 +154,13 @@ type TokenStatusRef struct {
Store string `json:"store"`
}

type TokenStoreHash struct {
// Name is the name of the store.
Name string `json:"name"`
// Sha256 is the hash of the store configuration.
Sha256 string `json:"hash"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

Expand Down
29 changes: 28 additions & 1 deletion api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions config/crd/bases/cluster.appuio.io_emergencyaccounts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ spec:
logStore:
description: LogSpec configures the log store. The log store
outputs the token to the log but does not store it anywhere.
properties:
additionalFields:
additionalProperties:
type: string
description: AdditionalFields is a map of additional fields
to log.
type: object
type: object
name:
description: Name is the name of the store. Must be unique within
Expand Down Expand Up @@ -175,6 +182,24 @@ spec:
last token was created.
format: date-time
type: string
lastTokenStoreConfigurationHashes:
description: LastTokenStoreConfigurationHashes is the hash of the
last token store configuration. It is used to detect changes in
the token store configuration. A change in the configuration triggers
the creation of a new token.
items:
properties:
hash:
description: Sha256 is the hash of the store configuration.
type: string
name:
description: Name is the name of the store.
type: string
required:
- hash
- name
type: object
type: array
tokens:
description: Tokens is a list of tokens that have been created
items:
Expand Down
31 changes: 29 additions & 2 deletions controllers/emergencyaccount_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package controllers

import (
"context"
"crypto/sha256"
"encoding/gob"
"fmt"
"time"

Expand Down Expand Up @@ -85,6 +87,31 @@ func (r *EmergencyAccountReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, fmt.Errorf("unable to reconcile ServiceAccount: %w", err)
}

var configChanged bool
for _, store := range instance.Spec.TokenStores {
refI := slices.IndexFunc(instance.Status.LastTokenStoreHashes, func(ref emcv1beta1.TokenStoreHash) bool {
return store.Name == ref.Name
})
hw := sha256.New()
if err := gob.NewEncoder(hw).Encode(store); err != nil {
return ctrl.Result{}, fmt.Errorf("unable to hash store configuration: %w", err)
}
hsh := fmt.Sprintf("%x", hw.Sum(nil))
if refI >= 0 && instance.Status.LastTokenStoreHashes[refI].Sha256 == hsh {
continue
}
l.Info("store configuration changed", "store", store.Name, "hash", hsh)
configChanged = configChanged || true
if refI == -1 {
instance.Status.LastTokenStoreHashes = append(instance.Status.LastTokenStoreHashes, emcv1beta1.TokenStoreHash{
Name: store.Name,
Sha256: hsh,
})
} else {
instance.Status.LastTokenStoreHashes[refI].Sha256 = hsh
}
}

verified, failedVerification := r.verifyTokens(ctx, instance)
if len(failedVerification) > 0 {
us := make([]string, len(failedVerification))
Expand All @@ -108,11 +135,11 @@ func (r *EmergencyAccountReconciler) Reconcile(ctx context.Context, req ctrl.Req
nValidityLeft++
}
}
if nValidityLeft > 0 {
if nValidityLeft > 0 && !configChanged {
l.Info("enough tokens have validity left, not creating new one", "ntokens", nValidityLeft)
return ctrl.Result{RequeueAfter: instance.Spec.CheckInterval.Duration}, nil
}
l.Info("not enough tokens have validity left, creating new one")
l.Info("not enough tokens have validity left or store config changed, creating new one")

if instance.Status.LastTokenCreationTimestamp.Add(instance.Spec.MinRecreateInterval.Duration).After(r.Clock.Now()) {
l.Info("last token creation too recent, not creating a new one")
Expand Down
17 changes: 14 additions & 3 deletions controllers/emergencyaccount_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,24 @@ func Test_EmergencyAccountReconciler_Reconcile(t *testing.T) {
require.Len(t, ea.Status.Tokens, 1, "should not have created a new token")
require.WithinDuration(t, lastTimestamp, ea.Status.LastTokenCreationTimestamp.Time, 0, "last created timestamp should not have changed")

// Modify token store
ea.Spec.TokenStores[1].LogSpec = emcv1beta1.LogStoreSpec{
AdditionalFields: map[string]string{"test": "test"},
}
require.NoError(t, c.Update(ctx, ea))
_, err = subject.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ea)})
require.NoError(t, err)
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ea), ea))
t.Logf("status %+v", ea.Status)
require.Len(t, ea.Status.Tokens, 2, "should add a new token")

// Check token - too old and renew
clock.Advance(12 * time.Hour)
_, err = subject.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ea)})
require.NoError(t, err)
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ea), ea))
t.Logf("status %+v", ea.Status)
require.Len(t, ea.Status.Tokens, 2, "should add a new token")
require.Len(t, ea.Status.Tokens, 3, "should add a new token")

// Check token - verification fails
clock.Advance(time.Minute)
Expand All @@ -97,15 +108,15 @@ func Test_EmergencyAccountReconciler_Reconcile(t *testing.T) {
require.NoError(t, err)
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ea), ea))
t.Logf("status %+v", ea.Status)
require.Len(t, ea.Status.Tokens, 2, "still in MinRecreateInterval, should not have created a new token")
require.Len(t, ea.Status.Tokens, 3, "still in MinRecreateInterval, should not have created a new token")

// Check token - verification fails, but MinRecreateInterval is over
clock.Advance(5 * time.Minute)
_, err = subject.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ea)})
require.NoError(t, err)
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ea), ea))
t.Logf("status %+v", ea.Status)
require.Len(t, ea.Status.Tokens, 3, "should add a new token")
require.Len(t, ea.Status.Tokens, 4, "should add a new token")

// Finalizer should be removed and no metric left
require.NoError(t, c.Delete(ctx, ea))
Expand Down
7 changes: 6 additions & 1 deletion controllers/stores/log_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package stores

import (
"context"
"slices"

emcv1beta1 "github.com/appuio/emergency-credentials-controller/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -24,6 +25,10 @@ func NewLogStore(sts emcv1beta1.LogStoreSpec) *LogStore {
}

func (ss *LogStore) StoreToken(ctx context.Context, ea emcv1beta1.EmergencyAccount, token string) (string, error) {
log.FromContext(ctx).Info("new token created", "token", token)
fs := slices.Grow([]any{"token", token}, len(ss.LogStoreSpec.AdditionalFields)*2)
for k, v := range ss.LogStoreSpec.AdditionalFields {
fs = append(fs, k, v)
}
log.FromContext(ctx).Info("new token created", fs...)
return "", nil
}

0 comments on commit 54b92b9

Please sign in to comment.