From 7f12735fffdc490eae59e98d0f03638067b028de Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Fri, 2 Feb 2024 16:57:16 +0000 Subject: [PATCH] use authentication.kubernetes.io/issued-credential-id audit annotation in serviceaccount token registry endpoint --- .../serviceaccount/storage/storage_test.go | 73 ++++++++++++++++++- .../core/serviceaccount/storage/token.go | 2 +- .../pkg/authentication/serviceaccount/util.go | 6 ++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/pkg/registry/core/serviceaccount/storage/storage_test.go b/pkg/registry/core/serviceaccount/storage/storage_test.go index 9cae0884f56..32e4f058ea6 100644 --- a/pkg/registry/core/serviceaccount/storage/storage_test.go +++ b/pkg/registry/core/serviceaccount/storage/storage_test.go @@ -17,17 +17,28 @@ limitations under the License. package storage import ( + "context" "testing" + "gopkg.in/square/go-jose.v2/jwt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + "k8s.io/apiserver/pkg/registry/rest" etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/registry/registrytest" + token "k8s.io/kubernetes/pkg/serviceaccount" ) func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { @@ -38,13 +49,35 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { DeleteCollectionWorkers: 1, ResourcePrefix: "serviceaccounts", } - rest, err := NewREST(restOptions, nil, nil, 0, nil, nil, nil, false) + // set issuer, podStore and secretStore to allow the token endpoint to be initialised + rest, err := NewREST(restOptions, fakeTokenGenerator{"fake"}, nil, 0, panicGetter{}, panicGetter{}, nil, false) if err != nil { t.Fatalf("unexpected error from REST storage: %v", err) } return rest, server } +// A basic fake token generator which always returns a static string +type fakeTokenGenerator struct { + staticToken string +} + +func (f fakeTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) { + return f.staticToken, nil +} + +var _ token.TokenGenerator = fakeTokenGenerator{} + +// Currently this getter only panics as the only test case doesn't actually need the getters to function. +// When more test cases are added, this getter will need extending/replacing to have a real test implementation. +type panicGetter struct{} + +func (f panicGetter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + panic("not implemented") +} + +var _ rest.Getter = panicGetter{} + func validNewServiceAccount(name string) *api.ServiceAccount { return &api.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -73,6 +106,44 @@ func TestCreate(t *testing.T) { ) } +func TestCreate_Token_SetsCredentialIDAuditAnnotation(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + + // Enable JTI feature + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenJTI, true)() + + ctx := context.Background() + // Create a test service account + serviceAccount := validNewServiceAccount("foo") + // add the namespace to the context as it is required + ctx = request.WithNamespace(ctx, serviceAccount.Namespace) + _, err := storage.Store.Create(ctx, serviceAccount, rest.ValidateAllObjectFunc, &metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed creating test service account: %v", err) + } + + // create an audit context to allow recording audit information + ctx = audit.WithAuditContext(ctx) + _, err = storage.Token.Create(ctx, serviceAccount.Name, &authenticationapi.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccount.Name, + Namespace: serviceAccount.Namespace, + }, + Spec: authenticationapi.TokenRequestSpec{ExpirationSeconds: 3600}, + }, rest.ValidateAllObjectFunc, &metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed calling /token endpoint for service account: %v", err) + } + + auditContext := audit.AuditContextFrom(ctx) + issuedCredentialID, ok := auditContext.Event.Annotations["authentication.kubernetes.io/issued-credential-id"] + if !ok || len(issuedCredentialID) == 0 { + t.Errorf("did not find issued-credential-id in audit event annotations") + } +} + func TestUpdate(t *testing.T) { storage, server := newStorage(t) defer server.Terminate(t) diff --git a/pkg/registry/core/serviceaccount/storage/token.go b/pkg/registry/core/serviceaccount/storage/token.go index 5335afb8901..1e802bcb3e5 100644 --- a/pkg/registry/core/serviceaccount/storage/token.go +++ b/pkg/registry/core/serviceaccount/storage/token.go @@ -235,7 +235,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)}, } if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) && len(sc.ID) > 0 { - audit.AddAuditAnnotation(ctx, serviceaccount.CredentialIDKey, serviceaccount.CredentialIDForJTI(sc.ID)) + audit.AddAuditAnnotation(ctx, serviceaccount.IssuedCredentialIDAuditAnnotationKey, serviceaccount.CredentialIDForJTI(sc.ID)) } return out, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go b/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go index c55fe5d2ed6..3e29d4e71c9 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go @@ -39,6 +39,12 @@ const ( // CredentialIDKey is the key used in a user's "extra" to specify the unique // identifier for this identity document). CredentialIDKey = "authentication.kubernetes.io/credential-id" + // IssuedCredentialIDAuditAnnotationKey is the annotation key used in the audit event that is persisted to the + // '/token' endpoint for service accounts. + // This annotation indicates the generated credential identifier for the service account token being issued. + // This is useful when tracing back the origin of tokens that have gone on to make request that have persisted + // their credential-identifier into the audit log via the user's extra info stored on subsequent audit events. + IssuedCredentialIDAuditAnnotationKey = "authentication.kubernetes.io/issued-credential-id" // PodNameKey is the key used in a user's "extra" to specify the pod name of // the authenticating request. PodNameKey = "authentication.kubernetes.io/pod-name"