Skip to content

Commit ef1d28a

Browse files
authored
Merge pull request kubernetes#125177 from liggitt/dynamic-public-key
Move public key serviceaccount getter to interface, filter by key id
2 parents 991e7a8 + 3e03707 commit ef1d28a

File tree

10 files changed

+504
-144
lines changed

10 files changed

+504
-144
lines changed

pkg/controlplane/apiserver/config.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ type Extra struct {
9494
ExtendExpiration bool
9595

9696
// ServiceAccountIssuerDiscovery
97-
ServiceAccountIssuerURL string
98-
ServiceAccountJWKSURI string
99-
ServiceAccountPublicKeys []interface{}
97+
ServiceAccountIssuerURL string
98+
ServiceAccountJWKSURI string
99+
ServiceAccountPublicKeysGetter serviceaccount.PublicKeysGetter
100100

101101
SystemNamespaces []string
102102

@@ -368,18 +368,24 @@ func CreateConfig(
368368
return nil, nil, fmt.Errorf("failed to apply admission: %w", err)
369369
}
370370

371-
// Load and set the public keys.
372-
var pubKeys []interface{}
373-
for _, f := range opts.Authentication.ServiceAccounts.KeyFiles {
374-
keys, err := keyutil.PublicKeysFromFile(f)
371+
if len(opts.Authentication.ServiceAccounts.KeyFiles) > 0 {
372+
// Load and set the public keys.
373+
var pubKeys []interface{}
374+
for _, f := range opts.Authentication.ServiceAccounts.KeyFiles {
375+
keys, err := keyutil.PublicKeysFromFile(f)
376+
if err != nil {
377+
return nil, nil, fmt.Errorf("failed to parse key file %q: %w", f, err)
378+
}
379+
pubKeys = append(pubKeys, keys...)
380+
}
381+
keysGetter, err := serviceaccount.StaticPublicKeysGetter(pubKeys)
375382
if err != nil {
376-
return nil, nil, fmt.Errorf("failed to parse key file %q: %w", f, err)
383+
return nil, nil, fmt.Errorf("failed to set up public service account keys: %w", err)
377384
}
378-
pubKeys = append(pubKeys, keys...)
385+
config.ServiceAccountPublicKeysGetter = keysGetter
379386
}
380387
config.ServiceAccountIssuerURL = opts.Authentication.ServiceAccounts.Issuers[0]
381388
config.ServiceAccountJWKSURI = opts.Authentication.ServiceAccounts.JWKSURI
382-
config.ServiceAccountPublicKeys = pubKeys
383389

384390
return config, genericInitializers, nil
385391
}

pkg/controlplane/apiserver/server.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,11 @@ func (c completedConfig) New(name string, delegationTarget genericapiserver.Dele
9393
routes.Logs{}.Install(generic.Handler.GoRestfulContainer)
9494
}
9595

96-
// Metadata and keys are expected to only change across restarts at present,
97-
// so we just marshal immediately and serve the cached JSON bytes.
98-
md, err := serviceaccount.NewOpenIDMetadata(
96+
md, err := serviceaccount.NewOpenIDMetadataProvider(
9997
c.ServiceAccountIssuerURL,
10098
c.ServiceAccountJWKSURI,
10199
c.Generic.ExternalAddress,
102-
c.ServiceAccountPublicKeys,
100+
c.ServiceAccountPublicKeysGetter,
103101
)
104102
if err != nil {
105103
// If there was an error, skip installing the endpoints and log the
@@ -120,8 +118,7 @@ func (c completedConfig) New(name string, delegationTarget genericapiserver.Dele
120118
klog.Info(msg)
121119
}
122120
} else {
123-
routes.NewOpenIDMetadataServer(md.ConfigJSON, md.PublicKeysetJSON).
124-
Install(generic.Handler.GoRestfulContainer)
121+
routes.NewOpenIDMetadataServer(md).Install(generic.Handler.GoRestfulContainer)
125122
}
126123

127124
s := &Server{

pkg/kubeapiserver/authenticator/config.go

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ type Config struct {
6262
AuthenticationConfig *apiserver.AuthenticationConfiguration
6363
AuthenticationConfigData string
6464
OIDCSigningAlgs []string
65-
ServiceAccountKeyFiles []string
6665
ServiceAccountLookup bool
6766
ServiceAccountIssuers []string
6867
APIAudiences authenticator.Audiences
@@ -79,7 +78,9 @@ type Config struct {
7978

8079
RequestHeaderConfig *authenticatorfactory.RequestHeaderConfig
8180

82-
// TODO, this is the only non-serializable part of the entire config. Factor it out into a clientconfig
81+
// ServiceAccountPublicKeysGetter returns public keys for verifying service account tokens.
82+
ServiceAccountPublicKeysGetter serviceaccount.PublicKeysGetter
83+
// ServiceAccountTokenGetter fetches API objects used to verify bound objects in service account token claims.
8384
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
8485
SecretsWriter typedv1core.SecretsGetter
8586
BootstrapTokenAuthenticator authenticator.Token
@@ -127,15 +128,15 @@ func (config Config) New(serverLifecycle context.Context) (authenticator.Request
127128
}
128129
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
129130
}
130-
if len(config.ServiceAccountKeyFiles) > 0 {
131-
serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter, config.SecretsWriter)
131+
if config.ServiceAccountPublicKeysGetter != nil {
132+
serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountPublicKeysGetter, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter, config.SecretsWriter)
132133
if err != nil {
133134
return nil, nil, nil, nil, err
134135
}
135136
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
136137
}
137138
if len(config.ServiceAccountIssuers) > 0 {
138-
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
139+
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountPublicKeysGetter, config.APIAudiences, config.ServiceAccountTokenGetter)
139140
if err != nil {
140141
return nil, nil, nil, nil, err
141142
}
@@ -338,36 +339,25 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e
338339
}
339340

340341
// newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error
341-
func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) {
342-
allPublicKeys := []interface{}{}
343-
for _, keyfile := range keyfiles {
344-
publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
345-
if err != nil {
346-
return nil, err
347-
}
348-
allPublicKeys = append(allPublicKeys, publicKeys...)
342+
func newLegacyServiceAccountAuthenticator(publicKeysGetter serviceaccount.PublicKeysGetter, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) {
343+
if publicKeysGetter == nil {
344+
return nil, fmt.Errorf("no public key getter provided")
349345
}
350346
validator, err := serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter, secretsWriter)
351347
if err != nil {
352348
return nil, fmt.Errorf("while creating legacy validator, err: %w", err)
353349
}
354350

355-
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer}, allPublicKeys, apiAudiences, validator)
351+
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer}, publicKeysGetter, apiAudiences, validator)
356352
return tokenAuthenticator, nil
357353
}
358354

