Skip to content

Commit 8ca96f3

Browse files
authored
Merge pull request kubernetes#80724 from cceckman/provider-info-e2e
Provide OIDC discovery for service account token issuer
2 parents c099585 + 5a176ac commit 8ca96f3

File tree

15 files changed

+1090
-5
lines changed

15 files changed

+1090
-5
lines changed

cmd/kube-apiserver/app/server.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,22 @@ func CreateKubeAPIServerConfig(
381381
config.ExtraConfig.KubeletClientConfig.Lookup = config.GenericConfig.EgressSelector.Lookup
382382
}
383383

384+
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
385+
// Load the public keys.
386+
var pubKeys []interface{}
387+
for _, f := range s.Authentication.ServiceAccounts.KeyFiles {
388+
keys, err := keyutil.PublicKeysFromFile(f)
389+
if err != nil {
390+
return nil, nil, nil, nil, fmt.Errorf("failed to parse key file %q: %v", f, err)
391+
}
392+
pubKeys = append(pubKeys, keys...)
393+
}
394+
// Plumb the required metadata through ExtraConfig.
395+
config.ExtraConfig.ServiceAccountIssuerURL = s.Authentication.ServiceAccounts.Issuer
396+
config.ExtraConfig.ServiceAccountJWKSURI = s.Authentication.ServiceAccounts.JWKSURI
397+
config.ExtraConfig.ServiceAccountPublicKeys = pubKeys
398+
}
399+
384400
return config, insecureServingInfo, serviceResolver, pluginInitializers, nil
385401
}
386402

pkg/features/kube_features.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ const (
239239
// to the API server.
240240
BoundServiceAccountTokenVolume featuregate.Feature = "BoundServiceAccountTokenVolume"
241241

242+
// owner: @mtaufen
243+
// alpha: v1.18
244+
//
245+
// Enable OIDC discovery endpoints (issuer and JWKS URLs) for the service
246+
// account issuer in the API server.
247+
// Note these endpoints serve minimally-compliant discovery docs that are
248+
// intended to be used for service account token verification.
249+
ServiceAccountIssuerDiscovery featuregate.Feature = "ServiceAccountIssuerDiscovery"
250+
242251
// owner: @Random-Liu
243252
// beta: v1.11
244253
//
@@ -573,6 +582,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
573582
TokenRequest: {Default: true, PreRelease: featuregate.Beta},
574583
TokenRequestProjection: {Default: true, PreRelease: featuregate.Beta},
575584
BoundServiceAccountTokenVolume: {Default: false, PreRelease: featuregate.Alpha},
585+
ServiceAccountIssuerDiscovery: {Default: false, PreRelease: featuregate.Alpha},
576586
CRIContainerLogRotation: {Default: true, PreRelease: featuregate.Beta},
577587
CSIMigration: {Default: true, PreRelease: featuregate.Beta},
578588
CSIMigrationGCE: {Default: false, PreRelease: featuregate.Beta}, // Off by default (requires GCE PD CSI Driver)

pkg/kubeapiserver/options/authentication.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type ServiceAccountAuthenticationOptions struct {
8181
KeyFiles []string
8282
Lookup bool
8383
Issuer string
84+
JWKSURI string
8485
MaxExpiration time.Duration
8586
}
8687

@@ -188,6 +189,22 @@ func (s *BuiltInAuthenticationOptions) Validate() []error {
188189
}
189190
}
190191

192+
if s.ServiceAccounts != nil {
193+
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
194+
// Validate the JWKS URI when it is explicitly set.
195+
// When unset, it is later derived from ExternalHost.
196+
if s.ServiceAccounts.JWKSURI != "" {
197+
if u, err := url.Parse(s.ServiceAccounts.JWKSURI); err != nil {
198+
allErrors = append(allErrors, fmt.Errorf("service-account-jwks-uri must be a valid URL: %v", err))
199+
} else if u.Scheme != "https" {
200+
allErrors = append(allErrors, fmt.Errorf("service-account-jwks-uri requires https scheme, parsed as: %v", u.String()))
201+
}
202+
}
203+
} else if len(s.ServiceAccounts.JWKSURI) > 0 {
204+
allErrors = append(allErrors, fmt.Errorf("service-account-jwks-uri may only be set when the ServiceAccountIssuerDiscovery feature gate is enabled"))
205+
}
206+
}
207+
191208
return allErrors
192209
}
193210

