diff --git a/go.mod b/go.mod index 4a48b7f630..e19e202c66 100644 --- a/go.mod +++ b/go.mod @@ -80,7 +80,7 @@ require ( github.com/zeebo/errs v1.3.0 golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/net v0.29.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 golang.org/x/sys v0.26.0 golang.org/x/time v0.7.0 diff --git a/go.sum b/go.sum index 843bf4796e..e540473080 100644 --- a/go.sum +++ b/go.sum @@ -1662,8 +1662,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/pkg/agent/manager/cache/jwt_cache.go b/pkg/agent/manager/cache/jwt_cache.go index 7058fd443b..b9f6a98393 100644 --- a/pkg/agent/manager/cache/jwt_cache.go +++ b/pkg/agent/manager/cache/jwt_cache.go @@ -1,28 +1,41 @@ package cache import ( + "context" "crypto/sha256" "encoding/base64" + "errors" + "fmt" "io" "sort" + "strings" "sync" + "github.com/go-jose/go-jose/v4/jwt" + "github.com/sirupsen/logrus" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/agent/client" + "github.com/spiffe/spire/pkg/common/jwtsvid" + "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" ) type JWTSVIDCache struct { - mu sync.Mutex - svids map[string]*client.JWTSVID + log logrus.FieldLogger + metrics telemetry.Metrics + mu sync.RWMutex + svids map[string]*client.JWTSVID } func (c *JWTSVIDCache) CountJWTSVIDs() int { return len(c.svids) } -func NewJWTSVIDCache() *JWTSVIDCache { +func NewJWTSVIDCache(log logrus.FieldLogger, metrics telemetry.Metrics) *JWTSVIDCache { return &JWTSVIDCache{ - svids: make(map[string]*client.JWTSVID), + metrics: metrics, + log: log, + svids: make(map[string]*client.JWTSVID), } } @@ -43,6 +56,60 @@ func (c *JWTSVIDCache) SetJWTSVID(spiffeID spiffeid.ID, audience []string, svid c.svids[key] = svid } +func (c *JWTSVIDCache) TaintJWTSVIDs(ctx context.Context, taintedJWTAuthorities map[string]struct{}) { + c.mu.Lock() + defer c.mu.Unlock() + + counter := telemetry.StartCall(c.metrics, telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedJWTSVIDs) + defer counter.Done(nil) + + var taintedKeyIDs []string + svidsRemoved := 0 + for key, jwtSVID := range c.svids { + keyID, err := getKeyIDFromSVIDToken(jwtSVID.Token) + if err != nil { + c.log.Error(err) + continue + } + if _, tainted := taintedJWTAuthorities[keyID]; tainted { + delete(c.svids, key) + taintedKeyIDs = append(taintedKeyIDs, keyID) + svidsRemoved++ + } + select { + case <-ctx.Done(): + c.log.WithError(ctx.Err()).Warn("Context cancelled, exiting process of tainting JWT-SVIDs in cache") + return + default: + } + } + taintedKeyIDsCount := len(taintedKeyIDs) + if taintedKeyIDsCount > 0 { + c.log.WithField(telemetry.JWTAuthorityKeyIDs, strings.Join(taintedKeyIDs, ",")). + WithField(telemetry.CountJWTSVIDs, svidsRemoved). + Info("JWT-SVIDs were removed from the JWT cache because they were issued by a tainted authority") + } + agent.AddCacheManagerTaintedJWTSVIDsSample(c.metrics, agent.CacheTypeWorkload, float32(taintedKeyIDsCount)) +} + +func getKeyIDFromSVIDToken(svidToken string) (string, error) { + token, err := jwt.ParseSigned(svidToken, jwtsvid.AllowedSignatureAlgorithms) + if err != nil { + return "", fmt.Errorf("failed to parse JWT-SVID: %w", err) + } + + if len(token.Headers) != 1 { + return "", fmt.Errorf("malformed JWT-SVID: expected a single token header; got %d", len(token.Headers)) + } + + keyID := token.Headers[0].KeyID + if keyID == "" { + return "", errors.New("missing key ID in token header of minted JWT-SVID") + } + + return keyID, nil +} + func jwtSVIDKey(spiffeID spiffeid.ID, audience []string) string { h := sha256.New() diff --git a/pkg/agent/manager/cache/jwt_cache_test.go b/pkg/agent/manager/cache/jwt_cache_test.go index d2e66006b6..cc3402c4ec 100644 --- a/pkg/agent/manager/cache/jwt_cache_test.go +++ b/pkg/agent/manager/cache/jwt_cache_test.go @@ -1,19 +1,32 @@ package cache import ( + "context" "testing" "time" + "github.com/hashicorp/go-metrics" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/agent/client" + "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" + "github.com/spiffe/spire/test/fakes/fakemetrics" + "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/assert" ) func TestJWTSVIDCacheBasic(t *testing.T) { now := time.Now() - expected := &client.JWTSVID{Token: "X", IssuedAt: now, ExpiresAt: now.Add(time.Second)} + tok := "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRaRGZZaXcxdUd6TXdkTVlITDdGRVl5SzhIT0tLd0xYIiwidHlwIjoiSldUIn0.eyJhdWQiOlsidGVzdC1hdWRpZW5jZSJdLCJleHAiOjE3MjQzNjU3MzEsImlhdCI6MTcyNDI3OTQwNywic3ViIjoic3BpZmZlOi8vZXhhbXBsZS5vcmcvYWdlbnQvZGJ1c2VyIn0.dFr-oWhm5tK0bBuVXt-sGESM5l7hhoY-Gtt5DkuFoJL5Y9d4ZfmicCvUCjL4CqDB3BO_cPqmFfrO7H7pxQbGLg" + keyID := "dZDfYiw1uGzMwdMYHL7FEYyK8HOKKwLX" + expected := &client.JWTSVID{Token: tok, IssuedAt: now, ExpiresAt: now.Add(time.Second)} - cache := NewJWTSVIDCache() + fakeMetrics := fakemetrics.New() + log, hook := test.NewNullLogger() + log.Level = logrus.DebugLevel + cache := NewJWTSVIDCache(log, fakeMetrics) spiffeID := spiffeid.RequireFromString("spiffe://example.org/blog") @@ -27,6 +40,46 @@ func TestJWTSVIDCacheBasic(t *testing.T) { actual, ok = cache.GetJWTSVID(spiffeID, []string{"bar"}) assert.True(t, ok) assert.Equal(t, expected, actual) + + // Remove tainted authority, should not be cached anymore + cache.TaintJWTSVIDs(context.Background(), map[string]struct{}{keyID: {}}) + actual, ok = cache.GetJWTSVID(spiffeID, []string{"bar"}) + assert.False(t, ok) + assert.Nil(t, actual) + + // Assert logs and metrics + expectLogs := []spiretest.LogEntry{ + { + Level: logrus.InfoLevel, + Message: "JWT-SVIDs were removed from the JWT cache because they were issued by a tainted authority", + Data: logrus.Fields{ + telemetry.CountJWTSVIDs: "1", + telemetry.JWTAuthorityKeyIDs: keyID, + }, + }, + } + expectMetrics := []fakemetrics.MetricItem{ + { + Type: fakemetrics.AddSampleType, + Key: []string{telemetry.CacheManager, telemetry.CountJWTSVIDs, agent.CacheTypeWorkload}, + Val: 1, + }, + { + Type: fakemetrics.IncrCounterWithLabelsType, + Key: []string{telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedJWTSVIDs}, + Val: 1, + Labels: []metrics.Label{{Name: "status", Value: "OK"}}, + }, + { + Type: fakemetrics.MeasureSinceWithLabelsType, + Key: []string{telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedJWTSVIDs, telemetry.ElapsedTime}, + Val: 0, + Labels: []metrics.Label{{Name: "status", Value: "OK"}}, + }, + } + + spiretest.AssertLogs(t, hook.AllEntries(), expectLogs) + assert.Equal(t, expectMetrics, fakeMetrics.AllMetrics()) } func TestJWTSVIDCacheKeyHashing(t *testing.T) { @@ -34,7 +87,10 @@ func TestJWTSVIDCacheKeyHashing(t *testing.T) { now := time.Now() expected := &client.JWTSVID{Token: "X", IssuedAt: now, ExpiresAt: now.Add(time.Second)} - cache := NewJWTSVIDCache() + fakeMetrics := fakemetrics.New() + log, _ := test.NewNullLogger() + log.Level = logrus.DebugLevel + cache := NewJWTSVIDCache(log, fakeMetrics) cache.SetJWTSVID(spiffeID, []string{"ab", "cd"}, expected) // JWT is cached diff --git a/pkg/agent/manager/cache/lru_cache.go b/pkg/agent/manager/cache/lru_cache.go index f3786ea296..07a07a3eec 100644 --- a/pkg/agent/manager/cache/lru_cache.go +++ b/pkg/agent/manager/cache/lru_cache.go @@ -14,6 +14,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/common/backoff" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" agentmetrics "github.com/spiffe/spire/pkg/common/telemetry/agent" "github.com/spiffe/spire/pkg/common/x509util" "github.com/spiffe/spire/proto/spire/common" @@ -42,7 +43,7 @@ type UpdateEntries struct { TaintedX509Authorities []string // TaintedJWTAuthorities is a set of all tainted JWT authorities notified by the server. - TaintedJWTAuthorities []string + TaintedJWTAuthorities map[string]struct{} // RegistrationEntries is a set of all registration entries available to the // agent, keyed by registration entry id. @@ -156,7 +157,7 @@ func NewLRUCache(log logrus.FieldLogger, trustDomain spiffeid.TrustDomain, bundl return &LRUCache{ BundleCache: NewBundleCache(trustDomain, bundle), - JWTSVIDCache: NewJWTSVIDCache(), + JWTSVIDCache: NewJWTSVIDCache(log, metrics), log: log, metrics: metrics, @@ -635,7 +636,7 @@ func (c *LRUCache) notifyTaintedBatchProcessed() { // processTaintedSVIDs identifies and removes tainted SVIDs from the cache that have been signed by the given tainted authorities. func (c *LRUCache) processTaintedSVIDs(entryIDs []string, taintedX509Authorities []*x509.Certificate) { - counter := telemetry.StartCall(c.metrics, telemetry.CacheManager, "", telemetry.ProcessTaintedSVIDs) + counter := telemetry.StartCall(c.metrics, telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedX509SVIDs) defer counter.Done(nil) taintedSVIDs := 0 @@ -664,8 +665,8 @@ func (c *LRUCache) processTaintedSVIDs(entryIDs []string, taintedX509Authorities } } - agentmetrics.AddCacheManagerTaintedSVIDsSample(c.metrics, "", float32(taintedSVIDs)) - c.log.WithField(telemetry.TaintedSVIDs, taintedSVIDs).Info("Tainted X.509 SVIDs") + agentmetrics.AddCacheManagerTaintedX509SVIDsSample(c.metrics, agentmetrics.CacheTypeWorkload, float32(taintedSVIDs)) + c.log.WithField(telemetry.TaintedX509SVIDs, taintedSVIDs).Info("Tainted X.509 SVIDs") } // Notify subscriber of selector set only if all SVIDs for corresponding selector set are cached diff --git a/pkg/agent/manager/cache/lru_cache_test.go b/pkg/agent/manager/cache/lru_cache_test.go index 3047631e57..c490cbd2d4 100644 --- a/pkg/agent/manager/cache/lru_cache_test.go +++ b/pkg/agent/manager/cache/lru_cache_test.go @@ -13,6 +13,7 @@ import ( "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" "github.com/spiffe/spire/proto/spire/common" "github.com/spiffe/spire/test/clock" "github.com/spiffe/spire/test/fakes/fakemetrics" @@ -1033,7 +1034,7 @@ func TestTaintX509SVIDs(t *testing.T) { expectElapsedTimeMetric := []fakemetrics.MetricItem{ { Type: fakemetrics.IncrCounterWithLabelsType, - Key: []string{"cache_manager", "", "process_tainted_svids"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedX509SVIDs}, Val: 1, Labels: []telemetry.Label{ { @@ -1044,7 +1045,7 @@ func TestTaintX509SVIDs(t *testing.T) { }, { Type: fakemetrics.MeasureSinceWithLabelsType, - Key: []string{"cache_manager", "", "process_tainted_svids", "elapsed_time"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeWorkload, telemetry.ProcessTaintedX509SVIDs, telemetry.ElapsedTime}, Val: 0, Labels: []telemetry.Label{ { @@ -1070,7 +1071,7 @@ func TestTaintX509SVIDs(t *testing.T) { { Level: logrus.InfoLevel, Message: "Tainted X.509 SVIDs", - Data: logrus.Fields{telemetry.TaintedSVIDs: "3"}, + Data: logrus.Fields{telemetry.TaintedX509SVIDs: "3"}, }, { Level: logrus.InfoLevel, @@ -1079,7 +1080,7 @@ func TestTaintX509SVIDs(t *testing.T) { }, } expectMetrics := append([]fakemetrics.MetricItem{ - {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, "", telemetry.TaintedSVIDs}, Val: 3}}, + {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, telemetry.TaintedX509SVIDs, agent.CacheTypeWorkload}, Val: 3}}, expectElapsedTimeMetric...) assertBatchProcess(expectLog, expectMetrics, "e3", "e4", "e5", "e6", "e7", "e8", "e9") @@ -1091,7 +1092,7 @@ func TestTaintX509SVIDs(t *testing.T) { { Level: logrus.InfoLevel, Message: "Tainted X.509 SVIDs", - Data: logrus.Fields{telemetry.TaintedSVIDs: "3"}, + Data: logrus.Fields{telemetry.TaintedX509SVIDs: "3"}, }, { Level: logrus.InfoLevel, @@ -1100,7 +1101,7 @@ func TestTaintX509SVIDs(t *testing.T) { }, } expectMetrics = append([]fakemetrics.MetricItem{ - {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, "", telemetry.TaintedSVIDs}, Val: 3}}, + {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, telemetry.TaintedX509SVIDs, agent.CacheTypeWorkload}, Val: 3}}, expectElapsedTimeMetric...) assertBatchProcess(expectLog, expectMetrics, "e3", "e4", "e8", "e9") @@ -1112,7 +1113,7 @@ func TestTaintX509SVIDs(t *testing.T) { { Level: logrus.InfoLevel, Message: "Tainted X.509 SVIDs", - Data: logrus.Fields{telemetry.TaintedSVIDs: "2"}, + Data: logrus.Fields{telemetry.TaintedX509SVIDs: "2"}, }, { Level: logrus.InfoLevel, @@ -1120,7 +1121,7 @@ func TestTaintX509SVIDs(t *testing.T) { }, } expectMetrics = append([]fakemetrics.MetricItem{ - {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, "", telemetry.TaintedSVIDs}, Val: 2}}, + {Type: fakemetrics.AddSampleType, Key: []string{telemetry.CacheManager, telemetry.TaintedX509SVIDs, agent.CacheTypeWorkload}, Val: 2}}, expectElapsedTimeMetric...) assertBatchProcess(expectLog, expectMetrics, "e3", "e4") } diff --git a/pkg/agent/manager/storecache/cache.go b/pkg/agent/manager/storecache/cache.go index a2c7538f84..85de43a544 100644 --- a/pkg/agent/manager/storecache/cache.go +++ b/pkg/agent/manager/storecache/cache.go @@ -12,6 +12,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/agent/manager/cache" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" telemetry_agent "github.com/spiffe/spire/pkg/common/telemetry/agent" "github.com/spiffe/spire/pkg/common/x509util" "github.com/spiffe/spire/proto/spire/common" @@ -228,7 +229,7 @@ func (c *Cache) TaintX509SVIDs(ctx context.Context, taintedX509Authorities []*x5 c.mtx.Lock() defer c.mtx.Unlock() - counter := telemetry.StartCall(c.c.Metrics, telemetry.CacheManager, "svid_store", telemetry.ProcessTaintedSVIDs) + counter := telemetry.StartCall(c.c.Metrics, telemetry.CacheManager, agent.CacheTypeSVIDStore, telemetry.ProcessTaintedX509SVIDs) defer counter.Done(nil) taintedSVIDs := 0 @@ -252,8 +253,12 @@ func (c *Cache) TaintX509SVIDs(ctx context.Context, taintedX509Authorities []*x5 } } - telemetry_agent.AddCacheManagerExpiredSVIDsSample(c.c.Metrics, "svid_store", float32(taintedSVIDs)) - c.c.Log.WithField(telemetry.TaintedSVIDs, taintedSVIDs).Info("Tainted X.509 SVIDs") + telemetry_agent.AddCacheManagerExpiredSVIDsSample(c.c.Metrics, agent.CacheTypeSVIDStore, float32(taintedSVIDs)) + c.c.Log.WithField(telemetry.TaintedX509SVIDs, taintedSVIDs).Info("Tainted X.509 SVIDs") +} + +func (c *Cache) TaintJWTSVIDs(ctx context.Context, taintedJWTAuthorities map[string]struct{}) { + // Nothing to do here } // GetStaleEntries obtains a list of stale entries, that needs new SVIDs diff --git a/pkg/agent/manager/storecache/cache_test.go b/pkg/agent/manager/storecache/cache_test.go index 0e10503ee4..dbb4031e78 100644 --- a/pkg/agent/manager/storecache/cache_test.go +++ b/pkg/agent/manager/storecache/cache_test.go @@ -16,6 +16,7 @@ import ( "github.com/spiffe/spire/pkg/agent/manager/cache" "github.com/spiffe/spire/pkg/agent/manager/storecache" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" "github.com/spiffe/spire/proto/spire/common" "github.com/spiffe/spire/test/fakes/fakemetrics" "github.com/spiffe/spire/test/spiretest" @@ -971,25 +972,25 @@ func TestTaintX509SVIDs(t *testing.T) { Level: logrus.InfoLevel, Message: "Tainted X.509 SVIDs", Data: logrus.Fields{ - telemetry.TaintedSVIDs: "3", + telemetry.TaintedX509SVIDs: "3", }, }, }, expectMetrics: []fakemetrics.MetricItem{ { Type: fakemetrics.AddSampleType, - Key: []string{"cache_manager", "svid_store", "expiring_svids", "svid_store"}, + Key: []string{telemetry.CacheManager, telemetry.ExpiringSVIDs, agent.CacheTypeSVIDStore}, Val: 3, }, { Type: fakemetrics.IncrCounterWithLabelsType, - Key: []string{"cache_manager", "svid_store", "process_tainted_svids"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeSVIDStore, telemetry.ProcessTaintedX509SVIDs}, Val: 1, Labels: []metrics.Label{{Name: "status", Value: "OK"}}, }, { Type: fakemetrics.MeasureSinceWithLabelsType, - Key: []string{"cache_manager", "svid_store", "process_tainted_svids", "elapsed_time"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeSVIDStore, telemetry.ProcessTaintedX509SVIDs, telemetry.ElapsedTime}, Val: 0, Labels: []metrics.Label{{Name: "status", Value: "OK"}}, }, @@ -1010,25 +1011,25 @@ func TestTaintX509SVIDs(t *testing.T) { Level: logrus.InfoLevel, Message: "Tainted X.509 SVIDs", Data: logrus.Fields{ - telemetry.TaintedSVIDs: "0", + telemetry.TaintedX509SVIDs: "0", }, }, }, expectMetrics: []fakemetrics.MetricItem{ { Type: fakemetrics.AddSampleType, - Key: []string{"cache_manager", "svid_store", "expiring_svids", "svid_store"}, + Key: []string{telemetry.CacheManager, telemetry.ExpiringSVIDs, agent.CacheTypeSVIDStore}, Val: 0, }, { Type: fakemetrics.IncrCounterWithLabelsType, - Key: []string{"cache_manager", "svid_store", "process_tainted_svids"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeSVIDStore, telemetry.ProcessTaintedX509SVIDs}, Val: 1, Labels: []metrics.Label{{Name: "status", Value: "OK"}}, }, { Type: fakemetrics.MeasureSinceWithLabelsType, - Key: []string{"cache_manager", "svid_store", "process_tainted_svids", "elapsed_time"}, + Key: []string{telemetry.CacheManager, agent.CacheTypeSVIDStore, telemetry.ProcessTaintedX509SVIDs, telemetry.ElapsedTime}, Val: 0, Labels: []metrics.Label{{Name: "status", Value: "OK"}}, }, diff --git a/pkg/agent/manager/sync.go b/pkg/agent/manager/sync.go index b856bdede7..5cd5f40034 100644 --- a/pkg/agent/manager/sync.go +++ b/pkg/agent/manager/sync.go @@ -9,12 +9,14 @@ import ( "time" "github.com/sirupsen/logrus" + "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/agent/client" "github.com/spiffe/spire/pkg/agent/manager/cache" "github.com/spiffe/spire/pkg/agent/workloadkey" "github.com/spiffe/spire/pkg/common/bundleutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/telemetry/agent" telemetry_agent "github.com/spiffe/spire/pkg/common/telemetry/agent" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/common/x509util" @@ -40,6 +42,11 @@ type SVIDCache interface { // TaintX509SVIDs marks all SVIDs signed by a tainted X.509 authority as tainted // to force their rotation. TaintX509SVIDs(ctx context.Context, taintedX509Authorities []*x509.Certificate) + + // TaintJWTSVIDs removes JWT-SVIDs with tainted authorities from the cache, + // forcing the server to issue a new JWT-SVID when one with a tainted + // authority is requested. + TaintJWTSVIDs(ctx context.Context, taintedJWTAuthorities map[string]struct{}) } func (m *manager) syncSVIDs(ctx context.Context) (err error) { @@ -48,13 +55,13 @@ func (m *manager) syncSVIDs(ctx context.Context) (err error) { } // processTaintedAuthorities verifies if a new authority is tainted and forces rotation in all caches if required. -func (m *manager) processTaintedAuthorities(ctx context.Context, x509Authorities []string, jwtAuthorities []string) error { +func (m *manager) processTaintedAuthorities(ctx context.Context, bundle *spiffebundle.Bundle, x509Authorities []string, jwtAuthorities map[string]struct{}) error { newTaintedX509Authorities := getNewItems(m.processedTaintedX509Authorities, x509Authorities) if len(newTaintedX509Authorities) > 0 { m.c.Log.WithField(telemetry.SubjectKeyIDs, strings.Join(newTaintedX509Authorities, ",")). Debug("New tainted X.509 authorities found") - taintedX509Authorities, err := bundleutil.FindX509Authorities(m.c.Bundle, newTaintedX509Authorities) + taintedX509Authorities, err := bundleutil.FindX509Authorities(bundle, newTaintedX509Authorities) if err != nil { return fmt.Errorf("failed to search X.509 authorities: %w", err) } @@ -75,11 +82,9 @@ func (m *manager) processTaintedAuthorities(ctx context.Context, x509Authorities } } - newTaintedJWTAuthorities := getNewItems(m.processedTaintedJWTAuthorities, jwtAuthorities) - if len(newTaintedJWTAuthorities) > 0 { - m.c.Log.WithField(telemetry.SubjectKeyIDs, strings.Join(newTaintedJWTAuthorities, ",")). - Debug("New tainted JWT authorities found") - // TODO: IMPLEMENT!!! + if len(jwtAuthorities) > 0 { + // Taint JWT-SVIDs in the cache + m.cache.TaintJWTSVIDs(ctx, jwtAuthorities) } return nil @@ -94,15 +99,15 @@ func (m *manager) synchronize(ctx context.Context) (err error) { } // Process all tainted authorities. The bundle is shared between both caches using regular cache data. - if err := m.processTaintedAuthorities(ctx, cacheUpdate.TaintedX509Authorities, cacheUpdate.TaintedJWTAuthorities); err != nil { + if err := m.processTaintedAuthorities(ctx, cacheUpdate.Bundles[m.c.TrustDomain], cacheUpdate.TaintedX509Authorities, cacheUpdate.TaintedJWTAuthorities); err != nil { return err } - if err := m.updateCache(ctx, cacheUpdate, m.c.Log.WithField(telemetry.CacheType, "workload"), "", m.cache); err != nil { + if err := m.updateCache(ctx, cacheUpdate, m.c.Log.WithField(telemetry.CacheType, agent.CacheTypeWorkload), "", m.cache); err != nil { return err } - if err := m.updateCache(ctx, storeUpdate, m.c.Log.WithField(telemetry.CacheType, "svid_store"), "svid_store", m.svidStoreCache); err != nil { + if err := m.updateCache(ctx, storeUpdate, m.c.Log.WithField(telemetry.CacheType, agent.CacheTypeSVIDStore), agent.CacheTypeSVIDStore, m.svidStoreCache); err != nil { return err } @@ -306,7 +311,7 @@ func (m *manager) fetchEntries(ctx context.Context) (_ *cache.UpdateEntries, _ * // Get all Subject Key IDs and KeyIDs of tainted authorities var taintedX509Authorities []string - var taintedJWTAuthorities []string + taintedJWTAuthorities := make(map[string]struct{}) if b, ok := update.Bundles[m.c.TrustDomain.IDString()]; ok { for _, rootCA := range b.RootCas { if rootCA.TaintedKey { @@ -320,7 +325,7 @@ func (m *manager) fetchEntries(ctx context.Context) (_ *cache.UpdateEntries, _ * } for _, jwtKey := range b.JwtSigningKeys { if jwtKey.TaintedKey { - taintedJWTAuthorities = append(taintedJWTAuthorities, jwtKey.Kid) + taintedJWTAuthorities[jwtKey.Kid] = struct{}{} } } } diff --git a/pkg/common/telemetry/agent/manager.go b/pkg/common/telemetry/agent/manager.go index b30105a397..428896b923 100644 --- a/pkg/common/telemetry/agent/manager.go +++ b/pkg/common/telemetry/agent/manager.go @@ -5,6 +5,11 @@ import ( "github.com/spiffe/spire/pkg/common/telemetry" ) +const ( + CacheTypeWorkload = "workload" + CacheTypeSVIDStore = "svid_store" +) + // Call Counters (timing and success metrics) // Allows adding labels in-code @@ -29,7 +34,7 @@ func StartManagerFetchSVIDsUpdatesCall(m telemetry.Metrics) *telemetry.CallCount // AddCacheManagerExpiredSVIDsSample count of expiring SVIDs according to // agent cache manager func AddCacheManagerExpiredSVIDsSample(m telemetry.Metrics, cacheType string, count float32) { - key := []string{telemetry.CacheManager, cacheType, telemetry.ExpiringSVIDs} + key := []string{telemetry.CacheManager, telemetry.ExpiringSVIDs} if cacheType != "" { key = append(key, cacheType) } @@ -46,10 +51,20 @@ func AddCacheManagerOutdatedSVIDsSample(m telemetry.Metrics, cacheType string, c m.AddSample(key, count) } -// AddCacheManagerTaintedSVIDsSample count of tainted SVIDs according to +// AddCacheManagerTaintedX509SVIDsSample count of tainted SVIDs according to +// agent cache manager +func AddCacheManagerTaintedX509SVIDsSample(m telemetry.Metrics, cacheType string, count float32) { + key := []string{telemetry.CacheManager, telemetry.TaintedX509SVIDs} + if cacheType != "" { + key = append(key, cacheType) + } + m.AddSample(key, count) +} + +// AddCacheManagerTaintedJWTSVIDsSample count of tainted SVIDs according to // agent cache manager -func AddCacheManagerTaintedSVIDsSample(m telemetry.Metrics, cacheType string, count float32) { - key := []string{telemetry.CacheManager, cacheType, telemetry.TaintedSVIDs} +func AddCacheManagerTaintedJWTSVIDsSample(m telemetry.Metrics, cacheType string, count float32) { + key := []string{telemetry.CacheManager, telemetry.CountJWTSVIDs} if cacheType != "" { key = append(key, cacheType) } diff --git a/pkg/common/telemetry/names.go b/pkg/common/telemetry/names.go index 165e41f2f8..95822884e2 100644 --- a/pkg/common/telemetry/names.go +++ b/pkg/common/telemetry/names.go @@ -356,9 +356,12 @@ const ( // JWTAuthorityExpiresAt tags a JWT Authority expiration JWTAuthorityExpiresAt = "jwt_authority_expires_at" - // JWTAuthorityPublicKey tags a JWT authority key ID + // JWTAuthorityKeyID tags a JWT authority key ID JWTAuthorityKeyID = "jwt_authority_key_id" + // JWTAuthorityKeyIDs tags a list of JWT authority key IDs + JWTAuthorityKeyIDs = "jwt_authority_key_ids" + // JWTAuthorityPublicKeySHA256 tags a JWT Authority public key JWTAuthorityPublicKeySHA256 = "jwt_authority_public_key_sha256" @@ -780,8 +783,11 @@ const ( // RegistrationManager functionality related to a registration manager RegistrationManager = "registration_manager" - // TaintedSVIDs tags tainted SVID count/list - TaintedSVIDs = "tainted_svids" + // CountJWTSVIDs functionality related to counting JWT-SVIDs + CountJWTSVIDs = "count_jwt_svids" + + // TaintedX509SVIDs tags tainted X.509 SVID count/list + TaintedX509SVIDs = "tainted_x509_svids" // Telemetry tags a telemetry module Telemetry = "telemetry" @@ -921,8 +927,11 @@ const ( // PushJWTKeyUpstream functionality related to pushing a public JWT Key to an upstream server. PushJWTKeyUpstream = "push_jwtkey_upstream" - // ProcessTaintedSVIDs functionality related to processing tainted SVIDs. - ProcessTaintedSVIDs = "process_tainted_svids" + // ProcessTaintedX509SVIDs functionality related to processing tainted X.509 SVIDs. + ProcessTaintedX509SVIDs = "process_tainted_x509_svids" + + // ProcessTaintedJWTSVIDs functionality related to processing tainted JWT SVIDs. + ProcessTaintedJWTSVIDs = "process_tainted_jwt_svids" // SDSAPI functionality related to SDS; should be used with other tags // to add clarity diff --git a/test/integration/suites/force-rotation-jwt-authority/00-setup b/test/integration/suites/force-rotation-jwt-authority/00-setup new file mode 100755 index 0000000000..7a467f829b --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/00-setup @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +"${ROOTDIR}/setup/x509pop/setup.sh" conf/server conf/agent + diff --git a/test/integration/suites/force-rotation-jwt-authority/01-start-server b/test/integration/suites/force-rotation-jwt-authority/01-start-server new file mode 100755 index 0000000000..a3e999b264 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/01-start-server @@ -0,0 +1,3 @@ +#!/bin/bash + +docker-up spire-server diff --git a/test/integration/suites/force-rotation-jwt-authority/02-bootstrap-agent b/test/integration/suites/force-rotation-jwt-authority/02-bootstrap-agent new file mode 100755 index 0000000000..8ee7d32c26 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/02-bootstrap-agent @@ -0,0 +1,5 @@ +#!/bin/bash + +log-debug "bootstrapping agent..." +docker compose exec -T spire-server \ + /opt/spire/bin/spire-server bundle show > conf/agent/bootstrap.crt diff --git a/test/integration/suites/force-rotation-jwt-authority/03-start-agent b/test/integration/suites/force-rotation-jwt-authority/03-start-agent new file mode 100755 index 0000000000..ac36d05f0d --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/03-start-agent @@ -0,0 +1,3 @@ +#!/bin/bash + +docker-up spire-agent diff --git a/test/integration/suites/force-rotation-jwt-authority/04-create-workload-entry b/test/integration/suites/force-rotation-jwt-authority/04-create-workload-entry new file mode 100755 index 0000000000..661c0ea6d8 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/04-create-workload-entry @@ -0,0 +1,14 @@ +#!/bin/bash + +log-debug "creating registration entry..." +docker compose exec -T spire-server \ + /opt/spire/bin/spire-server entry create \ + -parentID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" \ + -spiffeID "spiffe://domain.test/workload" \ + -selector "unix:uid:0" \ + -x509SVIDTTL 0 +check-synced-entry "spire-agent" "spiffe://domain.test/workload" + +log-info "checking X509-SVID" +docker compose exec -T spire-agent \ + /opt/spire/bin/spire-agent api fetch x509 || fail-now "SVID check failed" diff --git a/test/integration/suites/force-rotation-jwt-authority/05-prepare-jwt-authority b/test/integration/suites/force-rotation-jwt-authority/05-prepare-jwt-authority new file mode 100755 index 0000000000..8bcc43958f --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/05-prepare-jwt-authority @@ -0,0 +1,36 @@ +#!/bin/bash + +# Initial check for x509 authorities in spire-server +jwt_authorities=$(docker compose exec -T spire-server \ + /opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities' -c) + +amount_authorities=$(echo "$jwt_authorities" | jq length) + +# Ensure only one JWT authority is present at the start +if [[ $amount_authorities -ne 1 ]]; then + fail-now "Only one JWT authority expected at start" +fi + +# Prepare authority +prepared_authority_id=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server localauthority jwt prepare -output json | jq -r .prepared_authority.authority_id) + +# Verify that the prepared authority is logged +searching="JWT key prepared|local_authority_id=${prepared_authority_id}" +check-log-line spire-server "$searching" + +# Check for updated x509 authorities in spire-server +# Check for updated JWT authorities in spire-server +jwt_authorities=$(docker compose exec -T spire-server \ + /opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities' -c) +amount_authorities=$(echo "$jwt_authorities" | jq length) + +# Ensure two JWT authorities are present after preparation +if [[ $amount_authorities -ne 2 ]]; then + fail-now "Two JWT authorities expected after prepare" +fi + +# Ensure the prepared authority is present +if ! echo "$jwt_authorities" | jq -e ".[] | select(.key_id == \"$prepared_authority_id\")" > /dev/null; then + fail-now "Prepared authority not found" +fi diff --git a/test/integration/suites/force-rotation-jwt-authority/06-fetch-jwt-svid b/test/integration/suites/force-rotation-jwt-authority/06-fetch-jwt-svid new file mode 100755 index 0000000000..42c3a82b13 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/06-fetch-jwt-svid @@ -0,0 +1,50 @@ +#!/bin/bash + +prepared_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch prepared JWT authority ID" + +svid_json=$(docker compose exec spire-agent ./bin/spire-agent \ + api fetch jwt -audience aud -output json) || fail-now "Failed to fetch JWT SVID" + +jwt_svid=$(echo $svid_json | jq -c '.[0].svids[0].svid') || fail-now "Failed to parse JWT SVID" + +# Store JWT SVID for the next steps +echo $jwt_svid > conf/agent/jwt_svid + +# Extract key ID from JWT SVID +skid=$(echo "$jwt_svid" | jq -r 'split(".") | .[0] | @base64d | fromjson | .kid') + +# Check if the key ID matches the prepared authority ID +if [[ $skid != $prepared_authority ]]; then + fail-now "JWT SVID key ID does not match the prepared authority ID, got $skid, expected $prepared_authority" +fi + +keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson') + +retry_count=0 +max_retries=20 +success=false + +while [[ $retry_count -lt $max_retries ]]; do + keysLen=$(echo $keys | jq -c '.keys | length') + if [[ $keysLen -eq 2 ]]; then + success=true + break + else + echo "Retrying... ($((retry_count+1))/$max_retries)" + retry_count=$((retry_count+1)) + sleep 2 + # Re-fetch the JWT SVID and keys + svid_json=$(docker compose exec spire-agent ./bin/spire-agent \ + api fetch jwt -audience aud -output json) || fail-now "Failed to re-fetch JWT SVID" + jwt_svid=$(echo $svid_json | jq -c '.[0].svids[0].svid') || fail-now "Failed to parse re-fetched JWT SVID" + keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson') + fi +done + +if [[ $success == false ]]; then + fail-now "Expected one key in JWT SVID bundle, got $keysLen after $max_retries retries" +fi + +echo $keys | jq --arg kid $prepared_authority -e '.keys[] | select(.kid == $kid)' > /dev/null || fail-now "Prepared authority not found in JWT SVID bundle" diff --git a/test/integration/suites/force-rotation-jwt-authority/07-activate-jwt-authority b/test/integration/suites/force-rotation-jwt-authority/07-activate-jwt-authority new file mode 100755 index 0000000000..45683d098d --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/07-activate-jwt-authority @@ -0,0 +1,18 @@ +#!/bin/bash + +# Fetch the prepared authority ID +prepared_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .prepared.authority_id) || fail-now "Failed to fetch prepared JWT authority ID" + +# Activate the authority +activated_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt activate -authorityID "${prepared_authority}" \ + -output json | jq -r .activated_authority.authority_id) || fail-now "Failed to activate JWT authority" + +log-info "Activated authority: ${activated_authority}" + +# Check logs for specific lines +check-log-line spire-server "JWT key activated|local_authority_id=${prepared_authority}" + diff --git a/test/integration/suites/force-rotation-jwt-authority/08-taint-jwt-authority b/test/integration/suites/force-rotation-jwt-authority/08-taint-jwt-authority new file mode 100755 index 0000000000..13baa8ae17 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/08-taint-jwt-authority @@ -0,0 +1,31 @@ +#!/bin/bash + +check-logs() { + local component=$1 + shift + for log in "$@"; do + check-log-line "$component" "$log" + done +} + +# Fetch old authority ID +old_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .old.authority_id) || fail-now "Failed to fetch old authority ID" + +log-debug "Old authority: $old_jwt_authority" + +# Taint the old authority +docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt taint -authorityID "${old_jwt_authority}" || fail-now "Failed to taint old authority" + +# Root server logs +check-logs spire-server \ + "JWT authority tainted successfully|local_authority_id=${old_jwt_authority}" + +# TODO: update this error message check +# Root agent logs +check-logs spire-agent \ + "JWT-SVIDs were removed from the JWT cache because they were issued by a tainted authority|count_jwt_svids=1|jwt_authority_key_ids=${old_jwt_authority}" + diff --git a/test/integration/suites/force-rotation-jwt-authority/09-verify-svid-rotation b/test/integration/suites/force-rotation-jwt-authority/09-verify-svid-rotation new file mode 100755 index 0000000000..52d895d14e --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/09-verify-svid-rotation @@ -0,0 +1,21 @@ +#!/bin/bash + +active_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch active JWT authority ID" + +jwt_svid=$(docker compose exec spire-agent ./bin/spire-agent \ + api fetch jwt -audience aud -output json | jq -c '.[0].svids[0].svid') || fail-now "Failed to fetch JWT SVID" + +oldJWT=$(cat conf/agent/jwt_svid) +if [[ $oldJWT == $jwt_svid ]]; then + fail-now "JWT SVID did not rotate" +fi + +# Extract key ID from JWT SVID +skid=$(echo "$jwt_svid" | jq -r 'split(".") | .[0] | @base64d | fromjson | .kid') + +# Check if the key ID matches the active authority ID +if [[ $skid != $active_authority ]]; then + fail-now "JWT SVID key ID does not match the active authority ID, got $skid, expected $active_authority" +fi diff --git a/test/integration/suites/force-rotation-jwt-authority/10-revoke-jwt-authority b/test/integration/suites/force-rotation-jwt-authority/10-revoke-jwt-authority new file mode 100755 index 0000000000..747989a41b --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/10-revoke-jwt-authority @@ -0,0 +1,30 @@ +#!/bin/bash + +old_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .old.authority_id) || fail-now "Failed to fetch old authority ID" + +log-debug "Old authority: $old_jwt_authority" + +jwt_authorities_count=$(docker compose exec -T spire-server \ + /opt/spire/bin/spire-server bundle \ + show -output json | jq '.jwt_authorities | length') + +if [ $jwt_authorities_count -eq 2 ]; then + log-debug "Two JWT Authorities found" +else + fail-now "Expected to be two JWT Authorities. Found $jwt_authorities_count." +fi + +tainted_found=$(docker compose exec -T spire-server /opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities[] | select(.tainted == true)') + +if [[ -z "$tainted_found" ]]; then + fail-now "Tainted JWT authority expected" +fi + +docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server localauthority jwt \ + revoke -authorityID $old_jwt_authority -output json || fail-now "Failed to revoke JWT authority" + +check-log-line spire-server "JWT authority revoked successfully|local_authority_id=$old_jwt_authority" + diff --git a/test/integration/suites/force-rotation-jwt-authority/11-verify-revoked-jwt-authority b/test/integration/suites/force-rotation-jwt-authority/11-verify-revoked-jwt-authority new file mode 100755 index 0000000000..2d7ef4ca35 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/11-verify-revoked-jwt-authority @@ -0,0 +1,28 @@ +#!/bin/bash + +for i in {1..20}; do + active_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \ + /opt/spire/bin/spire-server \ + localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch old jwt authority ID" + + log-debug "Active old authority: $active_jwt_authority" + + svid_json=$(docker compose exec spire-agent ./bin/spire-agent \ + api fetch jwt -audience aud -output json) + + keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson') + + keysLen=$(echo $keys | jq -c '.keys | length') + if [[ $keysLen -eq 1 ]]; then + break + fi + + if [[ $i -eq 20 ]]; then + fail-now "Expected one key in JWT SVID bundle, got $keysLen after 20 attempts" + fi + + sleep 2s +done + +echo $keys | jq --arg kid $active_jwt_authority -e '.keys[] | select(.kid == $kid)' > /dev/null || fail-now "Active authority not found in JWT SVID bundle" + diff --git a/test/integration/suites/force-rotation-jwt-authority/README.md b/test/integration/suites/force-rotation-jwt-authority/README.md new file mode 100644 index 0000000000..63448f8e72 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/README.md @@ -0,0 +1,12 @@ +# Force rotation with JWT Authority Test Suite + +## Description + +This test suite configures a single SPIRE Server and Agent to validate the forced rotation and revocation of JWT authorities. + +## Test steps + +1. **Prepare a new JWT authority**: Verify that a new JWT authority is successfully created. +2. **Activate the new JWT authority**: Ensure that the new JWT authority becomes the active authority. +3. **Taint the old JWT authority**: Confirm that the old JWT authority is marked as tainted, and verify that the taint instruction is propagated to the agent, triggering the deletion of any JWT-SVID signed by tainted authority. +4. **Revoke the tainted JWT authority**: Validate that the revocation instruction is propagated to the agent and that all the JWT-SVIDs have the revoked authority removed. diff --git a/test/integration/suites/force-rotation-jwt-authority/conf/agent/agent.conf b/test/integration/suites/force-rotation-jwt-authority/conf/agent/agent.conf new file mode 100644 index 0000000000..f79c4e9b06 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/conf/agent/agent.conf @@ -0,0 +1,26 @@ +agent { + data_dir = "/opt/spire/data/agent" + log_level = "DEBUG" + server_address = "spire-server" + server_port = "8081" + trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt" + trust_domain = "domain.test" +} + +plugins { + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "/opt/spire/conf/agent/agent.key.pem" + certificate_path = "/opt/spire/conf/agent/agent.crt.pem" + } + } + KeyManager "disk" { + plugin_data { + directory = "/opt/spire/data/agent" + } + } + WorkloadAttestor "unix" { + plugin_data { + } + } +} diff --git a/test/integration/suites/force-rotation-jwt-authority/conf/server/server.conf b/test/integration/suites/force-rotation-jwt-authority/conf/server/server.conf new file mode 100644 index 0000000000..7793db57c1 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/conf/server/server.conf @@ -0,0 +1,29 @@ +server { + bind_address = "0.0.0.0" + bind_port = "8081" + trust_domain = "domain.test" + data_dir = "/opt/spire/data/server" + log_level = "DEBUG" + ca_ttl = "24h" + default_jwt_svid_ttl = "8h" + experimental { + feature_flags = ["forced_rotation"] + } +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/opt/spire/data/server/datastore.sqlite3" + } + } + NodeAttestor "x509pop" { + plugin_data { + ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" + } + } + KeyManager "memory" { + plugin_data = {} + } +} diff --git a/test/integration/suites/force-rotation-jwt-authority/docker-compose.yaml b/test/integration/suites/force-rotation-jwt-authority/docker-compose.yaml new file mode 100644 index 0000000000..288be5fd27 --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + spire-server: + image: spire-server:latest-local + hostname: spire-server + volumes: + - ./conf/server:/opt/spire/conf/server + command: ["-config", "/opt/spire/conf/server/server.conf"] + spire-agent: + image: spire-agent:latest-local + hostname: spire-agent + depends_on: ["spire-server"] + volumes: + - ./conf/agent:/opt/spire/conf/agent + command: ["-config", "/opt/spire/conf/agent/agent.conf"] diff --git a/test/integration/suites/force-rotation-jwt-authority/teardown b/test/integration/suites/force-rotation-jwt-authority/teardown new file mode 100755 index 0000000000..fabbf145ae --- /dev/null +++ b/test/integration/suites/force-rotation-jwt-authority/teardown @@ -0,0 +1,6 @@ +#!/bin/bash + +if [ -z "$SUCCESS" ]; then + docker compose logs +fi +docker-down