1
16
17 package serviceaccount
18
19 import (
20 "context"
21 "crypto/subtle"
22 "encoding/json"
23 "errors"
24 "fmt"
25 "time"
26
27 "gopkg.in/square/go-jose.v2/jwt"
28 v1 "k8s.io/api/core/v1"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/types"
31 "k8s.io/apiserver/pkg/audit"
32 apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
33 "k8s.io/apiserver/pkg/warning"
34 applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
35 typedv1core "k8s.io/client-go/kubernetes/typed/core/v1"
36 "k8s.io/klog/v2"
37 )
38
39 const InvalidSinceLabelKey = "kubernetes.io/legacy-token-invalid-since"
40
41 func LegacyClaims(serviceAccount v1.ServiceAccount, secret v1.Secret) (*jwt.Claims, interface{}) {
42 return &jwt.Claims{
43 Subject: apiserverserviceaccount.MakeUsername(serviceAccount.Namespace, serviceAccount.Name),
44 }, &legacyPrivateClaims{
45 Namespace: serviceAccount.Namespace,
46 ServiceAccountName: serviceAccount.Name,
47 ServiceAccountUID: string(serviceAccount.UID),
48 SecretName: secret.Name,
49 }
50 }
51
52 const (
53 LegacyIssuer = "kubernetes/serviceaccount"
54 LastUsedLabelKey = "kubernetes.io/legacy-token-last-used"
55 )
56
57 type legacyPrivateClaims struct {
58 ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
59 ServiceAccountUID string `json:"kubernetes.io/serviceaccount/service-account.uid"`
60 SecretName string `json:"kubernetes.io/serviceaccount/secret.name"`
61 Namespace string `json:"kubernetes.io/serviceaccount/namespace"`
62 }
63
64 func NewLegacyValidator(lookup bool, getter ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (Validator, error) {
65 if lookup && getter == nil {
66 return nil, errors.New("ServiceAccountTokenGetter must be provided")
67 }
68 if lookup && secretsWriter == nil {
69 return nil, errors.New("SecretsWriter must be provided")
70 }
71 return &legacyValidator{
72 lookup: lookup,
73 getter: getter,
74 secretsWriter: secretsWriter,
75 }, nil
76 }
77
78 type legacyValidator struct {
79 lookup bool
80 getter ServiceAccountTokenGetter
81 secretsWriter typedv1core.SecretsGetter
82 }
83
84 var _ = Validator(&legacyValidator{})
85
86 func (v *legacyValidator) Validate(ctx context.Context, tokenData string, public *jwt.Claims, privateObj interface{}) (*apiserverserviceaccount.ServiceAccountInfo, error) {
87 private, ok := privateObj.(*legacyPrivateClaims)
88 if !ok {
89 klog.Errorf("jwt validator expected private claim of type *legacyPrivateClaims but got: %T", privateObj)
90 return nil, errors.New("Token could not be validated.")
91 }
92
93
94 if len(public.Subject) == 0 {
95 return nil, errors.New("sub claim is missing")
96 }
97 namespace := private.Namespace
98 if len(namespace) == 0 {
99 return nil, errors.New("namespace claim is missing")
100 }
101 secretName := private.SecretName
102 if len(secretName) == 0 {
103 return nil, errors.New("secretName claim is missing")
104 }
105 serviceAccountName := private.ServiceAccountName
106 if len(serviceAccountName) == 0 {
107 return nil, errors.New("serviceAccountName claim is missing")
108 }
109 serviceAccountUID := private.ServiceAccountUID
110 if len(serviceAccountUID) == 0 {
111 return nil, errors.New("serviceAccountUID claim is missing")
112 }
113
114 subjectNamespace, subjectName, err := apiserverserviceaccount.SplitUsername(public.Subject)
115 if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName {
116 return nil, errors.New("sub claim is invalid")
117 }
118
119 if v.lookup {
120
121 secret, err := v.getter.GetSecret(namespace, secretName)
122 if err != nil {
123 klog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err)
124 return nil, errors.New("Token has been invalidated")
125 }
126 if secret.DeletionTimestamp != nil {
127 klog.V(4).Infof("Token is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
128 return nil, errors.New("Token has been invalidated")
129 }
130 if subtle.ConstantTimeCompare(secret.Data[v1.ServiceAccountTokenKey], []byte(tokenData)) == 0 {
131 klog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
132 return nil, errors.New("Token does not match server's copy")
133 }
134
135
136 serviceAccount, err := v.getter.GetServiceAccount(namespace, serviceAccountName)
137 if err != nil {
138 klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err)
139 return nil, err
140 }
141 if serviceAccount.DeletionTimestamp != nil {
142 klog.V(4).Infof("Service account has been deleted %s/%s", namespace, serviceAccountName)
143 return nil, fmt.Errorf("ServiceAccount %s/%s has been deleted", namespace, serviceAccountName)
144 }
145 if string(serviceAccount.UID) != serviceAccountUID {
146 klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID)
147 return nil, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
148 }
149
150
151 autoGenerated := false
152
153
154 if invalidSince := secret.Labels[InvalidSinceLabelKey]; invalidSince != "" {
155 audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-invalidated", secret.Name+"/"+secret.Namespace)
156 invalidatedAutoTokensTotal.WithContext(ctx).Inc()
157 v.patchSecretWithLastUsedDate(ctx, secret)
158 return nil, fmt.Errorf("the token in secret %s/%s for service account %s/%s has been marked invalid. Use tokens from the TokenRequest API or manually created secret-based tokens, or remove the '%s' label from the secret to temporarily allow use of this token", namespace, secretName, namespace, serviceAccountName, InvalidSinceLabelKey)
159 }
160
161
162 for _, ref := range serviceAccount.Secrets {
163 if ref.Name == secret.Name {
164 autoGenerated = true
165 warning.AddWarning(ctx, "", "Use tokens from the TokenRequest API or manually created secret-based tokens instead of auto-generated secret-based tokens.")
166 audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-autogenerated-secret", secret.Name)
167 autoGeneratedTokensTotal.WithContext(ctx).Inc()
168 break
169 }
170 }
171
172
173 if !autoGenerated {
174 audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-manual-secret", secret.Name)
175 manuallyCreatedTokensTotal.WithContext(ctx).Inc()
176 }
177
178 v.patchSecretWithLastUsedDate(ctx, secret)
179 }
180
181 return &apiserverserviceaccount.ServiceAccountInfo{
182 Namespace: private.Namespace,
183 Name: private.ServiceAccountName,
184 UID: private.ServiceAccountUID,
185 }, nil
186 }
187
188 func (v *legacyValidator) patchSecretWithLastUsedDate(ctx context.Context, secret *v1.Secret) {
189 now := time.Now().UTC()
190 today := now.Format("2006-01-02")
191 tomorrow := now.AddDate(0, 0, 1).Format("2006-01-02")
192 lastUsed := secret.Labels[LastUsedLabelKey]
193 if lastUsed != today && lastUsed != tomorrow {
194 patchContent, err := json.Marshal(applyv1.Secret(secret.Name, secret.Namespace).WithUID(secret.UID).WithLabels(map[string]string{LastUsedLabelKey: today}))
195 if err != nil {
196 klog.Errorf("Failed to marshal legacy service account token %s/%s tracking labels, err: %v", secret.Name, secret.Namespace, err)
197 } else {
198 if _, err := v.secretsWriter.Secrets(secret.Namespace).Patch(ctx, secret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{}); err != nil {
199 klog.Errorf("Failed to label legacy service account token %s/%s with last-used date, err: %v", secret.Name, secret.Namespace, err)
200 }
201 }
202 }
203 }
204
205 func (v *legacyValidator) NewPrivateClaims() interface{} {
206 return &legacyPrivateClaims{}
207 }
208
View as plain text