@@ -281,7 +298,20 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
281298

282299
fs.StringVar(&s.ServiceAccounts.Issuer, "service-account-issuer", s.ServiceAccounts.Issuer, ""+
283300
"Identifier of the service account token issuer. The issuer will assert this identifier "+
284-
"in \"iss\" claim of issued tokens. This value is a string or URI.")
301+
"in \"iss\" claim of issued tokens. This value is a string or URI. If this option is not "+
302+
"a valid URI per the OpenID Discovery 1.0 spec, the ServiceAccountIssuerDiscovery feature "+
303+
"will remain disabled, even if the feature gate is set to true. It is highly recommended "+
304+
"that this value comply with the OpenID spec: https://openid.net/specs/openid-connect-discovery-1_0.html. "+
305+
"In practice, this means that service-account-issuer must be an https URL. It is also highly "+
306+
"recommended that this URL be capable of serving OpenID discovery documents at "+
307+
"`{service-account-issuer}/.well-known/openid-configuration`.")
308+
309+
fs.StringVar(&s.ServiceAccounts.JWKSURI, "service-account-jwks-uri", s.ServiceAccounts.JWKSURI, ""+
310+
"Overrides the URI for the JSON Web Key Set in the discovery doc served at "+
311+
"/.well-known/openid-configuration. This flag is useful if the discovery doc"+
312+
"and key set are served to relying parties from a URL other than the "+
313+
"API server's external (as auto-detected or overridden with external-hostname). "+
314+
"Only valid if the ServiceAccountIssuerDiscovery feature gate is enabled.")
285315

286316
// Deprecated in 1.13
287317
fs.StringSliceVar(&s.APIAudiences, "service-account-api-audiences", s.APIAudiences, ""+

pkg/master/master.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ type ExtraConfig struct {
191191
ServiceAccountIssuer serviceaccount.TokenGenerator
192192
ServiceAccountMaxExpiration time.Duration
193193

194+
// ServiceAccountIssuerDiscovery
195+
ServiceAccountIssuerURL string
196+
ServiceAccountJWKSURI string
197+
ServiceAccountPublicKeys []interface{}
198+
194199
VersionedInformers informers.SharedInformerFactory
195200
}
196201

@@ -342,6 +347,39 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
342347
routes.Logs{}.Install(s.Handler.GoRestfulContainer)
343348
}
344349

