retrofit svcacct token authenticator to support audience validation

pull/58/head
Mike Danese 2018-10-23 19:44:59 -07:00
parent bfb95290b9
commit 67bbf753cb
7 changed files with 52 additions and 38 deletions

View File

@ -64,7 +64,7 @@ type Config struct {
ServiceAccountKeyFiles []string
ServiceAccountLookup bool
ServiceAccountIssuer string
APIAudiences []string
APIAudiences authenticator.Audiences
WebhookTokenAuthnConfigFile string
WebhookTokenAuthnCacheTTL time.Duration
@ -135,14 +135,14 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
}
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 {
return nil, nil, err
}
tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, serviceAccountAuth))
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
}
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 {
return nil, nil, err
}
@ -276,7 +276,7 @@ func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token,
}
// 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{}{}
for _, keyfile := range keyfiles {
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
@ -286,12 +286,12 @@ func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, servic
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
}
// 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{}{}
for _, keyfile := range keyfiles {
publicKeys, err := certutil.PublicKeysFromFile(keyfile)
@ -301,7 +301,7 @@ func newServiceAccountAuthenticator(iss string, audiences []string, keyfiles []s
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
}

View File

@ -54,6 +54,7 @@ go_test(
"//pkg/controller/serviceaccount: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/apiserver/pkg/authentication/authenticator: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/util/cert:go_default_library",

View File

@ -80,15 +80,13 @@ func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirati
return sc, pc
}
func NewValidator(audiences []string, getter ServiceAccountTokenGetter) Validator {
func NewValidator(getter ServiceAccountTokenGetter) Validator {
return &validator{
auds: audiences,
getter: getter,
}
}
type validator struct {
auds []string
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.")
}
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
saref := private.Kubernetes.Svcacct
podref := private.Kubernetes.Pod

View File

@ -111,18 +111,20 @@ func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims inte
// 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)
// 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{
iss: iss,
keys: keys,
validator: validator,
iss: iss,
keys: keys,
implicitAuds: implicitAuds,
validator: validator,
}
}
type jwtTokenAuthenticator struct {
iss string
keys []interface{}
validator Validator
iss string
keys []interface{}
validator Validator
implicitAuds authenticator.Audiences
}
// 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)
}
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
// issuer string.
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 &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

View File

@ -23,6 +23,7 @@ import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/authenticator"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
certutil "k8s.io/client-go/util/cert"
@ -275,16 +276,18 @@ func TestTokenGenerateAndValidate(t *testing.T) {
}
for k, tc := range testCases {
auds := authenticator.Audiences{"api"}
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
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)
continue
}
resp, ok, err := authenticator.AuthenticateToken(context.Background(), tc.Token)
resp, ok, err := authn.AuthenticateToken(ctx, tc.Token)
if (err != nil) != tc.ExpectedErr {
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
continue

View File

@ -32,6 +32,7 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
@ -63,7 +64,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
pk := sk.(*ecdsa.PrivateKey).PublicKey
const iss = "https://foo.bar.example.com"
aud := []string{"api"}
aud := authenticator.Audiences{"api"}
maxExpirationSeconds := int64(60 * 60)
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
@ -76,11 +77,13 @@ func TestServiceAccountTokenCreate(t *testing.T) {
// Start the server
masterConfig := framework.NewIntegrationTestMasterConfig()
masterConfig.GenericConfig.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
masterConfig.GenericConfig.Authentication.APIAudiences = aud
masterConfig.GenericConfig.Authentication.Authenticator = bearertoken.New(
serviceaccount.JWTTokenAuthenticator(
iss,
[]interface{}{&pk},
serviceaccount.NewValidator(aud, serviceaccountgetter.NewGetterFromClient(gcs)),
aud,
serviceaccount.NewValidator(serviceaccountgetter.NewGetterFromClient(gcs)),
),
)
tokenGenerator, err := serviceaccount.JWTTokenGenerator(iss, sk)

View File

@ -374,7 +374,7 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie
})
serviceAccountKey, _ := rsa.GenerateKey(rand.Reader, 2048)
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(
bearertoken.New(rootTokenAuth),
bearertoken.New(serviceAccountTokenAuth),