Skip to content

Commit

Permalink
Merge pull request kubernetes#107880 from liggitt/kubectl-auth-token
Browse files Browse the repository at this point in the history
Add command to request a bound service account token
  • Loading branch information
k8s-ci-robot authored Feb 9, 2022
2 parents 6ab748e + fca9b1d commit e74c42a
Show file tree
Hide file tree
Showing 8 changed files with 758 additions and 21 deletions.
76 changes: 56 additions & 20 deletions pkg/registry/core/serviceaccount/storage/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/authenticator"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/warning"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
api "k8s.io/kubernetes/pkg/apis/core"
Expand Down Expand Up @@ -62,30 +64,67 @@ var gvk = schema.GroupVersionKind{
}

func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
req := obj.(*authenticationapi.TokenRequest)

out := obj.(*authenticationapi.TokenRequest)
// Get the namespace from the context (populated from the URL).
namespace, ok := genericapirequest.NamespaceFrom(ctx)
if !ok {
return nil, errors.NewBadRequest("namespace is required")
}

if errs := authenticationvalidation.ValidateTokenRequest(out); len(errs) != 0 {
return nil, errors.NewInvalid(gvk.GroupKind(), "", errs)
// require name/namespace in the body to match URL if specified
if len(req.Name) > 0 && req.Name != name {
errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("name"), req.Name, "must match the service account name if specified")}
return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
}
if len(req.Namespace) > 0 && req.Namespace != namespace {
errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("namespace"), req.Namespace, "must match the service account namespace if specified")}
return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
}

// Lookup service account
svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, err
}
svcacct := svcacctObj.(*api.ServiceAccount)

// Default unset spec audiences to API server audiences based on server config
if len(req.Spec.Audiences) == 0 {
req.Spec.Audiences = r.auds
}
// Populate metadata fields if not set
if len(req.Name) == 0 {
req.Name = svcacct.Name
}
if len(req.Namespace) == 0 {
req.Namespace = svcacct.Namespace
}

// Save current time before building the token, to make sure the expiration
// returned in TokenRequestStatus would be <= the exp field in token.
nowTime := time.Now()
req.CreationTimestamp = metav1.NewTime(nowTime)

// Clear status
req.Status = authenticationapi.TokenRequestStatus{}

// call static validation, then validating admission
if errs := authenticationvalidation.ValidateTokenRequest(req); len(errs) != 0 {
return nil, errors.NewInvalid(gvk.GroupKind(), "", errs)
}
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}

var (
pod *api.Pod
secret *api.Secret
)

if ref := out.Spec.BoundObjectRef; ref != nil {
if ref := req.Spec.BoundObjectRef; ref != nil {
var uid types.UID

gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
Expand Down Expand Up @@ -116,35 +155,32 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
return nil, errors.NewConflict(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, ref.Name, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record. The object might have been deleted and then recreated", ref.UID))
}
}
if len(out.Spec.Audiences) == 0 {
out.Spec.Audiences = r.auds
}

if r.maxExpirationSeconds > 0 && out.Spec.ExpirationSeconds > r.maxExpirationSeconds {
if r.maxExpirationSeconds > 0 && req.Spec.ExpirationSeconds > r.maxExpirationSeconds {
//only positive value is valid
out.Spec.ExpirationSeconds = r.maxExpirationSeconds
warning.AddWarning(ctx, "", fmt.Sprintf("requested expiration of %d seconds shortened to %d seconds", req.Spec.ExpirationSeconds, r.maxExpirationSeconds))
req.Spec.ExpirationSeconds = r.maxExpirationSeconds
}

// Tweak expiration for safe transition of projected service account token.
// Warn (instead of fail) after requested expiration time.
// Fail after hard-coded extended expiration time.
// Only perform the extension when token is pod-bound.
var warnAfter int64
exp := out.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && out.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(out.Spec.Audiences) {
exp := req.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
warnAfter = exp
exp = token.ExpirationExtensionSeconds
}

// Save current time before building the token, to make sure the expiration
// returned in TokenRequestStatus would be earlier than exp field in token.
nowTime := time.Now()
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, out.Spec.Audiences)
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, req.Spec.Audiences)
tokdata, err := r.issuer.GenerateToken(sc, pc)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %v", err)
}

// populate status
out := req.DeepCopy()
out.Status = authenticationapi.TokenRequestStatus{
Token: tokdata,
ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)},
Expand Down
1 change: 1 addition & 0 deletions plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ func ClusterRoles() []rbacv1.ClusterRole {
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("pods", "pods/attach", "pods/proxy", "pods/exec", "pods/portforward").RuleOrDie(),
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts",
"services", "services/proxy", "persistentvolumeclaims", "configmaps", "secrets", "events").RuleOrDie(),
rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie(),

rbacv1helpers.NewRule(Write...).Groups(appsGroup).Resources(
"statefulsets", "statefulsets/scale",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ items:
- deletecollection
- patch
- update
- apiGroups:
- ""
resources:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- apps
resources:
Expand Down
1 change: 1 addition & 0 deletions staging/src/k8s.io/kubectl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
k8s.io/metrics v0.0.0
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2
sigs.k8s.io/kustomize/kustomize/v4 v4.4.1
sigs.k8s.io/kustomize/kyaml v0.13.0
sigs.k8s.io/yaml v1.2.0
Expand Down
1 change: 1 addition & 0 deletions staging/src/k8s.io/kubectl/pkg/cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cob
cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
cmd.AddCommand(NewCmdCreateToken(f, ioStreams))
return cmd
}

Expand Down
Loading

0 comments on commit e74c42a

Please sign in to comment.