350+
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
351+
// Metadata and keys are expected to only change across restarts at present,
352+
// so we just marshal immediately and serve the cached JSON bytes.
353+
md, err := serviceaccount.NewOpenIDMetadata(
354+
c.ExtraConfig.ServiceAccountIssuerURL,
355+
c.ExtraConfig.ServiceAccountJWKSURI,
356+
c.GenericConfig.ExternalAddress,
357+
c.ExtraConfig.ServiceAccountPublicKeys,
358+
)
359+
if err != nil {
360+
// If there was an error, skip installing the endpoints and log the
361+
// error, but continue on. We don't return the error because the
362+
// metadata responses require additional, backwards incompatible
363+
// validation of command-line options.
364+
msg := fmt.Sprintf("Could not construct pre-rendered responses for"+
365+
" ServiceAccountIssuerDiscovery endpoints. Endpoints will not be"+
366+
" enabled. Error: %v", err)
367+
if c.ExtraConfig.ServiceAccountIssuerURL != "" {
368+
// The user likely expects this feature to be enabled if issuer URL is
369+
// set and the feature gate is enabled. In the future, if there is no
370+
// longer a feature gate and issuer URL is not set, the user may not
371+
// expect this feature to be enabled. We log the former case as an Error
372+
// and the latter case as an Info.
373+
klog.Error(msg)
374+
} else {
375+
klog.Info(msg)
376+
}
377+
} else {
378+
routes.NewOpenIDMetadataServer(md.ConfigJSON, md.PublicKeysetJSON).
379+
Install(s.Handler.GoRestfulContainer)
380+
}
381+
}
382+
345383
m := &Master{
346384
GenericAPIServer: s,
347385
ClusterAuthenticationInfo: c.ExtraConfig.ClusterAuthenticationInfo,

pkg/routes/BUILD

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ go_library(
1010
srcs = [
1111
"doc.go",
1212
"logs.go",
13+
"openidmetadata.go",
1314
],
1415
importpath = "k8s.io/kubernetes/pkg/routes",
15-
deps = ["//vendor/github.com/emicklei/go-restful:go_default_library"],
16+
deps = [
17+
"//pkg/serviceaccount:go_default_library",
18+
"//vendor/github.com/emicklei/go-restful:go_default_library",
19+
"//vendor/k8s.io/klog:go_default_library",
20+
],
1621
)
1722

1823
filegroup(

pkg/routes/openidmetadata.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package routes
18+
19+
import (
20+
"net/http"
21+
22+
restful "github.com/emicklei/go-restful"
23+
24+
"k8s.io/klog"
25+
"k8s.io/kubernetes/pkg/serviceaccount"
26+
)
27+
28+
// This code is in package routes because many controllers import
29+
// pkg/serviceaccount, but are not allowed by import-boss to depend on
30+
// go-restful. All logic that deals with keys is kept in pkg/serviceaccount,
31+
// and only the rendered JSON is passed into this server.
32+
33+
const (
34+
// cacheControl is the value of the Cache-Control header. Overrides the
35+
// global `private, no-cache` setting.
36+
headerCacheControl = "Cache-Control"
37+
cacheControl = "public, max-age=3600" // 1 hour
38+
39+
// mimeJWKS is the content type of the keyset response
40+
mimeJWKS = "application/jwk-set+json"
41+
)
42+
43+
// OpenIDMetadataServer is an HTTP server for metadata of the KSA token issuer.
44+
type OpenIDMetadataServer struct {
45+
configJSON []byte
46+
keysetJSON []byte
47+
}
48+
49+
// NewOpenIDMetadataServer creates a new OpenIDMetadataServer.
50+
// The issuer is the OIDC issuer; keys are the keys that may be used to sign
51+
// KSA tokens.
52+
func NewOpenIDMetadataServer(configJSON, keysetJSON []byte) *OpenIDMetadataServer {
53+
return &OpenIDMetadataServer{
54+
configJSON: configJSON,
55+
keysetJSON: keysetJSON,
56+
}
57+
}
58+
59+
// Install adds this server to the request router c.
60+
func (s *OpenIDMetadataServer) Install(c *restful.Container) {
61+
// Configuration WebService
62+
// Container.Add "will detect duplicate root paths and exit in that case",
63+
// so we need a root for /.well-known/openid-configuration to avoid conflicts.
64+
cfg := new(restful.WebService).
65+
Produces(restful.MIME_JSON)
66+
67+
cfg.Path(serviceaccount.OpenIDConfigPath).Route(
68+
cfg.GET("").
69+
To(fromStandard(s.serveConfiguration)).
70+
Doc("get service account issuer OpenID configuration, also known as the 'OIDC discovery doc'").
71+
Operation("getServiceAccountIssuerOpenIDConfiguration").
72+
// Just include the OK, doesn't look like we include Internal Error in our openapi-spec.
73+
Returns(http.StatusOK, "OK", ""))
74+
c.Add(cfg)
75+
76+
// JWKS WebService
77+
jwks := new(restful.WebService).
78+
Produces(mimeJWKS)
79+
80+
jwks.Path(serviceaccount.JWKSPath).Route(
81+
jwks.GET("").
82+
To(fromStandard(s.serveKeys)).
83+
Doc("get service account issuer OpenID JSON Web Key Set (contains public token verification keys)").
84+
Operation("getServiceAccountIssuerOpenIDKeyset").
85+
// Just include the OK, doesn't look like we include Internal Error in our openapi-spec.
86+
Returns(http.StatusOK, "OK", ""))
87+
c.Add(jwks)
88+
}
89+
90+
// fromStandard provides compatibility between the standard (net/http) handler signature and the restful signature.
91+
func fromStandard(h http.HandlerFunc) restful.RouteFunction {
92+
return func(req *restful.Request, resp *restful.Response) {
93+
h(resp, req.Request)
94+
}
95+
}
96+
97+
func (s *OpenIDMetadataServer) serveConfiguration(w http.ResponseWriter, req *http.Request) {
98+
w.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
99+
w.Header().Set(headerCacheControl, cacheControl)
100+
if _, err := w.Write(s.configJSON); err != nil {
101+
klog.Errorf("failed to write service account issuer metadata response: %v", err)
102+
return
103+
}
104+
}
105+
106+
func (s *OpenIDMetadataServer) serveKeys(w http.ResponseWriter, req *http.Request) {
107+
// Per RFC7517 : https://tools.ietf.org/html/rfc7517#section-8.5.1
108+
w.Header().Set(restful.HEADER_ContentType, mimeJWKS)
109+
w.Header().Set(headerCacheControl, cacheControl)
110+
if _, err := w.Write(s.keysetJSON); err != nil {
111+
klog.Errorf("failed to write service account issuer JWKS response: %v", err)
112+
return
113+
}
114+
}

pkg/serviceaccount/BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ go_library(
1212
"claims.go",
1313
"jwt.go",
1414
"legacy.go",
15+
"openidmetadata.go",
1516
"util.go",
1617
],
1718
importpath = "k8s.io/kubernetes/pkg/serviceaccount",
1819
deps = [
1920
"//pkg/apis/core:go_default_library",
2021
"//staging/src/k8s.io/api/core/v1:go_default_library",
2122
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
23+
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
2224
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
2325
"//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library",
2426
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
@@ -46,12 +48,14 @@ go_test(
4648
srcs = [
4749
"claims_test.go",
4850
"jwt_test.go",
51+
"openidmetadata_test.go",
4952
"util_test.go",
5053
],
5154
embed = [":go_default_library"],
5255
deps = [
5356
"//pkg/apis/core:go_default_library",
5457
"//pkg/controller/serviceaccount:go_default_library",
58+
"//pkg/routes:go_default_library",
5559
"//staging/src/k8s.io/api/core/v1:go_default_library",
5660
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
5761
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
@@ -60,6 +64,8 @@ go_test(
6064
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
6165
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
6266
"//staging/src/k8s.io/client-go/util/keyutil:go_default_library",
67+
"//vendor/github.com/emicklei/go-restful:go_default_library",
68+
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
6369
"//vendor/gopkg.in/square/go-jose.v2:go_default_library",
6470
"//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library",
6571
],

pkg/serviceaccount/jwt.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ func signerFromRSAPrivateKey(keyPair *rsa.PrivateKey) (jose.Signer, error) {
114114
return nil, fmt.Errorf("failed to derive keyID: %v", err)
115115
}
116116

117+
// IMPORTANT: If this function is updated to support additional key sizes,
118+
// algorithmForPublicKey in serviceaccount/openidmetadata.go must also be
119+
// updated to support the same key sizes. Today we only support RS256.
120+
117121
// Wrap the RSA keypair in a JOSE JWK with the designated key ID.
118122
privateJWK := &jose.JSONWebKey{
119123
Algorithm: string(jose.RS256),

pkg/serviceaccount/jwt_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package serviceaccount_test
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"reflect"
2223
"strings"
2324
"testing"
@@ -116,12 +117,18 @@ X2i8uIp/C/ASqiIGUeeKQtX0/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg==
116117
const ecdsaKeyID = "SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc"
117118

118119
func getPrivateKey(data string) interface{} {
119-
key, _ := keyutil.ParsePrivateKeyPEM([]byte(data))
120+
key, err := keyutil.ParsePrivateKeyPEM([]byte(data))
121+
if err != nil {
122+
panic(fmt.Errorf("unexpected error parsing private key: %v", err))
123+
}
120124
return key
121125
}
122126

123127
func getPublicKey(data string) interface{} {
124-
keys, _ := keyutil.ParsePublicKeysPEM([]byte(data))
128+
keys, err := keyutil.ParsePublicKeysPEM([]byte(data))
129+
if err != nil {
130+
panic(fmt.Errorf("unexpected error parsing public key: %v", err))
131+
}
125132
return keys[0]
126133
}
127134

0 commit comments

Comments
 (0)