mirror of https://github.com/k3s-io/k3s
retrofit svcacct token authenticator to support audience validation
parent
bfb95290b9
commit
67bbf753cb
|
@ -64,7 +64,7 @@ type Config struct {
|
||||||
ServiceAccountKeyFiles []string
|
ServiceAccountKeyFiles []string
|
||||||
ServiceAccountLookup bool
|
ServiceAccountLookup bool
|
||||||
ServiceAccountIssuer string
|
ServiceAccountIssuer string
|
||||||
APIAudiences []string
|
APIAudiences authenticator.Audiences
|
||||||
WebhookTokenAuthnConfigFile string
|
WebhookTokenAuthnConfigFile string
|
||||||
WebhookTokenAuthnCacheTTL time.Duration
|
WebhookTokenAuthnCacheTTL time.Duration
|
||||||
|
|
||||||
|
@ -135,14 +135,14 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er
|
||||||
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
|
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
|
||||||
}
|
}
|
||||||
if len(config.ServiceAccountKeyFiles) > 0 {
|
if len(config.ServiceAccountKeyFiles) > 0 {
|
||||||
serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.ServiceAccountTokenGetter)
|
serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, serviceAccountAuth))
|
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
|
||||||
}
|
}
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) && config.ServiceAccountIssuer != "" {
|
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) && config.ServiceAccountIssuer != "" {
|
||||||
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.APIAudiences, config.ServiceAccountKeyFiles, config.ServiceAccountTokenGetter)
|
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error
|
// newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error
|
||||||
func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
|
func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
|
||||||
allPublicKeys := []interface{}{}
|
allPublicKeys := []interface{}{}
|
||||||
for _, keyfile := range keyfiles {
|
for _, keyfile := range keyfiles {
|
||||||
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
|
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
|
||||||
|
@ -286,12 +286,12 @@ func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, servic
|
||||||
allPublicKeys = append(allPublicKeys, publicKeys...)
|
allPublicKeys = append(allPublicKeys, publicKeys...)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, allPublicKeys, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter))
|
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, allPublicKeys, apiAudiences, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter))
|
||||||
return tokenAuthenticator, nil
|
return tokenAuthenticator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newServiceAccountAuthenticator returns an authenticator.Token or an error
|
// newServiceAccountAuthenticator returns an authenticator.Token or an error
|
||||||
func newServiceAccountAuthenticator(iss string, audiences []string, keyfiles []string, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
|
func newServiceAccountAuthenticator(iss string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
|
||||||
allPublicKeys := []interface{}{}
|
allPublicKeys := []interface{}{}
|
||||||
for _, keyfile := range keyfiles {
|
for _, keyfile := range keyfiles {
|
||||||
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
|
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
|
||||||
|
@ -301,7 +301,7 @@ func newServiceAccountAuthenticator(iss string, audiences []string, keyfiles []s
|
||||||
allPublicKeys = append(allPublicKeys, publicKeys...)
|
allPublicKeys = append(allPublicKeys, publicKeys...)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(iss, allPublicKeys, serviceaccount.NewValidator(audiences, serviceAccountGetter))
|
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(iss, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
|
||||||
return tokenAuthenticator, nil
|
return tokenAuthenticator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ go_test(
|
||||||
"//pkg/controller/serviceaccount:go_default_library",
|
"//pkg/controller/serviceaccount:go_default_library",
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
|
|
|
@ -80,15 +80,13 @@ func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirati
|
||||||
return sc, pc
|
return sc, pc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidator(audiences []string, getter ServiceAccountTokenGetter) Validator {
|
func NewValidator(getter ServiceAccountTokenGetter) Validator {
|
||||||
return &validator{
|
return &validator{
|
||||||
auds: audiences,
|
|
||||||
getter: getter,
|
getter: getter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type validator struct {
|
type validator struct {
|
||||||
auds []string
|
|
||||||
getter ServiceAccountTokenGetter
|
getter ServiceAccountTokenGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,19 +110,6 @@ func (v *validator) Validate(_ string, public *jwt.Claims, privateObj interface{
|
||||||
return nil, errors.New("Token could not be validated.")
|
return nil, errors.New("Token could not be validated.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var audValid bool
|
|
||||||
|
|
||||||
for _, aud := range v.auds {
|
|
||||||
audValid = public.Audience.Contains(aud)
|
|
||||||
if audValid {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !audValid {
|
|
||||||
return nil, errors.New("Token is invalid for this audience.")
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace := private.Kubernetes.Namespace
|
namespace := private.Kubernetes.Namespace
|
||||||
saref := private.Kubernetes.Svcacct
|
saref := private.Kubernetes.Svcacct
|
||||||
podref := private.Kubernetes.Pod
|
podref := private.Kubernetes.Pod
|
||||||
|
|
|
@ -111,18 +111,20 @@ func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims inte
|
||||||
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
||||||
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
|
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
|
||||||
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter
|
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter
|
||||||
func JWTTokenAuthenticator(iss string, keys []interface{}, validator Validator) authenticator.Token {
|
func JWTTokenAuthenticator(iss string, keys []interface{}, implicitAuds authenticator.Audiences, validator Validator) authenticator.Token {
|
||||||
return &jwtTokenAuthenticator{
|
return &jwtTokenAuthenticator{
|
||||||
iss: iss,
|
iss: iss,
|
||||||
keys: keys,
|
keys: keys,
|
||||||
validator: validator,
|
implicitAuds: implicitAuds,
|
||||||
|
validator: validator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type jwtTokenAuthenticator struct {
|
type jwtTokenAuthenticator struct {
|
||||||
iss string
|
iss string
|
||||||
keys []interface{}
|
keys []interface{}
|
||||||
validator Validator
|
validator Validator
|
||||||
|
implicitAuds authenticator.Audiences
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator is called by the JWT token authenticator to apply domain specific
|
// Validator is called by the JWT token authenticator to apply domain specific
|
||||||
|
@ -170,6 +172,23 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData
|
||||||
return nil, false, utilerrors.NewAggregate(errlist)
|
return nil, false, utilerrors.NewAggregate(errlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenAudiences := authenticator.Audiences(public.Audience)
|
||||||
|
if len(tokenAudiences) == 0 {
|
||||||
|
// only apiserver audiences are allowed for legacy tokens
|
||||||
|
tokenAudiences = j.implicitAuds
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedAudiences, ok := authenticator.AudiencesFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
// default to apiserver audiences
|
||||||
|
requestedAudiences = j.implicitAuds
|
||||||
|
}
|
||||||
|
|
||||||
|
auds := authenticator.Audiences(tokenAudiences).Intersect(requestedAudiences)
|
||||||
|
if len(auds) == 0 && len(j.implicitAuds) != 0 {
|
||||||
|
return nil, false, fmt.Errorf("token audiences %q is invalid for the target audiences %q", tokenAudiences, requestedAudiences)
|
||||||
|
}
|
||||||
|
|
||||||
// If we get here, we have a token with a recognized signature and
|
// If we get here, we have a token with a recognized signature and
|
||||||
// issuer string.
|
// issuer string.
|
||||||
sa, err := j.validator.Validate(tokenData, public, private)
|
sa, err := j.validator.Validate(tokenData, public, private)
|
||||||
|
@ -177,7 +196,10 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authenticator.Response{User: sa.UserInfo()}, true, nil
|
return &authenticator.Response{
|
||||||
|
User: sa.UserInfo(),
|
||||||
|
Audiences: auds,
|
||||||
|
}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasCorrectIssuer returns true if tokenData is a valid JWT in compact
|
// hasCorrectIssuer returns true if tokenData is a valid JWT in compact
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
certutil "k8s.io/client-go/util/cert"
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
@ -275,16 +276,18 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, tc := range testCases {
|
for k, tc := range testCases {
|
||||||
|
auds := authenticator.Audiences{"api"}
|
||||||
getter := serviceaccountcontroller.NewGetterFromClient(tc.Client)
|
getter := serviceaccountcontroller.NewGetterFromClient(tc.Client)
|
||||||
authenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, tc.Keys, serviceaccount.NewLegacyValidator(tc.Client != nil, getter))
|
authn := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, tc.Keys, auds, serviceaccount.NewLegacyValidator(tc.Client != nil, getter))
|
||||||
|
|
||||||
// An invalid, non-JWT token should always fail
|
// An invalid, non-JWT token should always fail
|
||||||
if _, ok, err := authenticator.AuthenticateToken(context.Background(), "invalid token"); err != nil || ok {
|
ctx := authenticator.WithAudiences(context.Background(), auds)
|
||||||
|
if _, ok, err := authn.AuthenticateToken(ctx, "invalid token"); err != nil || ok {
|
||||||
t.Errorf("%s: Expected err=nil, ok=false for non-JWT token", k)
|
t.Errorf("%s: Expected err=nil, ok=false for non-JWT token", k)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, ok, err := authenticator.AuthenticateToken(context.Background(), tc.Token)
|
resp, ok, err := authn.AuthenticateToken(ctx, tc.Token)
|
||||||
if (err != nil) != tc.ExpectedErr {
|
if (err != nil) != tc.ExpectedErr {
|
||||||
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
|
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
|
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
|
@ -63,7 +64,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||||
pk := sk.(*ecdsa.PrivateKey).PublicKey
|
pk := sk.(*ecdsa.PrivateKey).PublicKey
|
||||||
|
|
||||||
const iss = "https://foo.bar.example.com"
|
const iss = "https://foo.bar.example.com"
|
||||||
aud := []string{"api"}
|
aud := authenticator.Audiences{"api"}
|
||||||
|
|
||||||
maxExpirationSeconds := int64(60 * 60)
|
maxExpirationSeconds := int64(60 * 60)
|
||||||
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
|
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
|
||||||
|
@ -76,11 +77,13 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
||||||
// Start the server
|
// Start the server
|
||||||
masterConfig := framework.NewIntegrationTestMasterConfig()
|
masterConfig := framework.NewIntegrationTestMasterConfig()
|
||||||
masterConfig.GenericConfig.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
|
masterConfig.GenericConfig.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
|
||||||
|
masterConfig.GenericConfig.Authentication.APIAudiences = aud
|
||||||
masterConfig.GenericConfig.Authentication.Authenticator = bearertoken.New(
|
masterConfig.GenericConfig.Authentication.Authenticator = bearertoken.New(
|
||||||
serviceaccount.JWTTokenAuthenticator(
|
serviceaccount.JWTTokenAuthenticator(
|
||||||
iss,
|
iss,
|
||||||
[]interface{}{&pk},
|
[]interface{}{&pk},
|
||||||
serviceaccount.NewValidator(aud, serviceaccountgetter.NewGetterFromClient(gcs)),
|
aud,
|
||||||
|
serviceaccount.NewValidator(serviceaccountgetter.NewGetterFromClient(gcs)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
tokenGenerator, err := serviceaccount.JWTTokenGenerator(iss, sk)
|
tokenGenerator, err := serviceaccount.JWTTokenGenerator(iss, sk)
|
||||||
|
|
|
@ -374,7 +374,7 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie
|
||||||
})
|
})
|
||||||
serviceAccountKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
serviceAccountKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
serviceAccountTokenGetter := serviceaccountcontroller.NewGetterFromClient(rootClientset)
|
serviceAccountTokenGetter := serviceaccountcontroller.NewGetterFromClient(rootClientset)
|
||||||
serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, []interface{}{&serviceAccountKey.PublicKey}, serviceaccount.NewLegacyValidator(true, serviceAccountTokenGetter))
|
serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, []interface{}{&serviceAccountKey.PublicKey}, nil, serviceaccount.NewLegacyValidator(true, serviceAccountTokenGetter))
|
||||||
authenticator := union.New(
|
authenticator := union.New(
|
||||||
bearertoken.New(rootTokenAuth),
|
bearertoken.New(rootTokenAuth),
|
||||||
bearertoken.New(serviceAccountTokenAuth),
|
bearertoken.New(serviceAccountTokenAuth),
|
||||||
|
|
Loading…
Reference in New Issue