mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
KEP-4193: bound service account token improvements
This commit is contained in:
parent
5cb83d1cd2
commit
76463e21d4
@ -19,6 +19,7 @@ package serviceaccount
|
||||
import (
|
||||
"context"
|
||||
"k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
v1listers "k8s.io/client-go/listers/core/v1"
|
||||
@ -31,14 +32,15 @@ type clientGetter struct {
|
||||
secretLister v1listers.SecretLister
|
||||
serviceAccountLister v1listers.ServiceAccountLister
|
||||
podLister v1listers.PodLister
|
||||
nodeLister v1listers.NodeLister
|
||||
}
|
||||
|
||||
// NewGetterFromClient returns a ServiceAccountTokenGetter that
|
||||
// uses the specified client to retrieve service accounts and secrets.
|
||||
// uses the specified client to retrieve service accounts, pods, secrets and nodes.
|
||||
// The client should NOT authenticate using a service account token
|
||||
// the returned getter will be used to retrieve, or recursion will result.
|
||||
func NewGetterFromClient(c clientset.Interface, secretLister v1listers.SecretLister, serviceAccountLister v1listers.ServiceAccountLister, podLister v1listers.PodLister) serviceaccount.ServiceAccountTokenGetter {
|
||||
return clientGetter{c, secretLister, serviceAccountLister, podLister}
|
||||
func NewGetterFromClient(c clientset.Interface, secretLister v1listers.SecretLister, serviceAccountLister v1listers.ServiceAccountLister, podLister v1listers.PodLister, nodeLister v1listers.NodeLister) serviceaccount.ServiceAccountTokenGetter {
|
||||
return clientGetter{c, secretLister, serviceAccountLister, podLister, nodeLister}
|
||||
}
|
||||
|
||||
func (c clientGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) {
|
||||
@ -61,3 +63,14 @@ func (c clientGetter) GetSecret(namespace, name string) (*v1.Secret, error) {
|
||||
}
|
||||
return c.client.CoreV1().Secrets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (c clientGetter) GetNode(name string) (*v1.Node, error) {
|
||||
// handle the case where the node lister isn't set due to feature being disabled
|
||||
if c.nodeLister == nil {
|
||||
return nil, apierrors.NewNotFound(v1.Resource("nodes"), name)
|
||||
}
|
||||
if node, err := c.nodeLister.Get(name); err == nil {
|
||||
return node, nil
|
||||
}
|
||||
return c.client.CoreV1().Nodes().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
}
|
||||
|
@ -736,6 +736,36 @@ const (
|
||||
// Decouples Taint Eviction Controller, performing taint-based Pod eviction, from Node Lifecycle Controller.
|
||||
SeparateTaintEvictionController featuregate.Feature = "SeparateTaintEvictionController"
|
||||
|
||||
// owner: @munnerz
|
||||
// kep: http://kep.k8s.io/4193
|
||||
// alpha: v1.29
|
||||
//
|
||||
// Controls whether JTIs (UUIDs) are embedded into generated service account tokens, and whether these JTIs are
|
||||
// recorded into the audit log for future requests made by these tokens.
|
||||
ServiceAccountTokenJTI featuregate.Feature = "ServiceAccountTokenJTI"
|
||||
|
||||
// owner: @munnerz
|
||||
// kep: http://kep.k8s.io/4193
|
||||
// alpha: v1.29
|
||||
//
|
||||
// Controls whether the apiserver supports binding service account tokens to Node objects.
|
||||
ServiceAccountTokenNodeBinding featuregate.Feature = "ServiceAccountTokenNodeBinding"
|
||||
|
||||
// owner: @munnerz
|
||||
// kep: http://kep.k8s.io/4193
|
||||
// alpha: v1.29
|
||||
//
|
||||
// Controls whether the apiserver will validate Node claims in service account tokens.
|
||||
ServiceAccountTokenNodeBindingValidation featuregate.Feature = "ServiceAccountTokenNodeBindingValidation"
|
||||
|
||||
// owner: @munnerz
|
||||
// kep: http://kep.k8s.io/4193
|
||||
// alpha: v1.29
|
||||
//
|
||||
// Controls whether the apiserver embeds the node name and uid for the associated node when issuing
|
||||
// service account tokens bound to Pod objects.
|
||||
ServiceAccountTokenPodNodeInfo featuregate.Feature = "ServiceAccountTokenPodNodeInfo"
|
||||
|
||||
// owner: @xuzhenglun
|
||||
// kep: http://kep.k8s.io/3682
|
||||
// alpha: v1.27
|
||||
@ -1102,6 +1132,14 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
SeparateTaintEvictionController: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
ServiceAccountTokenJTI: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ServiceAccountTokenPodNodeInfo: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ServiceAccountTokenNodeBinding: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ServiceAccountTokenNodeBindingValidation: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ServiceNodePortStaticSubrange: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.29; remove in 1.31
|
||||
|
||||
SidecarContainers: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
@ -42,10 +42,12 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
v1listers "k8s.io/client-go/listers/core/v1"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
"k8s.io/klog/v2"
|
||||
openapicommon "k8s.io/kube-openapi/pkg/common"
|
||||
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
|
||||
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
||||
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap"
|
||||
@ -600,11 +602,16 @@ func (o *BuiltInAuthenticationOptions) ApplyTo(authInfo *genericapiserver.Authen
|
||||
authInfo.APIAudiences = authenticator.Audiences(o.ServiceAccounts.Issuers)
|
||||
}
|
||||
|
||||
var nodeLister v1listers.NodeLister
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBindingValidation) {
|
||||
nodeLister = versionedInformer.Core().V1().Nodes().Lister()
|
||||
}
|
||||
authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromClient(
|
||||
extclient,
|
||||
versionedInformer.Core().V1().Secrets().Lister(),
|
||||
versionedInformer.Core().V1().ServiceAccounts().Lister(),
|
||||
versionedInformer.Core().V1().Pods().Lister(),
|
||||
nodeLister,
|
||||
)
|
||||
authenticatorConfig.SecretsWriter = extclient.CoreV1()
|
||||
|
||||
|
@ -217,7 +217,12 @@ func (p *legacyProvider) NewRESTStorage(apiResourceConfigSource serverstorage.AP
|
||||
// potentially override the generic serviceaccount storage with one that supports pods
|
||||
var serviceAccountStorage *serviceaccountstore.REST
|
||||
if p.ServiceAccountIssuer != nil {
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), p.ExtendExpiration)
|
||||
var nodeGetter rest.Getter
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) ||
|
||||
utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
||||
nodeGetter = nodeStorage.Node.Store
|
||||
}
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), nodeGetter, p.ExtendExpiration)
|
||||
if err != nil {
|
||||
return genericapiserver.APIGroupInfo{}, err
|
||||
}
|
||||
|
@ -95,9 +95,9 @@ func (c *GenericConfig) NewRESTStorage(apiResourceConfigSource serverstorage.API
|
||||
|
||||
var serviceAccountStorage *serviceaccountstore.REST
|
||||
if c.ServiceAccountIssuer != nil {
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, c.ExtendExpiration)
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), c.ExtendExpiration)
|
||||
} else {
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), false)
|
||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), false)
|
||||
}
|
||||
if err != nil {
|
||||
return genericapiserver.APIGroupInfo{}, err
|
||||
|
@ -39,7 +39,7 @@ type REST struct {
|
||||
}
|
||||
|
||||
// NewREST returns a RESTStorage object that will work against service accounts.
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage rest.Getter, extendExpiration bool) (*REST, error) {
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage, nodeStorage rest.Getter, extendExpiration bool) (*REST, error) {
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
|
||||
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
|
||||
@ -64,6 +64,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator,
|
||||
svcaccts: store,
|
||||
pods: podStorage,
|
||||
secrets: secretStorage,
|
||||
nodes: nodeStorage,
|
||||
issuer: issuer,
|
||||
auds: auds,
|
||||
audsSet: sets.NewString(auds...),
|
||||
|
@ -38,7 +38,7 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
DeleteCollectionWorkers: 1,
|
||||
ResourcePrefix: "serviceaccounts",
|
||||
}
|
||||
rest, err := NewREST(restOptions, nil, nil, 0, nil, nil, false)
|
||||
rest, err := NewREST(restOptions, nil, nil, 0, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||
}
|
||||
|
@ -29,13 +29,18 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
"k8s.io/klog/v2"
|
||||
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
||||
authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
token "k8s.io/kubernetes/pkg/serviceaccount"
|
||||
)
|
||||
|
||||
@ -53,6 +58,7 @@ type TokenREST struct {
|
||||
svcaccts rest.Getter
|
||||
pods rest.Getter
|
||||
secrets rest.Getter
|
||||
nodes rest.Getter
|
||||
issuer token.TokenGenerator
|
||||
auds authenticator.Audiences
|
||||
audsSet sets.String
|
||||
@ -127,6 +133,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
||||
|
||||
var (
|
||||
pod *api.Pod
|
||||
node *api.Node
|
||||
secret *api.Secret
|
||||
)
|
||||
|
||||
@ -136,7 +143,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
||||
gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
|
||||
switch {
|
||||
case gvk.Group == "" && gvk.Kind == "Pod":
|
||||
newCtx := newContext(ctx, "pods", ref.Name, gvk)
|
||||
newCtx := newContext(ctx, "pods", ref.Name, namespace, gvk)
|
||||
podObj, err := r.pods.Get(newCtx, ref.Name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -146,8 +153,41 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
||||
return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token for serviceaccount %q to pod running with different serviceaccount name.", name))
|
||||
}
|
||||
uid = pod.UID
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
||||
if nodeName := pod.Spec.NodeName; nodeName != "" {
|
||||
newCtx := newContext(ctx, "nodes", nodeName, "", api.SchemeGroupVersion.WithKind("Node"))
|
||||
// set ResourceVersion=0 to allow this to be read/served from the apiservers watch cache
|
||||
nodeObj, err := r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{ResourceVersion: "0"})
|
||||
if err != nil {
|
||||
nodeObj, err = r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{}) // fallback to a live lookup on any error
|
||||
}
|
||||
switch {
|
||||
case errors.IsNotFound(err):
|
||||
// if the referenced Node object does not exist, we still embed just the pod name into the
|
||||
// claims so that clients still have some indication of what node a pod is assigned to when
|
||||
// inspecting a token (even if the UID is not present).
|
||||
klog.V(4).ErrorS(err, "failed fetching node for pod", "pod", klog.KObj(pod), "podUID", pod.UID, "nodeName", nodeName)
|
||||
node = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}
|
||||
case err != nil:
|
||||
return nil, errors.NewInternalError(err)
|
||||
default:
|
||||
node = nodeObj.(*api.Node)
|
||||
}
|
||||
}
|
||||
}
|
||||
case gvk.Group == "" && gvk.Kind == "Node":
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) {
|
||||
return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to a Node object as the %q feature-gate is disabled", features.ServiceAccountTokenNodeBinding))
|
||||
}
|
||||
newCtx := newContext(ctx, "nodes", ref.Name, "", gvk)
|
||||
nodeObj, err := r.nodes.Get(newCtx, ref.Name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node = nodeObj.(*api.Node)
|
||||
uid = node.UID
|
||||
case gvk.Group == "" && gvk.Kind == "Secret":
|
||||
newCtx := newContext(ctx, "secrets", ref.Name, gvk)
|
||||
newCtx := newContext(ctx, "secrets", ref.Name, namespace, gvk)
|
||||
secretObj, err := r.secrets.Get(newCtx, ref.Name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -179,7 +219,10 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
||||
exp = token.ExpirationExtensionSeconds
|
||||
}
|
||||
|
||||
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, req.Spec.Audiences)
|
||||
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokdata, err := r.issuer.GenerateToken(sc, pc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %v", err)
|
||||
@ -191,6 +234,9 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
||||
Token: tokdata,
|
||||
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))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -199,15 +245,11 @@ func (r *TokenREST) GroupVersionKind(schema.GroupVersion) schema.GroupVersionKin
|
||||
}
|
||||
|
||||
// newContext return a copy of ctx in which new RequestInfo is set
|
||||
func newContext(ctx context.Context, resource, name string, gvk schema.GroupVersionKind) context.Context {
|
||||
oldInfo, found := genericapirequest.RequestInfoFrom(ctx)
|
||||
if !found {
|
||||
return ctx
|
||||
}
|
||||
func newContext(ctx context.Context, resource, name, namespace string, gvk schema.GroupVersionKind) context.Context {
|
||||
newInfo := genericapirequest.RequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Verb: "get",
|
||||
Namespace: oldInfo.Namespace,
|
||||
Namespace: namespace,
|
||||
Resource: resource,
|
||||
Name: name,
|
||||
Parts: []string{resource, name},
|
||||
|
@ -22,12 +22,15 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/apiserver/pkg/audit"
|
||||
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -38,8 +41,12 @@ const (
|
||||
ExpirationExtensionSeconds = 24 * 365 * 60 * 60
|
||||
)
|
||||
|
||||
// time.Now stubbed out to allow testing
|
||||
var now = time.Now
|
||||
var (
|
||||
// time.Now stubbed out to allow testing
|
||||
now = time.Now
|
||||
// uuid.New stubbed out to allow testing
|
||||
newUUID = uuid.NewString
|
||||
)
|
||||
|
||||
type privateClaims struct {
|
||||
Kubernetes kubernetes `json:"kubernetes.io,omitempty"`
|
||||
@ -50,6 +57,7 @@ type kubernetes struct {
|
||||
Svcacct ref `json:"serviceaccount,omitempty"`
|
||||
Pod *ref `json:"pod,omitempty"`
|
||||
Secret *ref `json:"secret,omitempty"`
|
||||
Node *ref `json:"node,omitempty"`
|
||||
WarnAfter *jwt.NumericDate `json:"warnafter,omitempty"`
|
||||
}
|
||||
|
||||
@ -58,7 +66,7 @@ type ref struct {
|
||||
UID string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirationSeconds, warnafter int64, audience []string) (*jwt.Claims, interface{}) {
|
||||
func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, node *core.Node, expirationSeconds, warnafter int64, audience []string) (*jwt.Claims, interface{}, error) {
|
||||
now := now()
|
||||
sc := &jwt.Claims{
|
||||
Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name),
|
||||
@ -67,6 +75,9 @@ func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirati
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)),
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) {
|
||||
sc.ID = newUUID()
|
||||
}
|
||||
pc := &privateClaims{
|
||||
Kubernetes: kubernetes{
|
||||
Namespace: sa.Namespace,
|
||||
@ -76,24 +87,45 @@ func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirati
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if secret != nil && (node != nil || pod != nil) {
|
||||
return nil, nil, fmt.Errorf("internal error, token can only be bound to one object type")
|
||||
}
|
||||
switch {
|
||||
case pod != nil:
|
||||
pc.Kubernetes.Pod = &ref{
|
||||
Name: pod.Name,
|
||||
UID: string(pod.UID),
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
||||
// if this is bound to a pod and the node information is available, persist that too
|
||||
if node != nil {
|
||||
pc.Kubernetes.Node = &ref{
|
||||
Name: node.Name,
|
||||
UID: string(node.UID),
|
||||
}
|
||||
}
|
||||
}
|
||||
case secret != nil:
|
||||
pc.Kubernetes.Secret = &ref{
|
||||
Name: secret.Name,
|
||||
UID: string(secret.UID),
|
||||
}
|
||||
case node != nil:
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) {
|
||||
return nil, nil, fmt.Errorf("token bound to Node object requested, but %q feature gate is disabled", features.ServiceAccountTokenNodeBinding)
|
||||
}
|
||||
pc.Kubernetes.Node = &ref{
|
||||
Name: node.Name,
|
||||
UID: string(node.UID),
|
||||
}
|
||||
}
|
||||
|
||||
if warnafter != 0 {
|
||||
pc.Kubernetes.WarnAfter = jwt.NewNumericDate(now.Add(time.Duration(warnafter) * time.Second))
|
||||
}
|
||||
|
||||
return sc, pc
|
||||
return sc, pc, nil
|
||||
}
|
||||
|
||||
func NewValidator(getter ServiceAccountTokenGetter) Validator {
|
||||
@ -146,6 +178,7 @@ func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims,
|
||||
namespace := private.Kubernetes.Namespace
|
||||
saref := private.Kubernetes.Svcacct
|
||||
podref := private.Kubernetes.Pod
|
||||
noderef := private.Kubernetes.Node
|
||||
secref := private.Kubernetes.Secret
|
||||
// Make sure service account still exists (name and UID)
|
||||
serviceAccount, err := v.getter.GetServiceAccount(namespace, saref.Name)
|
||||
@ -153,14 +186,15 @@ func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims,
|
||||
klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, saref.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
if serviceAccount.DeletionTimestamp != nil && serviceAccount.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name)
|
||||
return nil, fmt.Errorf("service account %s/%s has been deleted", namespace, saref.Name)
|
||||
}
|
||||
|
||||
if string(serviceAccount.UID) != saref.UID {
|
||||
klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, saref.Name, string(serviceAccount.UID), saref.UID)
|
||||
return nil, fmt.Errorf("service account UID (%s) does not match claim (%s)", serviceAccount.UID, saref.UID)
|
||||
}
|
||||
if serviceAccount.DeletionTimestamp != nil && serviceAccount.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name)
|
||||
return nil, fmt.Errorf("service account %s/%s has been deleted", namespace, saref.Name)
|
||||
}
|
||||
|
||||
if secref != nil {
|
||||
// Make sure token hasn't been invalidated by deletion of the secret
|
||||
@ -169,14 +203,14 @@ func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims,
|
||||
klog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, secref.Name, namespace, saref.Name, err)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
if secret.DeletionTimestamp != nil && secret.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
if secref.UID != string(secret.UID) {
|
||||
klog.V(4).Infof("Secret UID no longer matches %s/%s: %q != %q", namespace, secref.Name, string(secret.UID), secref.UID)
|
||||
return nil, fmt.Errorf("secret UID (%s) does not match service account secret ref claim (%s)", secret.UID, secref.UID)
|
||||
}
|
||||
if secret.DeletionTimestamp != nil && secret.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
}
|
||||
|
||||
var podName, podUID string
|
||||
@ -187,18 +221,51 @@ func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims,
|
||||
klog.V(4).Infof("Could not retrieve bound pod %s/%s for service account %s/%s: %v", namespace, podref.Name, namespace, saref.Name, err)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
if pod.DeletionTimestamp != nil && pod.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
if podref.UID != string(pod.UID) {
|
||||
klog.V(4).Infof("Pod UID no longer matches %s/%s: %q != %q", namespace, podref.Name, string(pod.UID), podref.UID)
|
||||
return nil, fmt.Errorf("pod UID (%s) does not match service account pod ref claim (%s)", pod.UID, podref.UID)
|
||||
}
|
||||
if pod.DeletionTimestamp != nil && pod.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
podName = podref.Name
|
||||
podUID = podref.UID
|
||||
}
|
||||
|
||||
var nodeName, nodeUID string
|
||||
if noderef != nil {
|
||||
switch {
|
||||
case podref != nil:
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
||||
// for pod-bound tokens, just extract the node claims
|
||||
nodeName = noderef.Name
|
||||
nodeUID = noderef.UID
|
||||
}
|
||||
case podref == nil:
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBindingValidation) {
|
||||
klog.V(4).Infof("ServiceAccount token is bound to a Node object, but the node bound token validation feature is disabled")
|
||||
return nil, fmt.Errorf("token is bound to a Node object but the %s feature gate is disabled", features.ServiceAccountTokenNodeBindingValidation)
|
||||
}
|
||||
|
||||
node, err := v.getter.GetNode(noderef.Name)
|
||||
if err != nil {
|
||||
klog.V(4).Infof("Could not retrieve node object %q for service account %s/%s: %v", noderef.Name, namespace, saref.Name, err)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
if noderef.UID != string(node.UID) {
|
||||
klog.V(4).Infof("Node UID no longer matches %s: %q != %q", noderef.Name, string(node.UID), noderef.UID)
|
||||
return nil, fmt.Errorf("node UID (%s) does not match service account node ref claim (%s)", node.UID, noderef.UID)
|
||||
}
|
||||
if node.DeletionTimestamp != nil && node.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
|
||||
klog.V(4).Infof("Node %q is deleted and awaiting removal for service account %s/%s", node.Name, namespace, saref.Name)
|
||||
return nil, errors.New("service account token has been invalidated")
|
||||
}
|
||||
nodeName = noderef.Name
|
||||
nodeUID = noderef.UID
|
||||
}
|
||||
}
|
||||
|
||||
// Check special 'warnafter' field for projected service account token transition.
|
||||
warnafter := private.Kubernetes.WarnAfter
|
||||
if warnafter != nil && *warnafter != 0 {
|
||||
@ -212,12 +279,19 @@ func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims,
|
||||
}
|
||||
}
|
||||
|
||||
var jti string
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) {
|
||||
jti = public.ID
|
||||
}
|
||||
return &apiserverserviceaccount.ServiceAccountInfo{
|
||||
Namespace: private.Kubernetes.Namespace,
|
||||
Name: private.Kubernetes.Svcacct.Name,
|
||||
UID: private.Kubernetes.Svcacct.UID,
|
||||
PodName: podName,
|
||||
PodUID: podUID,
|
||||
Namespace: private.Kubernetes.Namespace,
|
||||
Name: private.Kubernetes.Svcacct.Name,
|
||||
UID: private.Kubernetes.Svcacct.UID,
|
||||
PodName: podName,
|
||||
PodUID: podUID,
|
||||
NodeName: nodeName,
|
||||
NodeUID: nodeUID,
|
||||
CredentialID: apiserverserviceaccount.CredentialIDForJTI(jti),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,10 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -37,6 +40,11 @@ func init() {
|
||||
// epoch time: 1514764800
|
||||
return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
newUUID = func() string {
|
||||
// always return a fixed/static UUID for testing
|
||||
return "fixed"
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaims(t *testing.T) {
|
||||
@ -61,17 +69,27 @@ func TestClaims(t *testing.T) {
|
||||
UID: "mysecret-uid",
|
||||
},
|
||||
}
|
||||
node := &core.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "mynode",
|
||||
UID: "mynode-uid",
|
||||
},
|
||||
}
|
||||
cs := []struct {
|
||||
// input
|
||||
sa core.ServiceAccount
|
||||
pod *core.Pod
|
||||
sec *core.Secret
|
||||
node *core.Node
|
||||
exp int64
|
||||
warnafter int64
|
||||
aud []string
|
||||
err string
|
||||
// desired
|
||||
sc *jwt.Claims
|
||||
pc *privateClaims
|
||||
|
||||
featureJTI, featurePodNodeInfo, featureNodeBinding bool
|
||||
}{
|
||||
{
|
||||
// pod and secret
|
||||
@ -82,20 +100,7 @@ func TestClaims(t *testing.T) {
|
||||
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",
|
||||
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
|
||||
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
|
||||
},
|
||||
},
|
||||
err: "internal error, token can only be bound to one object type",
|
||||
},
|
||||
{
|
||||
// pod
|
||||
@ -167,7 +172,6 @@ func TestClaims(t *testing.T) {
|
||||
// warn after provided
|
||||
sa: sa,
|
||||
pod: pod,
|
||||
sec: sec,
|
||||
exp: 60 * 60 * 24,
|
||||
warnafter: 60 * 60,
|
||||
// nil audience
|
||||
@ -188,6 +192,141 @@ func TestClaims(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// node with feature gate disabled
|
||||
sa: sa,
|
||||
node: node,
|
||||
// really fast
|
||||
exp: 0,
|
||||
// nil audience
|
||||
aud: nil,
|
||||
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
|
||||
sa: sa,
|
||||
node: node,
|
||||
// enable node binding feature
|
||||
featureNodeBinding: true,
|
||||
// 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",
|
||||
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
|
||||
Node: &ref{Name: "mynode", UID: "mynode-uid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// node and pod
|
||||
sa: sa,
|
||||
pod: pod,
|
||||
node: node,
|
||||
// enable embedding pod node info feature
|
||||
featurePodNodeInfo: true,
|
||||
// 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",
|
||||
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
|
||||
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
|
||||
Node: &ref{Name: "mynode", UID: "mynode-uid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// node and secret should error
|
||||
sa: sa,
|
||||
sec: sec,
|
||||
node: node,
|
||||
// enable embedding node info feature
|
||||
featureNodeBinding: true,
|
||||
// really fast
|
||||
exp: 0,
|
||||
// nil audience
|
||||
aud: nil,
|
||||
err: "internal error, token can only be bound to one object type",
|
||||
},
|
||||
{
|
||||
// ensure JTI is set
|
||||
sa: sa,
|
||||
// enable setting JTI feature
|
||||
featureJTI: true,
|
||||
// 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)),
|
||||
ID: "fixed",
|
||||
},
|
||||
pc: &privateClaims{
|
||||
Kubernetes: kubernetes{
|
||||
Namespace: "myns",
|
||||
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// ensure it fails if node binding gate is disabled
|
||||
sa: sa,
|
||||
node: node,
|
||||
featureNodeBinding: false,
|
||||
// really fast
|
||||
exp: 0,
|
||||
// nil audience
|
||||
aud: nil,
|
||||
|
||||
err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled",
|
||||
},
|
||||
}
|
||||
for i, c := range cs {
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
@ -202,7 +341,18 @@ func TestClaims(t *testing.T) {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
sc, pc := Claims(c.sa, c.pod, c.sec, c.exp, c.warnafter, c.aud)
|
||||
// set feature flags for the duration of the test case
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenJTI, c.featureJTI)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, c.featureNodeBinding)()
|
||||
defer 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)
|
||||
if err != nil && err.Error() != c.err {
|
||||
t.Errorf("expected error %q but got: %v", c.err, err)
|
||||
}
|
||||
if err == nil && c.err != "" {
|
||||
t.Errorf("expected an error but got none")
|
||||
}
|
||||
if spew(sc) != spew(c.sc) {
|
||||
t.Errorf("standard claims differed\n\tsaw:\t%s\n\twant:\t%s", spew(sc), spew(c.sc))
|
||||
}
|
||||
@ -226,6 +376,8 @@ type claimTestCase struct {
|
||||
expiry jwt.NumericDate
|
||||
notBefore jwt.NumericDate
|
||||
expectErr string
|
||||
|
||||
featureNodeBindingValidation bool
|
||||
}
|
||||
|
||||
func TestValidatePrivateClaims(t *testing.T) {
|
||||
@ -235,6 +387,7 @@ func TestValidatePrivateClaims(t *testing.T) {
|
||||
serviceAccount = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "saname", Namespace: "ns", UID: "sauid"}}
|
||||
secret = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secretname", Namespace: "ns", UID: "secretuid"}}
|
||||
pod = &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "podname", Namespace: "ns", UID: "poduid"}}
|
||||
node = &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "nodename", UID: "nodeuid"}}
|
||||
)
|
||||
|
||||
deletionTestCases := []deletionTestCase{
|
||||
@ -268,57 +421,64 @@ func TestValidatePrivateClaims(t *testing.T) {
|
||||
testcases := []claimTestCase{
|
||||
{
|
||||
name: "good",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
|
||||
expectErr: "",
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
|
||||
expiry: *jwt.NewNumericDate(now().Add(-1_000 * time.Hour)),
|
||||
expectErr: "service account token has expired",
|
||||
},
|
||||
{
|
||||
name: "not yet valid",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
|
||||
notBefore: *jwt.NewNumericDate(now().Add(1_000 * time.Hour)),
|
||||
expectErr: "service account token is not valid yet",
|
||||
},
|
||||
{
|
||||
name: "missing serviceaccount",
|
||||
getter: fakeGetter{nil, nil, nil},
|
||||
getter: fakeGetter{nil, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
|
||||
expectErr: `serviceaccounts "saname" not found`,
|
||||
},
|
||||
{
|
||||
name: "missing secret",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}},
|
||||
expectErr: "service account token has been invalidated",
|
||||
},
|
||||
{
|
||||
name: "missing pod",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}},
|
||||
expectErr: "service account token has been invalidated",
|
||||
},
|
||||
{
|
||||
name: "missing node",
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
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",
|
||||
featureNodeBindingValidation: true,
|
||||
},
|
||||
{
|
||||
name: "different uid serviceaccount",
|
||||
getter: fakeGetter{serviceAccount, nil, nil},
|
||||
getter: fakeGetter{serviceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauidold"}, Namespace: "ns"}},
|
||||
expectErr: "service account UID (sauid) does not match claim (sauidold)",
|
||||
},
|
||||
{
|
||||
name: "different uid secret",
|
||||
getter: fakeGetter{serviceAccount, secret, nil},
|
||||
getter: fakeGetter{serviceAccount, secret, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuidold"}, Namespace: "ns"}},
|
||||
expectErr: "secret UID (secretuid) does not match service account secret ref claim (secretuidold)",
|
||||
},
|
||||
{
|
||||
name: "different uid pod",
|
||||
getter: fakeGetter{serviceAccount, nil, pod},
|
||||
getter: fakeGetter{serviceAccount, nil, pod, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduidold"}, Namespace: "ns"}},
|
||||
expectErr: "pod UID (poduid) does not match service account pod ref claim (poduidold)",
|
||||
},
|
||||
@ -329,10 +489,12 @@ func TestValidatePrivateClaims(t *testing.T) {
|
||||
deletedServiceAccount = serviceAccount.DeepCopy()
|
||||
deletedPod = pod.DeepCopy()
|
||||
deletedSecret = secret.DeepCopy()
|
||||
deletedNode = node.DeepCopy()
|
||||
)
|
||||
deletedServiceAccount.DeletionTimestamp = deletionTestCase.time
|
||||
deletedPod.DeletionTimestamp = deletionTestCase.time
|
||||
deletedSecret.DeletionTimestamp = deletionTestCase.time
|
||||
deletedNode.DeletionTimestamp = deletionTestCase.time
|
||||
|
||||
var saDeletedErr, deletedErr string
|
||||
if deletionTestCase.expectErr {
|
||||
@ -343,32 +505,42 @@ func TestValidatePrivateClaims(t *testing.T) {
|
||||
testcases = append(testcases,
|
||||
claimTestCase{
|
||||
name: deletionTestCase.name + " serviceaccount",
|
||||
getter: fakeGetter{deletedServiceAccount, nil, nil},
|
||||
getter: fakeGetter{deletedServiceAccount, nil, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
|
||||
expectErr: saDeletedErr,
|
||||
},
|
||||
claimTestCase{
|
||||
name: deletionTestCase.name + " secret",
|
||||
getter: fakeGetter{serviceAccount, deletedSecret, nil},
|
||||
getter: fakeGetter{serviceAccount, deletedSecret, nil, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}},
|
||||
expectErr: deletedErr,
|
||||
},
|
||||
claimTestCase{
|
||||
name: deletionTestCase.name + " pod",
|
||||
getter: fakeGetter{serviceAccount, nil, deletedPod},
|
||||
getter: fakeGetter{serviceAccount, nil, deletedPod, nil},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}},
|
||||
expectErr: deletedErr,
|
||||
},
|
||||
claimTestCase{
|
||||
name: deletionTestCase.name + " node",
|
||||
getter: fakeGetter{serviceAccount, nil, nil, deletedNode},
|
||||
private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}},
|
||||
expectErr: deletedErr,
|
||||
featureNodeBindingValidation: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
v := &validator{tc.getter}
|
||||
v := &validator{getter: tc.getter}
|
||||
expiry := jwt.NumericDate(nowUnix)
|
||||
if tc.expiry != 0 {
|
||||
expiry = tc.expiry
|
||||
}
|
||||
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBindingValidation, tc.featureNodeBindingValidation)()
|
||||
|
||||
_, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: &expiry, NotBefore: &tc.notBefore}, tc.private)
|
||||
if len(tc.expectErr) > 0 {
|
||||
if errStr := errString(err); tc.expectErr != errStr {
|
||||
@ -393,6 +565,7 @@ type fakeGetter struct {
|
||||
serviceAccount *v1.ServiceAccount
|
||||
secret *v1.Secret
|
||||
pod *v1.Pod
|
||||
node *v1.Node
|
||||
}
|
||||
|
||||
func (f fakeGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) {
|
||||
@ -413,3 +586,9 @@ func (f fakeGetter) GetSecret(namespace, name string) (*v1.Secret, error) {
|
||||
}
|
||||
return f.secret, nil
|
||||
}
|
||||
func (f fakeGetter) GetNode(name string) (*v1.Node, error) {
|
||||
if f.node == nil {
|
||||
return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "nodes"}, name)
|
||||
}
|
||||
return f.node, nil
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ type ServiceAccountTokenGetter interface {
|
||||
GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error)
|
||||
GetPod(namespace, name string) (*v1.Pod, error)
|
||||
GetSecret(namespace, name string) (*v1.Secret, error)
|
||||
GetNode(name string) (*v1.Node, error)
|
||||
}
|
||||
|
||||
type TokenGenerator interface {
|
||||
|
@ -371,6 +371,9 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
||||
v1listers.NewPodLister(newIndexer(func(namespace, name string) (interface{}, error) {
|
||||
return tc.Client.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
})),
|
||||
v1listers.NewNodeLister(newIndexer(func(_, name string) (interface{}, error) {
|
||||
return tc.Client.CoreV1().Nodes().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
})),
|
||||
)
|
||||
var secretsWriter typedv1core.SecretsGetter
|
||||
if tc.Client != nil {
|
||||
@ -443,6 +446,11 @@ func (f *fakeIndexer) GetByKey(key string) (interface{}, bool, error) {
|
||||
parts := strings.SplitN(key, "/", 2)
|
||||
namespace := parts[0]
|
||||
name := ""
|
||||
// implies the key does not contain a / (this is a cluster-scoped object)
|
||||
if len(parts) == 1 {
|
||||
name = parts[0]
|
||||
namespace = ""
|
||||
}
|
||||
if len(parts) == 2 {
|
||||
name = parts[1]
|
||||
}
|
||||
|
@ -36,12 +36,21 @@ const (
|
||||
ServiceAccountUsernameSeparator = ":"
|
||||
ServiceAccountGroupPrefix = "system:serviceaccounts:"
|
||||
AllServiceAccountsGroup = "system:serviceaccounts"
|
||||
// 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"
|
||||
// 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"
|
||||
// PodUIDKey is the key used in a user's "extra" to specify the pod UID of
|
||||
// the authenticating request.
|
||||
PodUIDKey = "authentication.kubernetes.io/pod-uid"
|
||||
// NodeNameKey is the key used in a user's "extra" to specify the node name of
|
||||
// the authenticating request.
|
||||
NodeNameKey = "authentication.kubernetes.io/node-name"
|
||||
// NodeUIDKey is the key used in a user's "extra" to specify the node UID of
|
||||
// the authenticating request.
|
||||
NodeUIDKey = "authentication.kubernetes.io/node-uid"
|
||||
)
|
||||
|
||||
// MakeUsername generates a username from the given namespace and ServiceAccount name.
|
||||
@ -119,6 +128,8 @@ func UserInfo(namespace, name, uid string) user.Info {
|
||||
type ServiceAccountInfo struct {
|
||||
Name, Namespace, UID string
|
||||
PodName, PodUID string
|
||||
CredentialID string
|
||||
NodeName, NodeUID string
|
||||
}
|
||||
|
||||
func (sa *ServiceAccountInfo) UserInfo() user.Info {
|
||||
@ -127,15 +138,43 @@ func (sa *ServiceAccountInfo) UserInfo() user.Info {
|
||||
UID: sa.UID,
|
||||
Groups: MakeGroupNames(sa.Namespace),
|
||||
}
|
||||
|
||||
if sa.PodName != "" && sa.PodUID != "" {
|
||||
info.Extra = map[string][]string{
|
||||
PodNameKey: {sa.PodName},
|
||||
PodUIDKey: {sa.PodUID},
|
||||
if info.Extra == nil {
|
||||
info.Extra = make(map[string][]string)
|
||||
}
|
||||
info.Extra[PodNameKey] = []string{sa.PodName}
|
||||
info.Extra[PodUIDKey] = []string{sa.PodUID}
|
||||
}
|
||||
if sa.CredentialID != "" {
|
||||
if info.Extra == nil {
|
||||
info.Extra = make(map[string][]string)
|
||||
}
|
||||
info.Extra[CredentialIDKey] = []string{sa.CredentialID}
|
||||
}
|
||||
if sa.NodeName != "" {
|
||||
if info.Extra == nil {
|
||||
info.Extra = make(map[string][]string)
|
||||
}
|
||||
info.Extra[NodeNameKey] = []string{sa.NodeName}
|
||||
// node UID is optional and will only be set if the node name is set
|
||||
if sa.NodeUID != "" {
|
||||
info.Extra[NodeUIDKey] = []string{sa.NodeUID}
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// CredentialIDForJTI converts a given JTI string into a credential identifier for use in a
|
||||
// users 'extra' info.
|
||||
func CredentialIDForJTI(jti string) string {
|
||||
if len(jti) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "JTI=" + jti
|
||||
}
|
||||
|
||||
// IsServiceAccountToken returns true if the secret is a valid api token for the service account
|
||||
func IsServiceAccountToken(secret *v1.Secret, sa *v1.ServiceAccount) bool {
|
||||
if secret.Type != v1.SecretTypeServiceAccountToken {
|
||||
|
@ -17,12 +17,70 @@ limitations under the License.
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
func TestUserInfo(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
info ServiceAccountInfo
|
||||
expectedUserInfo *user.DefaultInfo
|
||||
}{
|
||||
"extracts pod name/uid": {
|
||||
info: ServiceAccountInfo{Name: "name", Namespace: "ns", PodName: "test", PodUID: "uid"},
|
||||
expectedUserInfo: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:ns:name",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:ns"},
|
||||
Extra: map[string][]string{
|
||||
"authentication.kubernetes.io/pod-name": {"test"},
|
||||
"authentication.kubernetes.io/pod-uid": {"uid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"extracts node name/uid": {
|
||||
info: ServiceAccountInfo{Name: "name", Namespace: "ns", NodeName: "test", NodeUID: "uid"},
|
||||
expectedUserInfo: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:ns:name",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:ns"},
|
||||
Extra: map[string][]string{
|
||||
"authentication.kubernetes.io/node-name": {"test"},
|
||||
"authentication.kubernetes.io/node-uid": {"uid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"extracts node name only": {
|
||||
info: ServiceAccountInfo{Name: "name", Namespace: "ns", NodeName: "test"},
|
||||
expectedUserInfo: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:ns:name",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:ns"},
|
||||
Extra: map[string][]string{
|
||||
"authentication.kubernetes.io/node-name": {"test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"does not extract node UID if name is not set": {
|
||||
info: ServiceAccountInfo{Name: "name", Namespace: "ns", NodeUID: "test"},
|
||||
expectedUserInfo: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:ns:name",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:ns"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
userInfo := test.info.UserInfo()
|
||||
if !reflect.DeepEqual(userInfo, test.expectedUserInfo) {
|
||||
t.Errorf("expected %#v but got %#v", test.expectedUserInfo, userInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeUsername(t *testing.T) {
|
||||
|
||||
testCases := map[string]struct {
|
||||
|
@ -19,6 +19,7 @@ package create
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -96,12 +97,18 @@ var (
|
||||
# Request a token bound to an instance of a Secret object with a specific UID
|
||||
kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
|
||||
`)
|
||||
)
|
||||
|
||||
boundObjectKindToAPIVersion = map[string]string{
|
||||
func boundObjectKindToAPIVersions() map[string]string {
|
||||
kinds := map[string]string{
|
||||
"Pod": "v1",
|
||||
"Secret": "v1",
|
||||
}
|
||||
)
|
||||
if os.Getenv("KUBECTL_NODE_BOUND_TOKENS") == "true" {
|
||||
kinds["Node"] = "v1"
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions {
|
||||
return &TokenOptions{
|
||||
@ -144,7 +151,7 @@ func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
|
||||
cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. If not set, the lifetime will be determined by the server automatically. The server may return a token with a longer or shorter lifetime.")
|
||||
|
||||
cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
|
||||
"Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", ")+". "+
|
||||
"Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).List(), ", ")+". "+
|
||||
"If set, --bound-object-name must be provided.")
|
||||
cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
|
||||
"The token will expire when the object is deleted. "+
|
||||
@ -221,8 +228,8 @@ func (o *TokenOptions) Validate() error {
|
||||
return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
|
||||
}
|
||||
} else {
|
||||
if _, ok := boundObjectKindToAPIVersion[o.BoundObjectKind]; !ok {
|
||||
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", "))
|
||||
if _, ok := boundObjectKindToAPIVersions()[o.BoundObjectKind]; !ok {
|
||||
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).List(), ", "))
|
||||
}
|
||||
if len(o.BoundObjectName) == 0 {
|
||||
return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
|
||||
@ -245,7 +252,7 @@ func (o *TokenOptions) Run() error {
|
||||
if len(o.BoundObjectKind) > 0 {
|
||||
request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
|
||||
Kind: o.BoundObjectKind,
|
||||
APIVersion: boundObjectKindToAPIVersion[o.BoundObjectKind],
|
||||
APIVersion: boundObjectKindToAPIVersions()[o.BoundObjectKind],
|
||||
Name: o.BoundObjectName,
|
||||
UID: types.UID(o.BoundObjectUID),
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@ -53,6 +54,8 @@ func TestCreateToken(t *testing.T) {
|
||||
audiences []string
|
||||
duration time.Duration
|
||||
|
||||
enableNodeBindingFeature bool
|
||||
|
||||
serverResponseToken string
|
||||
serverResponseError string
|
||||
|
||||
@ -117,6 +120,13 @@ status:
|
||||
boundObjectKind: "Foo",
|
||||
expectStderr: `error: supported --bound-object-kind values are Pod, Secret`,
|
||||
},
|
||||
{
|
||||
test: "bad bound object kind (node feature enabled)",
|
||||
name: "mysa",
|
||||
enableNodeBindingFeature: true,
|
||||
boundObjectKind: "Foo",
|
||||
expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`,
|
||||
},
|
||||
{
|
||||
test: "missing bound object name",
|
||||
name: "mysa",
|
||||
@ -158,7 +168,30 @@ status:
|
||||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "valid bound object (Node)",
|
||||
name: "mysa",
|
||||
|
||||
enableNodeBindingFeature: true,
|
||||
boundObjectKind: "Node",
|
||||
boundObjectName: "mynode",
|
||||
boundObjectUID: "myuid",
|
||||
|
||||
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Node",
|
||||
APIVersion: "v1",
|
||||
Name: "mynode",
|
||||
UID: "myuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "invalid audience",
|
||||
name: "mysa",
|
||||
@ -319,6 +352,10 @@ status:
|
||||
if test.duration != 0 {
|
||||
cmd.Flags().Set("duration", test.duration.String())
|
||||
}
|
||||
if test.enableNodeBindingFeature {
|
||||
os.Setenv("KUBECTL_NODE_BOUND_TOKENS", "true")
|
||||
defer os.Unsetenv("KUBECTL_NODE_BOUND_TOKENS")
|
||||
}
|
||||
cmd.Run(cmd, []string{test.name})
|
||||
|
||||
if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) {
|
||||
|
@ -41,15 +41,19 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/controlplane"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -79,6 +83,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Enable the node token improvements feature gates prior to starting the apiserver, as the node getter is
|
||||
// conditionally passed to the service account token generator based on feature enablement.
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, true)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenPodNodeInfo, true)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBindingValidation, true)()
|
||||
|
||||
// Start the server
|
||||
var serverAddress string
|
||||
kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
|
||||
@ -129,6 +139,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
}
|
||||
node = &v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-node",
|
||||
},
|
||||
}
|
||||
pod = &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
@ -139,6 +154,17 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
Containers: []v1.Container{{Name: "test-container", Image: "nginx"}},
|
||||
},
|
||||
}
|
||||
scheduledpod = &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Namespace: sa.Namespace,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
ServiceAccountName: sa.Name,
|
||||
NodeName: node.Name,
|
||||
Containers: []v1.Container{{Name: "test-container", Image: "nginx"}},
|
||||
},
|
||||
}
|
||||
otherpod = &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "other-test-pod",
|
||||
@ -155,7 +181,6 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
Namespace: sa.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
wrongUID = types.UID("wrong")
|
||||
noUID = types.UID("")
|
||||
)
|
||||
@ -220,6 +245,8 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("bound to service account and pod", func(t *testing.T) {
|
||||
// Disable embedding pod's node info
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenPodNodeInfo, false)()
|
||||
treq := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
@ -276,6 +303,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "secret")
|
||||
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
||||
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
||||
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "node")
|
||||
|
||||
info := doTokenReview(t, cs, treq, false)
|
||||
if len(info.Extra) != 2 {
|
||||
@ -291,6 +319,196 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
doTokenReview(t, cs, treq, true)
|
||||
})
|
||||
|
||||
testPodWithAssignedNode := func(node *v1.Node) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
treq := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Pod",
|
||||
APIVersion: "v1",
|
||||
Name: scheduledpod.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||
defer del()
|
||||
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token bound to nonexistant pod but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
pod, delPod := createDeletePod(t, cs, scheduledpod)
|
||||
defer delPod()
|
||||
|
||||
if node != nil {
|
||||
var delNode func()
|
||||
node, delNode = createDeleteNode(t, cs, node)
|
||||
defer delNode()
|
||||
}
|
||||
|
||||
// right uid
|
||||
treq.Spec.BoundObjectRef.UID = pod.UID
|
||||
warningHandler.clear()
|
||||
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
// wrong uid
|
||||
treq.Spec.BoundObjectRef.UID = wrongUID
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token bound to pod with wrong uid but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
// no uid
|
||||
treq.Spec.BoundObjectRef.UID = noUID
|
||||
warningHandler.clear()
|
||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
|
||||
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||
checkPayload(t, treq.Status.Token, `"test-pod"`, "kubernetes.io", "pod", "name")
|
||||
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "secret")
|
||||
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
||||
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
||||
|
||||
expectedExtraValues := map[string]authenticationv1.ExtraValue{
|
||||
"authentication.kubernetes.io/pod-name": {pod.ObjectMeta.Name},
|
||||
"authentication.kubernetes.io/pod-uid": {string(pod.ObjectMeta.UID)},
|
||||
}
|
||||
// If the NodeName is set at all, expect it to be included in the claims
|
||||
if pod.Spec.NodeName != "" {
|
||||
checkPayload(t, treq.Status.Token, fmt.Sprintf(`"%s"`, pod.Spec.NodeName), "kubernetes.io", "node", "name")
|
||||
expectedExtraValues["authentication.kubernetes.io/node-name"] = authenticationv1.ExtraValue{pod.Spec.NodeName}
|
||||
}
|
||||
// If the node is non-nil, we expect the UID to be set too
|
||||
if node != nil {
|
||||
checkPayload(t, treq.Status.Token, fmt.Sprintf(`"%s"`, node.UID), "kubernetes.io", "node", "uid")
|
||||
expectedExtraValues["authentication.kubernetes.io/node-uid"] = authenticationv1.ExtraValue{string(node.ObjectMeta.UID)}
|
||||
}
|
||||
|
||||
info := doTokenReview(t, cs, treq, false)
|
||||
if len(info.Extra) != len(expectedExtraValues) {
|
||||
t.Fatalf("expected Extra have length of %d but was length %d: %#v", len(expectedExtraValues), len(info.Extra), info.Extra)
|
||||
}
|
||||
if !reflect.DeepEqual(info.Extra, expectedExtraValues) {
|
||||
t.Fatalf("unexpected Extra:\ngot:\t%#v\nwant:\t%#v", info.Extra, expectedExtraValues)
|
||||
}
|
||||
|
||||
delPod()
|
||||
doTokenReview(t, cs, treq, true)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("bound to service account and a pod with an assigned nodeName that does not exist", testPodWithAssignedNode(nil))
|
||||
t.Run("bound to service account and a pod with an assigned nodeName", testPodWithAssignedNode(node))
|
||||
|
||||
t.Run("fails to bind to a Node if the feature gate is disabled", func(t *testing.T) {
|
||||
// Disable node binding
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, false)()
|
||||
|
||||
// Create ServiceAccount and Node objects
|
||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||
defer del()
|
||||
node, delNode := createDeleteNode(t, cs, node)
|
||||
defer delNode()
|
||||
|
||||
treq := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Node",
|
||||
APIVersion: "v1",
|
||||
Name: node.Name,
|
||||
UID: node.UID,
|
||||
},
|
||||
},
|
||||
}
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token with featuregate disabled but got: %#v", resp)
|
||||
} else if err.Error() != "cannot bind token to a Node object as the \"ServiceAccountTokenNodeBinding\" feature-gate is disabled" {
|
||||
t.Fatalf("expected error due to feature gate being disabled, but got: %s", err.Error())
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
})
|
||||
|
||||
t.Run("bound to service account and node", func(t *testing.T) {
|
||||
treq := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Node",
|
||||
APIVersion: "v1",
|
||||
Name: node.Name,
|
||||
UID: node.UID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||
defer del()
|
||||
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token bound to nonexistant node but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
node, delNode := createDeleteNode(t, cs, node)
|
||||
defer delNode()
|
||||
|
||||
// right uid
|
||||
treq.Spec.BoundObjectRef.UID = node.UID
|
||||
warningHandler.clear()
|
||||
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
// wrong uid
|
||||
treq.Spec.BoundObjectRef.UID = wrongUID
|
||||
warningHandler.clear()
|
||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
||||
t.Fatalf("expected err creating token bound to node with wrong uid but got: %#v", resp)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
// no uid
|
||||
treq.Spec.BoundObjectRef.UID = noUID
|
||||
warningHandler.clear()
|
||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(ctx, sa.Name, treq, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
warningHandler.assertEqual(t, nil)
|
||||
|
||||
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
|
||||
checkPayload(t, treq.Status.Token, `"test-node"`, "kubernetes.io", "node", "name")
|
||||
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
||||
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
||||
|
||||
doTokenReview(t, cs, treq, false)
|
||||
delNode()
|
||||
doTokenReview(t, cs, treq, true)
|
||||
})
|
||||
|
||||
t.Run("bound to service account and secret", func(t *testing.T) {
|
||||
treq := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
@ -410,7 +628,10 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||
coresa := core.ServiceAccount{
|
||||
ObjectMeta: sa.ObjectMeta,
|
||||
}
|
||||
_, pc := serviceaccount.Claims(coresa, nil, nil, 0, 0, nil)
|
||||
_, pc, err := serviceaccount.Claims(coresa, nil, nil, nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err calling Claims: %v", err)
|
||||
}
|
||||
tok, err := tokenGenerator.GenerateToken(sc, pc)
|
||||
if err != nil {
|
||||
t.Fatalf("err signing expired token: %v", err)
|
||||
@ -985,7 +1206,9 @@ func createDeletePod(t *testing.T, cs clientset.Interface, pod *v1.Pod) (*v1.Pod
|
||||
return
|
||||
}
|
||||
done = true
|
||||
if err := cs.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}); err != nil {
|
||||
if err := cs.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{
|
||||
GracePeriodSeconds: ptr.To(int64(0)),
|
||||
}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
@ -1010,6 +1233,25 @@ func createDeleteSecret(t *testing.T, cs clientset.Interface, sec *v1.Secret) (*
|
||||
}
|
||||
}
|
||||
|
||||
func createDeleteNode(t *testing.T, cs clientset.Interface, node *v1.Node) (*v1.Node, func()) {
|
||||
t.Helper()
|
||||
node, err := cs.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
done := false
|
||||
return node, func() {
|
||||
t.Helper()
|
||||
if done {
|
||||
return
|
||||
}
|
||||
done = true
|
||||
if err := cs.CoreV1().Nodes().Delete(context.TODO(), node.Name, metav1.DeleteOptions{}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type recordingWarningHandler struct {
|
||||
warnings []string
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user