KEP-4193: Promote ServiceAccountTokenJTI, ServiceAccountTokenPodNodeInfo, ServiceAccountTokenNodeBindingValidation to stable

This commit is contained in:
Jordan Liggitt 2024-10-17 20:49:15 -04:00
parent 632ed16e00
commit 0771f601e1
No known key found for this signature in database
7 changed files with 61 additions and 57 deletions

View File

@ -639,6 +639,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
ServiceAccountTokenJTI: { ServiceAccountTokenJTI: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.34
}, },
ServiceAccountTokenNodeBinding: { ServiceAccountTokenNodeBinding: {
@ -649,11 +650,13 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
ServiceAccountTokenNodeBindingValidation: { ServiceAccountTokenNodeBindingValidation: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.34
}, },
ServiceAccountTokenPodNodeInfo: { ServiceAccountTokenPodNodeInfo: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.34
}, },
ServiceTrafficDistribution: { ServiceTrafficDistribution: {

View File

@ -43,7 +43,6 @@ import (
"k8s.io/component-base/featuregate" "k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
openapicommon "k8s.io/kube-openapi/pkg/common" openapicommon "k8s.io/kube-openapi/pkg/common"
kubefeatures "k8s.io/kubernetes/pkg/features"
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
"k8s.io/kubernetes/pkg/serviceaccount" "k8s.io/kubernetes/pkg/serviceaccount"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
@ -237,12 +236,6 @@ func TestAuthenticationValidate(t *testing.T) {
}, },
expectErr: "authentication-config file and oidc-* flags are mutually exclusive", expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
}, },
{
name: "fails to validate if ServiceAccountTokenNodeBindingValidation is disabled and ServiceAccountTokenNodeBinding is enabled",
enabledFeatures: []featuregate.Feature{kubefeatures.ServiceAccountTokenNodeBinding},
disabledFeatures: []featuregate.Feature{kubefeatures.ServiceAccountTokenNodeBindingValidation},
expectErr: "the \"ServiceAccountTokenNodeBinding\" feature gate can only be enabled if the \"ServiceAccountTokenNodeBindingValidation\" feature gate is also enabled",
},
{ {
name: "test when authentication config file and anonymous-auth flags are set AnonymousAuthConfigurableEndpoints disabled", name: "test when authentication config file and anonymous-auth flags are set AnonymousAuthConfigurableEndpoints disabled",
disabledFeatures: []featuregate.Feature{features.AnonymousAuthConfigurableEndpoints}, disabledFeatures: []featuregate.Feature{features.AnonymousAuthConfigurableEndpoints},
@ -489,7 +482,6 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
} }
func TestWithTokenGetterFunction(t *testing.T) { func TestWithTokenGetterFunction(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.ServiceAccountTokenNodeBindingValidation, false)
fakeClientset := fake.NewSimpleClientset() fakeClientset := fake.NewSimpleClientset()
versionedInformer := informers.NewSharedInformerFactory(fakeClientset, 0) versionedInformer := informers.NewSharedInformerFactory(fakeClientset, 0)
{ {

View File

@ -89,7 +89,7 @@ func TestClaims(t *testing.T) {
sc *jwt.Claims sc *jwt.Claims
pc *privateClaims pc *privateClaims
featureJTI, featurePodNodeInfo, featureNodeBinding bool featureNodeBinding bool
}{ }{
{ {
// pod and secret // pod and secret
@ -115,6 +115,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -138,6 +139,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -160,6 +162,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -182,6 +185,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800+60*60*24, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800+60*60*24, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -202,30 +206,6 @@ func TestClaims(t *testing.T) {
aud: nil, aud: nil,
err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled", err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled",
}, },
{
// node & pod with feature gate disabled
sa: sa,
node: node,
pod: pod,
// really fast
exp: 0,
// nil audience
aud: nil,
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
},
},
},
{ {
// node alone // node alone
sa: sa, sa: sa,
@ -242,6 +222,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -256,8 +237,6 @@ func TestClaims(t *testing.T) {
sa: sa, sa: sa,
pod: pod, pod: pod,
node: node, node: node,
// enable embedding pod node info feature
featurePodNodeInfo: true,
// really fast // really fast
exp: 0, exp: 0,
// nil audience // nil audience
@ -268,6 +247,7 @@ func TestClaims(t *testing.T) {
IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)), IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)), NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)), Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
ID: "fixed",
}, },
pc: &privateClaims{ pc: &privateClaims{
Kubernetes: kubernetes{ Kubernetes: kubernetes{
@ -294,8 +274,6 @@ func TestClaims(t *testing.T) {
{ {
// ensure JTI is set // ensure JTI is set
sa: sa, sa: sa,
// enable setting JTI feature
featureJTI: true,
// really fast // really fast
exp: 0, exp: 0,
// nil audience // nil audience
@ -342,9 +320,7 @@ func TestClaims(t *testing.T) {
} }
// set feature flags for the duration of the test case // set feature flags for the duration of the test case
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenJTI, c.featureJTI)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, c.featureNodeBinding) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, c.featureNodeBinding)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenPodNodeInfo, c.featurePodNodeInfo)
sc, pc, err := Claims(c.sa, c.pod, c.sec, c.node, c.exp, c.warnafter, c.aud) sc, pc, err := Claims(c.sa, c.pod, c.sec, c.node, c.exp, c.warnafter, c.aud)
if err != nil && err.Error() != c.err { if err != nil && err.Error() != c.err {
@ -376,8 +352,6 @@ type claimTestCase struct {
expiry jwt.NumericDate expiry jwt.NumericDate
notBefore jwt.NumericDate notBefore jwt.NumericDate
expectErr string expectErr string
featureNodeBindingValidation bool
} }
func TestValidatePrivateClaims(t *testing.T) { func TestValidatePrivateClaims(t *testing.T) {
@ -458,11 +432,10 @@ func TestValidatePrivateClaims(t *testing.T) {
expectErr: "service account token has been invalidated", expectErr: "service account token has been invalidated",
}, },
{ {
name: "missing node", name: "missing node",
getter: fakeGetter{serviceAccount, nil, nil, nil}, getter: fakeGetter{serviceAccount, nil, nil, nil},
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}}, private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}},
expectErr: "service account token has been invalidated", expectErr: "service account token has been invalidated",
featureNodeBindingValidation: true,
}, },
{ {
name: "different uid serviceaccount", name: "different uid serviceaccount",
@ -522,11 +495,10 @@ func TestValidatePrivateClaims(t *testing.T) {
expectErr: deletedErr, expectErr: deletedErr,
}, },
claimTestCase{ claimTestCase{
name: deletionTestCase.name + " node", name: deletionTestCase.name + " node",
getter: fakeGetter{serviceAccount, nil, nil, deletedNode}, getter: fakeGetter{serviceAccount, nil, nil, deletedNode},
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}}, private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}},
expectErr: deletedErr, expectErr: deletedErr,
featureNodeBindingValidation: true,
}, },
) )
} }
@ -539,8 +511,6 @@ func TestValidatePrivateClaims(t *testing.T) {
expiry = tc.expiry expiry = tc.expiry
} }
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBindingValidation, tc.featureNodeBindingValidation)
_, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: &expiry, NotBefore: &tc.notBefore}, tc.private) _, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: &expiry, NotBefore: &tc.notBefore}, tc.private)
if len(tc.expectErr) > 0 { if len(tc.expectErr) > 0 {
if errStr := errString(err); tc.expectErr != errStr { if errStr := errString(err); tc.expectErr != errStr {

View File

@ -24,6 +24,7 @@ import (
g "github.com/onsi/ginkgo/v2" g "github.com/onsi/ginkgo/v2"
o "github.com/onsi/gomega" o "github.com/onsi/gomega"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
authenticationv1 "k8s.io/api/authentication/v1" authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -34,7 +35,6 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
cgoscheme "k8s.io/client-go/kubernetes/scheme" cgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/framework"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod" e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
imageutils "k8s.io/kubernetes/test/utils/image" imageutils "k8s.io/kubernetes/test/utils/image"
@ -52,7 +52,7 @@ var (
perNodeCheckValidatingAdmissionPolicyBinding string perNodeCheckValidatingAdmissionPolicyBinding string
) )
var _ = SIGDescribe("ValidatingAdmissionPolicy", framework.WithFeatureGate(features.ServiceAccountTokenNodeBindingValidation), func() { var _ = SIGDescribe("ValidatingAdmissionPolicy", func() {
defer g.GinkgoRecover() defer g.GinkgoRecover()
f := framework.NewDefaultFramework("node-authn") f := framework.NewDefaultFramework("node-authn")
f.NamespacePodSecurityLevel = admissionapi.LevelRestricted f.NamespacePodSecurityLevel = admissionapi.LevelRestricted

View File

@ -100,6 +100,12 @@ var _ = SIGDescribe("ServiceAccounts", func() {
framework.ExpectNoError(err) framework.ExpectNoError(err)
framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)) framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod))
// Read the running pod to get the current node name
pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get(ctx, pod.Name, metav1.GetOptions{})
framework.ExpectNoError(err)
node, err := f.ClientSet.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{})
framework.ExpectNoError(err)
tk := e2ekubectl.NewTestKubeconfig(framework.TestContext.CertDir, framework.TestContext.Host, framework.TestContext.KubeConfig, framework.TestContext.KubeContext, framework.TestContext.KubectlPath, f.Namespace.Name) tk := e2ekubectl.NewTestKubeconfig(framework.TestContext.CertDir, framework.TestContext.Host, framework.TestContext.KubeConfig, framework.TestContext.KubeContext, framework.TestContext.KubectlPath, f.Namespace.Name)
mountedToken, err := tk.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, path.Join(serviceaccount.DefaultAPITokenMountPath, v1.ServiceAccountTokenKey)) mountedToken, err := tk.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, path.Join(serviceaccount.DefaultAPITokenMountPath, v1.ServiceAccountTokenKey))
framework.ExpectNoError(err) framework.ExpectNoError(err)
@ -133,6 +139,29 @@ var _ = SIGDescribe("ServiceAccounts", func() {
if !groups.Has("system:serviceaccounts:" + f.Namespace.Name) { if !groups.Has("system:serviceaccounts:" + f.Namespace.Name) {
framework.Failf("expected system:serviceaccounts:%s group, had %v", f.Namespace.Name, groups.List()) framework.Failf("expected system:serviceaccounts:%s group, had %v", f.Namespace.Name, groups.List())
} }
credentialID, ok := tokenReview.Status.User.Extra["authentication.kubernetes.io/credential-id"]
if !ok || len(credentialID) != 1 || !strings.HasPrefix(credentialID[0], "JTI=") {
framework.Failf("expected single authentication.kubernetes.io/credential-id extra info item starting with 'JTI=', got %v", credentialID)
}
podName, ok := tokenReview.Status.User.Extra["authentication.kubernetes.io/pod-name"]
if !ok || len(podName) != 1 || podName[0] != pod.Name {
framework.Failf("expected single authentication.kubernetes.io/pod-name extra info item matching %v, got %v", pod.Name, podName)
}
podUID, ok := tokenReview.Status.User.Extra["authentication.kubernetes.io/pod-uid"]
if !ok || len(podUID) != 1 || podUID[0] != string(pod.UID) {
framework.Failf("expected single authentication.kubernetes.io/pod-uid extra info item matching %v, got %v", pod.UID, podUID)
}
nodeName, ok := tokenReview.Status.User.Extra["authentication.kubernetes.io/node-name"]
if !ok || len(nodeName) != 1 || nodeName[0] != node.Name {
framework.Failf("expected single authentication.kubernetes.io/node-name extra info item matching %v, got %v", node.Name, nodeName)
}
nodeUID, ok := tokenReview.Status.User.Extra["authentication.kubernetes.io/node-uid"]
if !ok || len(nodeUID) != 1 || nodeUID[0] != string(node.UID) {
framework.Failf("expected single authentication.kubernetes.io/node-uid extra info item matching %v, got %v", node.UID, nodeUID)
}
}) })
/* /*

View File

@ -1036,6 +1036,10 @@
lockToDefault: false lockToDefault: false
preRelease: Beta preRelease: Beta
version: "1.30" version: "1.30"
- default: true
lockToDefault: true
preRelease: GA
version: "1.32"
- name: ServiceAccountTokenNodeBinding - name: ServiceAccountTokenNodeBinding
versionedSpecs: versionedSpecs:
- default: false - default: false
@ -1056,6 +1060,10 @@
lockToDefault: false lockToDefault: false
preRelease: Beta preRelease: Beta
version: "1.30" version: "1.30"
- default: true
lockToDefault: true
preRelease: GA
version: "1.32"
- name: ServiceAccountTokenPodNodeInfo - name: ServiceAccountTokenPodNodeInfo
versionedSpecs: versionedSpecs:
- default: false - default: false
@ -1066,6 +1074,10 @@
lockToDefault: false lockToDefault: false
preRelease: Beta preRelease: Beta
version: "1.30" version: "1.30"
- default: true
lockToDefault: true
preRelease: GA
version: "1.32"
- name: ServiceTrafficDistribution - name: ServiceTrafficDistribution
versionedSpecs: versionedSpecs:
- default: false - default: false

View File

@ -248,8 +248,6 @@ func TestServiceAccountTokenCreate(t *testing.T) {
}) })
t.Run("bound to service account and pod", func(t *testing.T) { t.Run("bound to service account and pod", func(t *testing.T) {
// Disable embedding pod's node info
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenPodNodeInfo, false)
treq := &authenticationv1.TokenRequest{ treq := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{ Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{"api"}, Audiences: []string{"api"},