359355
// newServiceAccountAuthenticator returns an authenticator.Token or an error
360-
func newServiceAccountAuthenticator(issuers []string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
361-
allPublicKeys := []interface{}{}
362-
for _, keyfile := range keyfiles {
363-
publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
364-
if err != nil {
365-
return nil, err
366-
}
367-
allPublicKeys = append(allPublicKeys, publicKeys...)
356+
func newServiceAccountAuthenticator(issuers []string, publicKeysGetter serviceaccount.PublicKeysGetter, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
357+
if publicKeysGetter == nil {
358+
return nil, fmt.Errorf("no public key getter provided")
368359
}
369-
370-
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
360+
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, publicKeysGetter, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
371361
return tokenAuthenticator, nil
372362
}
373363

pkg/kubeapiserver/options/authentication.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ import (
4747
"k8s.io/client-go/informers"
4848
"k8s.io/client-go/kubernetes"
4949
v1listers "k8s.io/client-go/listers/core/v1"
50+
"k8s.io/client-go/util/keyutil"
5051
cliflag "k8s.io/component-base/cli/flag"
5152
"k8s.io/klog/v2"
5253
openapicommon "k8s.io/kube-openapi/pkg/common"
5354
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
5455
"k8s.io/kubernetes/pkg/features"
5556
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
5657
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
58+
"k8s.io/kubernetes/pkg/serviceaccount"
5759
"k8s.io/kubernetes/pkg/util/filesystem"
5860
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap"
5961
"k8s.io/utils/pointer"
@@ -559,7 +561,21 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
559561
if len(o.ServiceAccounts.Issuers) != 0 && len(o.APIAudiences) == 0 {
560562
ret.APIAudiences = authenticator.Audiences(o.ServiceAccounts.Issuers)
561563
}
562-
ret.ServiceAccountKeyFiles = o.ServiceAccounts.KeyFiles
564+
if len(o.ServiceAccounts.KeyFiles) > 0 {
565+
allPublicKeys := []interface{}{}
566+
for _, keyfile := range o.ServiceAccounts.KeyFiles {
567+
publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
568+
if err != nil {
569+
return kubeauthenticator.Config{}, err
570+
}
571+
allPublicKeys = append(allPublicKeys, publicKeys...)
572+
}
573+
keysGetter, err := serviceaccount.StaticPublicKeysGetter(allPublicKeys)
574+
if err != nil {
575+
return kubeauthenticator.Config{}, fmt.Errorf("failed to set up public service account keys: %w", err)
576+
}
577+
ret.ServiceAccountPublicKeysGetter = keysGetter
578+
}
563579
ret.ServiceAccountIssuers = o.ServiceAccounts.Issuers
564580
ret.ServiceAccountLookup = o.ServiceAccounts.Lookup
565581
}

pkg/routes/openidmetadata.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package routes
1818

1919
import (
20+
"fmt"
2021
"net/http"
2122

2223
restful "github.com/emicklei/go-restful/v3"
@@ -34,26 +35,23 @@ const (
3435
// cacheControl is the value of the Cache-Control header. Overrides the
3536
// global `private, no-cache` setting.
3637
headerCacheControl = "Cache-Control"
37-
cacheControl = "public, max-age=3600" // 1 hour
38+
39+
cacheControlTemplate = "public, max-age=%d"
3840

3941
// mimeJWKS is the content type of the keyset response
4042
mimeJWKS = "application/jwk-set+json"
4143
)
4244

4345
// OpenIDMetadataServer is an HTTP server for metadata of the KSA token issuer.
4446
type OpenIDMetadataServer struct {
45-
configJSON []byte
46-
keysetJSON []byte
47+
provider serviceaccount.OpenIDMetadataProvider
4748
}
4849

4950
// NewOpenIDMetadataServer creates a new OpenIDMetadataServer.
5051
// The issuer is the OIDC issuer; keys are the keys that may be used to sign
5152
// KSA tokens.
52-
func NewOpenIDMetadataServer(configJSON, keysetJSON []byte) *OpenIDMetadataServer {
53-
return &OpenIDMetadataServer{
54-
configJSON: configJSON,
55-
keysetJSON: keysetJSON,
56-
}
53+
func NewOpenIDMetadataServer(provider serviceaccount.OpenIDMetadataProvider) *OpenIDMetadataServer {
54+
return &OpenIDMetadataServer{provider: provider}
5755
}
5856

5957
// Install adds this server to the request router c.
@@ -95,19 +93,21 @@ func fromStandard(h http.HandlerFunc) restful.RouteFunction {
9593
}
9694

9795
func (s *OpenIDMetadataServer) serveConfiguration(w http.ResponseWriter, req *http.Request) {
96+
configJSON, maxAge := s.provider.GetConfigJSON()
9897
w.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
99-
w.Header().Set(headerCacheControl, cacheControl)
100-
if _, err := w.Write(s.configJSON); err != nil {
98+
w.Header().Set(headerCacheControl, fmt.Sprintf(cacheControlTemplate, maxAge))
99+
if _, err := w.Write(configJSON); err != nil {
101100
klog.Errorf("failed to write service account issuer metadata response: %v", err)
102101
return
103102
}
104103
}
105104

106105
func (s *OpenIDMetadataServer) serveKeys(w http.ResponseWriter, req *http.Request) {
106+
keysetJSON, maxAge := s.provider.GetKeysetJSON()
107107
// Per RFC7517 : https://tools.ietf.org/html/rfc7517#section-8.5.1
108108
w.Header().Set(restful.HEADER_ContentType, mimeJWKS)
109-
w.Header().Set(headerCacheControl, cacheControl)
110-
if _, err := w.Write(s.keysetJSON); err != nil {
109+
w.Header().Set(headerCacheControl, fmt.Sprintf(cacheControlTemplate, maxAge))
110+
if _, err := w.Write(keysetJSON); err != nil {
111111
klog.Errorf("failed to write service account issuer JWKS response: %v", err)
112112
return
113113
}

pkg/serviceaccount/jwt.go

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,22 +225,97 @@ func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims inte
225225
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
226226
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
227227
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter
228-
func JWTTokenAuthenticator[PrivateClaims any](issuers []string, keys []interface{}, implicitAuds authenticator.Audiences, validator Validator[PrivateClaims]) authenticator.Token {
228+
func JWTTokenAuthenticator[PrivateClaims any](issuers []string, publicKeysGetter PublicKeysGetter, implicitAuds authenticator.Audiences, validator Validator[PrivateClaims]) authenticator.Token {
229229
issuersMap := make(map[string]bool)
230230
for _, issuer := range issuers {
231231
issuersMap[issuer] = true
232232
}
233233
return &jwtTokenAuthenticator[PrivateClaims]{
234234
issuers: issuersMap,
235-
keys: keys,
235+
keysGetter: publicKeysGetter,
236236
implicitAuds: implicitAuds,
237237
validator: validator,
238238
}
239239
}
240240

241+
// Listener is an interface to use to notify interested parties of a change.
242+
type Listener interface {
243+
// Enqueue should be called when an input may have changed
244+
Enqueue()
245+
}
246+
247+
// PublicKeysGetter returns public keys for a given key id.
248+
type PublicKeysGetter interface {
249+
// AddListener is adds a listener to be notified of potential input changes.
250+
// This is a noop on static providers.
251+
AddListener(listener Listener)
252+
253+
// GetCacheAgeMaxSeconds returns the seconds a call to GetPublicKeys() can be cached for.
254+
// If the results of GetPublicKeys() can be dynamic, this means a new key must be included in the results
255+
// for at least this long before it is used to sign new tokens.
256+
GetCacheAgeMaxSeconds() int
257+
258+
// GetPublicKeys returns public keys to use for verifying a token with the given key id.
259+
// keyIDHint may be empty if the token did not have a kid header, or if all public keys are desired.
260+
GetPublicKeys(keyIDHint string) []PublicKey
261+
}
262+
263+
type PublicKey struct {
264+
KeyID string
265+
PublicKey interface{}
266+
}
267+
268+
type staticPublicKeysGetter struct {
269+
allPublicKeys []PublicKey
270+
publicKeysByID map[string][]PublicKey
271+
}
272+
273+
// StaticPublicKeysGetter constructs an implementation of PublicKeysGetter
274+
// which returns all public keys when key id is unspecified, and returns
275+
// the public keys matching the keyIDFromPublicKey-derived key id when
276+
// a key id is specified.
277+
func StaticPublicKeysGetter(keys []interface{}) (PublicKeysGetter, error) {
278+
allPublicKeys := []PublicKey{}
279+
publicKeysByID := map[string][]PublicKey{}
280+
for _, key := range keys {
281+
if privateKey, isPrivateKey := key.(publicKeyGetter); isPrivateKey {
282+
// This is a private key. Extract its public key.
283+
key = privateKey.Public()
284+
}
285+
286+
keyID, err := keyIDFromPublicKey(key)
287+
if err != nil {
288+
return nil, err
289+
}
290+
pk := PublicKey{PublicKey: key, KeyID: keyID}
291+
publicKeysByID[keyID] = append(publicKeysByID[keyID], pk)
292+
allPublicKeys = append(allPublicKeys, pk)
293+
}
294+
return &staticPublicKeysGetter{
295+
allPublicKeys: allPublicKeys,
296+
publicKeysByID: publicKeysByID,
297+
}, nil
298+
}
299+
300+
func (s staticPublicKeysGetter) AddListener(listener Listener) {
301+
// no-op, static key content never changes
302+
}
303+
304+
func (s staticPublicKeysGetter) GetCacheAgeMaxSeconds() int {
305+
// hard-coded to match cache max-age set in OIDC discovery
306+
return 3600
307+
}
308+
309+
func (s staticPublicKeysGetter) GetPublicKeys(keyID string) []PublicKey {
310+
if len(keyID) == 0 {
311+
return s.allPublicKeys
312+
}
313+
return s.publicKeysByID[keyID]
314+
}
315+
241316
type jwtTokenAuthenticator[PrivateClaims any] struct {
242317
issuers map[string]bool
243-
keys []interface{}
318+
keysGetter PublicKeysGetter
244319
validator Validator[PrivateClaims]
245320
implicitAuds authenticator.Audiences
246321
}
@@ -269,13 +344,25 @@ func (j *jwtTokenAuthenticator[PrivateClaims]) AuthenticateToken(ctx context.Con
269344
public := &jwt.Claims{}
270345
private := new(PrivateClaims)
271346

272-
// TODO: Pick the key that has the same key ID as `tok`, if one exists.
347+
// Pick the key that has the same key ID as `tok`, if one exists.
348+
var kid string
349+
for _, header := range tok.Headers {
350+
if header.KeyID != "" {
351+
kid = header.KeyID
352+
break
353+
}
354+
}
355+
273356
var (
274357
found bool
275358
errlist []error
276359
)
277-
for _, key := range j.keys {
278-
if err := tok.Claims(key, public, private); err != nil {
360+
keys := j.keysGetter.GetPublicKeys(kid)
361+
if len(keys) == 0 {
362+
return nil, false, fmt.Errorf("invalid signature, no keys found")
363+
}
364+
for _, key := range keys {
365+
if err := tok.Claims(key.PublicKey, public, private); err != nil {
279366
errlist = append(errlist, err)
280367
continue
281368
}

0 commit comments

Comments
 (0)