vendor updates

This commit is contained in:
Serguei Bezverkhi
2018-03-06 17:33:18 -05:00
parent 4b3ebc171b
commit e9033989a0
5854 changed files with 248382 additions and 119809 deletions

View File

@ -9,15 +9,19 @@ load(
go_library(
name = "go_default_library",
srcs = [
"claims.go",
"jwt.go",
"legacy.go",
"util.go",
],
importpath = "k8s.io/kubernetes/pkg/serviceaccount",
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/github.com/dgrijalva/jwt-go:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
@ -27,7 +31,6 @@ go_library(
go_test(
name = "go_default_xtest",
srcs = ["jwt_test.go"],
importpath = "k8s.io/kubernetes/pkg/serviceaccount_test",
deps = [
":go_default_library",
"//pkg/controller/serviceaccount:go_default_library",
@ -52,3 +55,18 @@ filegroup(
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = [
"claims_test.go",
"util_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)

186
vendor/k8s.io/kubernetes/pkg/serviceaccount/claims.go generated vendored Normal file
View File

@ -0,0 +1,186 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"errors"
"fmt"
"time"
"github.com/golang/glog"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/kubernetes/pkg/apis/core"
"gopkg.in/square/go-jose.v2/jwt"
)
// time.Now stubbed out to allow testing
var now = time.Now
type privateClaims struct {
Kubernetes kubernetes `json:"kubernetes.io,omitempty"`
}
type kubernetes struct {
Namespace string `json:"namespace,omitempty"`
Svcacct ref `json:"serviceaccount,omitempty"`
Pod *ref `json:"pod,omitempty"`
Secret *ref `json:"secret,omitempty"`
}
type ref struct {
Name string `json:"name,omitempty"`
UID string `json:"uid,omitempty"`
}
func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirationSeconds int64, audience []string) (*jwt.Claims, interface{}) {
now := now()
sc := &jwt.Claims{
Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name),
Audience: jwt.Audience(audience),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)),
}
pc := &privateClaims{
Kubernetes: kubernetes{
Namespace: sa.Namespace,
Svcacct: ref{
Name: sa.Name,
UID: string(sa.UID),
},
},
}
switch {
case pod != nil:
pc.Kubernetes.Pod = &ref{
Name: pod.Name,
UID: string(pod.UID),
}
case secret != nil:
pc.Kubernetes.Secret = &ref{
Name: secret.Name,
UID: string(secret.UID),
}
}
return sc, pc
}
func NewValidator(audiences []string, getter ServiceAccountTokenGetter) Validator {
return &validator{
auds: audiences,
getter: getter,
}
}
type validator struct {
auds []string
getter ServiceAccountTokenGetter
}
var _ = Validator(&validator{})
func (v *validator) Validate(_ string, public *jwt.Claims, privateObj interface{}) (string, string, string, error) {
private, ok := privateObj.(*privateClaims)
if !ok {
glog.Errorf("jwt validator expected private claim of type *privateClaims but got: %T", privateObj)
return "", "", "", errors.New("Token could not be validated.")
}
err := public.Validate(jwt.Expected{
Time: now(),
})
switch {
case err == nil:
case err == jwt.ErrExpired:
return "", "", "", errors.New("Token has expired.")
default:
glog.Errorf("unexpected validation error: %T", err)
return "", "", "", 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 "", "", "", errors.New("Token is invalid for this audience.")
}
namespace := private.Kubernetes.Namespace
saref := private.Kubernetes.Svcacct
podref := private.Kubernetes.Pod
secref := private.Kubernetes.Secret
// Make sure service account still exists (name and UID)
serviceAccount, err := v.getter.GetServiceAccount(namespace, saref.Name)
if err != nil {
glog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, saref.Name, err)
return "", "", "", err
}
if serviceAccount.DeletionTimestamp != nil {
glog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name)
return "", "", "", fmt.Errorf("ServiceAccount %s/%s has been deleted", namespace, saref.Name)
}
if string(serviceAccount.UID) != saref.UID {
glog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, saref.Name, string(serviceAccount.UID), saref.UID)
return "", "", "", fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, saref.UID)
}
if secref != nil {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := v.getter.GetSecret(namespace, secref.Name)
if err != nil {
glog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, secref.Name, namespace, saref.Name, err)
return "", "", "", errors.New("Token has been invalidated")
}
if secret.DeletionTimestamp != nil {
glog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name)
return "", "", "", errors.New("Token has been invalidated")
}
if string(secref.UID) != secref.UID {
glog.V(4).Infof("Secret UID no longer matches %s/%s: %q != %q", namespace, secref.Name, string(serviceAccount.UID), secref.UID)
return "", "", "", fmt.Errorf("Secret UID (%s) does not match claim (%s)", secret.UID, secref.UID)
}
}
if podref != nil {
// Make sure token hasn't been invalidated by deletion of the pod
pod, err := v.getter.GetPod(namespace, podref.Name)
if err != nil {
glog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, podref.Name, namespace, saref.Name, err)
return "", "", "", errors.New("Token has been invalidated")
}
if pod.DeletionTimestamp != nil {
glog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name)
return "", "", "", errors.New("Token has been invalidated")
}
if string(podref.UID) != podref.UID {
glog.V(4).Infof("Pod UID no longer matches %s/%s: %q != %q", namespace, podref.Name, string(serviceAccount.UID), podref.UID)
return "", "", "", fmt.Errorf("Pod UID (%s) does not match claim (%s)", pod.UID, podref.UID)
}
}
return private.Kubernetes.Namespace, private.Kubernetes.Svcacct.Name, private.Kubernetes.Svcacct.UID, nil
}
func (v *validator) NewPrivateClaims() interface{} {
return &privateClaims{}
}

View File

@ -0,0 +1,184 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"encoding/json"
"fmt"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/core"
"gopkg.in/square/go-jose.v2/jwt"
)
func init() {
now = func() time.Time {
// epoch time: 1514764800
return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
}
}
func TestClaims(t *testing.T) {
sa := core.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: "myns",
Name: "mysvcacct",
UID: "mysvcacct-uid",
},
}
pod := &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "myns",
Name: "mypod",
UID: "mypod-uid",
},
}
sec := &core.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "myns",
Name: "mysecret",
UID: "mysecret-uid",
},
}
cs := []struct {
// input
sa core.ServiceAccount
pod *core.Pod
sec *core.Secret
exp int64
aud []string
// desired
sc *jwt.Claims
pc *privateClaims
}{
{
// pod and secret
sa: sa,
pod: pod,
sec: sec,
// really fast
exp: 0,
// nil audience
aud: nil,
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
IssuedAt: jwt.NumericDate(1514764800),
NotBefore: jwt.NumericDate(1514764800),
Expiry: jwt.NumericDate(1514764800),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
},
},
},
{
// pod
sa: sa,
pod: pod,
// empty audience
aud: []string{},
exp: 100,
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
IssuedAt: jwt.NumericDate(1514764800),
NotBefore: jwt.NumericDate(1514764800),
Expiry: jwt.NumericDate(1514764800 + 100),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
},
},
},
{
// secret
sa: sa,
sec: sec,
exp: 100,
// single member audience
aud: []string{"1"},
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
Audience: []string{"1"},
IssuedAt: jwt.NumericDate(1514764800),
NotBefore: jwt.NumericDate(1514764800),
Expiry: jwt.NumericDate(1514764800 + 100),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
Secret: &ref{Name: "mysecret", UID: "mysecret-uid"},
},
},
},
{
// no obj binding
sa: sa,
exp: 100,
// multimember audience
aud: []string{"1", "2"},
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
Audience: []string{"1", "2"},
IssuedAt: jwt.NumericDate(1514764800),
NotBefore: jwt.NumericDate(1514764800),
Expiry: jwt.NumericDate(1514764800 + 100),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
},
},
},
}
for i, c := range cs {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
// comparing json spews has the benefit over
// reflect.DeepEqual that we are also asserting that
// claims structs are json serializable
spew := func(obj interface{}) string {
b, err := json.Marshal(obj)
if err != nil {
t.Fatalf("err, couldn't marshal claims: %v", err)
}
return string(b)
}
sc, pc := Claims(c.sa, c.pod, c.sec, c.exp, c.aud)
if spew(sc) != spew(c.sc) {
t.Errorf("standard claims differed\n\tsaw:\t%s\n\twant:\t%s", spew(sc), spew(c.sc))
}
if spew(pc) != spew(c.pc) {
t.Errorf("private claims differed\n\tsaw: %s\n\twant: %s", spew(pc), spew(c.pc))
}
})
}
}

View File

@ -17,69 +17,69 @@ limitations under the License.
package serviceaccount
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"k8s.io/api/core/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/authenticator"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
jwt "github.com/dgrijalva/jwt-go"
"github.com/golang/glog"
)
const (
Issuer = "kubernetes/serviceaccount"
SubjectClaim = "sub"
IssuerClaim = "iss"
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
jose "gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
// ServiceAccountTokenGetter defines functions to retrieve a named service account and secret
type ServiceAccountTokenGetter interface {
GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error)
GetPod(namespace, name string) (*v1.Pod, error)
GetSecret(namespace, name string) (*v1.Secret, error)
}
type TokenGenerator interface {
// GenerateToken generates a token which will identify the given ServiceAccount.
// The returned token will be stored in the given (and yet-unpersisted) Secret.
GenerateToken(serviceAccount v1.ServiceAccount, secret v1.Secret) (string, error)
// GenerateToken generates a token which will identify the given
// ServiceAccount. privateClaims is an interface that will be
// serialized into the JWT payload JSON encoding at the root level of
// the payload object. Public claims take precedent over private
// claims i.e. if both claims and privateClaims have an "exp" field,
// the value in claims will be used.
GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error)
}
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
// privateKey is a PEM-encoded byte array of a private RSA key.
// JWTTokenAuthenticator()
func JWTTokenGenerator(privateKey interface{}) TokenGenerator {
return &jwtTokenGenerator{privateKey}
func JWTTokenGenerator(iss string, privateKey interface{}) TokenGenerator {
return &jwtTokenGenerator{
iss: iss,
privateKey: privateKey,
}
}
type jwtTokenGenerator struct {
iss string
privateKey interface{}
}
func (j *jwtTokenGenerator) GenerateToken(serviceAccount v1.ServiceAccount, secret v1.Secret) (string, error) {
var method jwt.SigningMethod
func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) {
var alg jose.SignatureAlgorithm
switch privateKey := j.privateKey.(type) {
case *rsa.PrivateKey:
method = jwt.SigningMethodRS256
alg = jose.RS256
case *ecdsa.PrivateKey:
switch privateKey.Curve {
case elliptic.P256():
method = jwt.SigningMethodES256
alg = jose.ES256
case elliptic.P384():
method = jwt.SigningMethodES384
alg = jose.ES384
case elliptic.P521():
method = jwt.SigningMethodES512
alg = jose.ES512
default:
return "", fmt.Errorf("unknown private key curve, must be 256, 384, or 521")
}
@ -87,164 +87,126 @@ func (j *jwtTokenGenerator) GenerateToken(serviceAccount v1.ServiceAccount, secr
return "", fmt.Errorf("unknown private key type %T, must be *rsa.PrivateKey or *ecdsa.PrivateKey", j.privateKey)
}
token := jwt.New(method)
signer, err := jose.NewSigner(
jose.SigningKey{
Algorithm: alg,
Key: j.privateKey,
},
nil,
)
if err != nil {
return "", err
}
claims, _ := token.Claims.(jwt.MapClaims)
// Identify the issuer
claims[IssuerClaim] = Issuer
// Username
claims[SubjectClaim] = apiserverserviceaccount.MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
// Persist enough structured info for the authenticator to be able to look up the service account and secret
claims[NamespaceClaim] = serviceAccount.Namespace
claims[ServiceAccountNameClaim] = serviceAccount.Name
claims[ServiceAccountUIDClaim] = serviceAccount.UID
claims[SecretNameClaim] = secret.Name
// Sign and get the complete encoded token as a string
return token.SignedString(j.privateKey)
// claims are applied in reverse precedence
return jwt.Signed(signer).
Claims(privateClaims).
Claims(claims).
Claims(&jwt.Claims{
Issuer: j.iss,
}).
CompactSerialize()
}
// 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(keys []interface{}, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token {
return &jwtTokenAuthenticator{keys, lookup, getter}
func JWTTokenAuthenticator(iss string, keys []interface{}, validator Validator) authenticator.Token {
return &jwtTokenAuthenticator{
iss: iss,
keys: keys,
validator: validator,
}
}
type jwtTokenAuthenticator struct {
keys []interface{}
lookup bool
getter ServiceAccountTokenGetter
iss string
keys []interface{}
validator Validator
}
// Validator is called by the JWT token authentictaor to apply domain specific
// validation to a token and extract user information.
type Validator interface {
// Validate validates a token and returns user information or an error.
// Validator can assume that the issuer and signature of a token are already
// verified when this function is called.
Validate(tokenData string, public *jwt.Claims, private interface{}) (namespace, name, uid string, err error)
// NewPrivateClaims returns a struct that the authenticator should
// deserialize the JWT payload into. The authenticator may then pass this
// struct back to the Validator as the 'private' argument to a Validate()
// call. This struct should contain fields for any private claims that the
// Validator requires to validate the JWT.
NewPrivateClaims() interface{}
}
var errMismatchedSigningMethod = errors.New("invalid signing method")
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
var validationError error
for i, key := range j.keys {
// Attempt to verify with each key until we find one that works
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
switch token.Method.(type) {
case *jwt.SigningMethodRSA:
if _, ok := key.(*rsa.PublicKey); ok {
return key, nil
}
return nil, errMismatchedSigningMethod
case *jwt.SigningMethodECDSA:
if _, ok := key.(*ecdsa.PublicKey); ok {
return key, nil
}
return nil, errMismatchedSigningMethod
default:
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
})
if err != nil {
switch err := err.(type) {
case *jwt.ValidationError:
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
// Not a JWT, no point in continuing
return nil, false, nil
}
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
// Signature error, perhaps one of the other keys will verify the signature
// If not, we want to return this error
glog.V(4).Infof("Signature error (key %d): %v", i, err)
validationError = err
continue
}
// This key doesn't apply to the given signature type
// Perhaps one of the other keys will verify the signature
// If not, we want to return this error
if err.Inner == errMismatchedSigningMethod {
glog.V(4).Infof("Mismatched key type (key %d): %v", i, err)
validationError = err
continue
}
}
// Other errors should just return as errors
return nil, false, err
}
// If we get here, we have a token with a recognized signature
claims, _ := parsedToken.Claims.(jwt.MapClaims)
// Make sure we issued the token
iss, _ := claims[IssuerClaim].(string)
if iss != Issuer {
return nil, false, nil
}
// Make sure the claims we need exist
sub, _ := claims[SubjectClaim].(string)
if len(sub) == 0 {
return nil, false, errors.New("sub claim is missing")
}
namespace, _ := claims[NamespaceClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("namespace claim is missing")
}
secretName, _ := claims[SecretNameClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("secretName claim is missing")
}
serviceAccountName, _ := claims[ServiceAccountNameClaim].(string)
if len(serviceAccountName) == 0 {
return nil, false, errors.New("serviceAccountName claim is missing")
}
serviceAccountUID, _ := claims[ServiceAccountUIDClaim].(string)
if len(serviceAccountUID) == 0 {
return nil, false, errors.New("serviceAccountUID claim is missing")
}
subjectNamespace, subjectName, err := apiserverserviceaccount.SplitUsername(sub)
if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName {
return nil, false, errors.New("sub claim is invalid")
}
if j.lookup {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := j.getter.GetSecret(namespace, secretName)
if err != nil {
glog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err)
return nil, false, errors.New("Token has been invalidated")
}
if secret.DeletionTimestamp != nil {
glog.V(4).Infof("Token is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
return nil, false, errors.New("Token has been invalidated")
}
if bytes.Compare(secret.Data[v1.ServiceAccountTokenKey], []byte(token)) != 0 {
glog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
return nil, false, errors.New("Token does not match server's copy")
}
// Make sure service account still exists (name and UID)
serviceAccount, err := j.getter.GetServiceAccount(namespace, serviceAccountName)
if err != nil {
glog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err)
return nil, false, err
}
if serviceAccount.DeletionTimestamp != nil {
glog.V(4).Infof("Service account has been deleted %s/%s", namespace, serviceAccountName)
return nil, false, fmt.Errorf("ServiceAccount %s/%s has been deleted", namespace, serviceAccountName)
}
if string(serviceAccount.UID) != serviceAccountUID {
glog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID)
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
}
}
return UserInfo(namespace, serviceAccountName, serviceAccountUID), true, nil
func (j *jwtTokenAuthenticator) AuthenticateToken(tokenData string) (user.Info, bool, error) {
if !j.hasCorrectIssuer(tokenData) {
return nil, false, nil
}
return nil, false, validationError
tok, err := jwt.ParseSigned(tokenData)
if err != nil {
return nil, false, nil
}
public := &jwt.Claims{}
private := j.validator.NewPrivateClaims()
var (
found bool
errlist []error
)
for _, key := range j.keys {
if err := tok.Claims(key, public, private); err != nil {
errlist = append(errlist, err)
continue
}
found = true
break
}
if !found {
return nil, false, utilerrors.NewAggregate(errlist)
}
// If we get here, we have a token with a recognized signature and
// issuer string.
ns, name, uid, err := j.validator.Validate(tokenData, public, private)
if err != nil {
return nil, false, err
}
return UserInfo(ns, name, uid), true, nil
}
// hasCorrectIssuer returns true if tokenData is a valid JWT in compact
// serialization format and the "iss" claim matches the iss field of this token
// authenticator, and otherwise returns false.
//
// Note: go-jose currently does not allow access to unverified JWS payloads.
// See https://github.com/square/go-jose/issues/169
func (j *jwtTokenAuthenticator) hasCorrectIssuer(tokenData string) bool {
parts := strings.Split(tokenData, ".")
if len(parts) != 3 {
return false
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return false
}
claims := struct {
// WARNING: this JWT is not verified. Do not trust these claims.
Issuer string `json:"iss"`
}{}
if err := json.Unmarshal(payload, &claims); err != nil {
return false
}
if claims.Issuer != j.iss {
return false
}
return true
}

View File

@ -80,17 +80,6 @@ X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
-----END RSA PRIVATE KEY-----
`
// openssl ecparam -name prime256v1 -genkey -out ecdsa256params.pem
const ecdsaPrivateKeyWithParams = `-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJ9LWDj3ZWe9CksPV7mZjD2dYXG9icfzxadCRwd3vr1toAoGCCqGSM49
AwEHoUQDQgAEaLNEpzbaaNTCkKjBVj7sxpfJ1ifJQGNvcck4nrzcwFRuujwVDDJh
95iIGwKCQeSg+yhdN6Q/p2XaxNIZlYmUhg==
-----END EC PRIVATE KEY-----
`
// openssl ecparam -name prime256v1 -genkey -noout -out ecdsa256.pem
const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49
@ -139,8 +128,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
}
// Generate the RSA token
rsaGenerator := serviceaccount.JWTTokenGenerator(getPrivateKey(rsaPrivateKey))
rsaToken, err := rsaGenerator.GenerateToken(*serviceAccount, *rsaSecret)
rsaGenerator := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, getPrivateKey(rsaPrivateKey))
rsaToken, err := rsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret))
if err != nil {
t.Fatalf("error generating token: %v", err)
}
@ -152,8 +141,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
}
// Generate the ECDSA token
ecdsaGenerator := serviceaccount.JWTTokenGenerator(getPrivateKey(ecdsaPrivateKey))
ecdsaToken, err := ecdsaGenerator.GenerateToken(*serviceAccount, *ecdsaSecret)
ecdsaGenerator := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, getPrivateKey(ecdsaPrivateKey))
ecdsaToken, err := ecdsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *ecdsaSecret))
if err != nil {
t.Fatalf("error generating token: %v", err)
}
@ -164,6 +153,13 @@ func TestTokenGenerateAndValidate(t *testing.T) {
"token": []byte(ecdsaToken),
}
// Generate signer with same keys as RSA signer but different issuer
badIssuerGenerator := serviceaccount.JWTTokenGenerator("foo", getPrivateKey(rsaPrivateKey))
badIssuerToken, err := badIssuerGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret))
if err != nil {
t.Fatalf("error generating token: %v", err)
}
testCases := map[string]struct {
Client clientset.Interface
Keys []interface{}
@ -206,6 +202,13 @@ func TestTokenGenerateAndValidate(t *testing.T) {
ExpectedUserUID: expectedUserUID,
ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"},
},
"valid key, invalid issuer (rsa)": {
Token: badIssuerToken,
Client: nil,
Keys: []interface{}{getPublicKey(rsaPublicKey)},
ExpectedErr: false,
ExpectedOK: false,
},
"valid key (ecdsa)": {
Token: ecdsaToken,
Client: nil,
@ -264,7 +267,7 @@ func TestTokenGenerateAndValidate(t *testing.T) {
for k, tc := range testCases {
getter := serviceaccountcontroller.NewGetterFromClient(tc.Client)
authenticator := serviceaccount.JWTTokenAuthenticator(tc.Keys, tc.Client != nil, getter)
authenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, tc.Keys, serviceaccount.NewLegacyValidator(tc.Client != nil, getter))
// An invalid, non-JWT token should always fail
if _, ok, err := authenticator.AuthenticateToken("invalid token"); err != nil || ok {

135
vendor/k8s.io/kubernetes/pkg/serviceaccount/legacy.go generated vendored Normal file
View File

@ -0,0 +1,135 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"bytes"
"errors"
"fmt"
"k8s.io/api/core/v1"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"github.com/golang/glog"
"gopkg.in/square/go-jose.v2/jwt"
)
func LegacyClaims(serviceAccount v1.ServiceAccount, secret v1.Secret) (*jwt.Claims, interface{}) {
return &jwt.Claims{
Subject: apiserverserviceaccount.MakeUsername(serviceAccount.Namespace, serviceAccount.Name),
}, &legacyPrivateClaims{
Namespace: serviceAccount.Namespace,
ServiceAccountName: serviceAccount.Name,
ServiceAccountUID: string(serviceAccount.UID),
SecretName: secret.Name,
}
}
const LegacyIssuer = "kubernetes/serviceaccount"
type legacyPrivateClaims struct {
ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
ServiceAccountUID string `json:"kubernetes.io/serviceaccount/service-account.uid"`
SecretName string `json:"kubernetes.io/serviceaccount/secret.name"`
Namespace string `json:"kubernetes.io/serviceaccount/namespace"`
}
func NewLegacyValidator(lookup bool, getter ServiceAccountTokenGetter) Validator {
return &legacyValidator{
lookup: lookup,
getter: getter,
}
}
type legacyValidator struct {
lookup bool
getter ServiceAccountTokenGetter
}
var _ = Validator(&legacyValidator{})
func (v *legacyValidator) Validate(tokenData string, public *jwt.Claims, privateObj interface{}) (string, string, string, error) {
private, ok := privateObj.(*legacyPrivateClaims)
if !ok {
glog.Errorf("jwt validator expected private claim of type *legacyPrivateClaims but got: %T", privateObj)
return "", "", "", errors.New("Token could not be validated.")
}
// Make sure the claims we need exist
if len(public.Subject) == 0 {
return "", "", "", errors.New("sub claim is missing")
}
namespace := private.Namespace
if len(namespace) == 0 {
return "", "", "", errors.New("namespace claim is missing")
}
secretName := private.SecretName
if len(secretName) == 0 {
return "", "", "", errors.New("secretName claim is missing")
}
serviceAccountName := private.ServiceAccountName
if len(serviceAccountName) == 0 {
return "", "", "", errors.New("serviceAccountName claim is missing")
}
serviceAccountUID := private.ServiceAccountUID
if len(serviceAccountUID) == 0 {
return "", "", "", errors.New("serviceAccountUID claim is missing")
}
subjectNamespace, subjectName, err := apiserverserviceaccount.SplitUsername(public.Subject)
if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName {
return "", "", "", errors.New("sub claim is invalid")
}
if v.lookup {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := v.getter.GetSecret(namespace, secretName)
if err != nil {
glog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err)
return "", "", "", errors.New("Token has been invalidated")
}
if secret.DeletionTimestamp != nil {
glog.V(4).Infof("Token is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
return "", "", "", errors.New("Token has been invalidated")
}
if bytes.Compare(secret.Data[v1.ServiceAccountTokenKey], []byte(tokenData)) != 0 {
glog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
return "", "", "", errors.New("Token does not match server's copy")
}
// Make sure service account still exists (name and UID)
serviceAccount, err := v.getter.GetServiceAccount(namespace, serviceAccountName)
if err != nil {
glog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err)
return "", "", "", err
}
if serviceAccount.DeletionTimestamp != nil {
glog.V(4).Infof("Service account has been deleted %s/%s", namespace, serviceAccountName)
return "", "", "", fmt.Errorf("ServiceAccount %s/%s has been deleted", namespace, serviceAccountName)
}
if string(serviceAccount.UID) != serviceAccountUID {
glog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID)
return "", "", "", fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
}
}
return private.Namespace, private.ServiceAccountName, private.ServiceAccountUID, nil
}
func (v *legacyValidator) NewPrivateClaims() interface{} {
return &legacyPrivateClaims{}
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"testing"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestIsServiceAccountToken(t *testing.T) {
secretIns := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "token-secret-1",
Namespace: "default",
UID: "23456",
ResourceVersion: "1",
Annotations: map[string]string{
v1.ServiceAccountNameKey: "default",
v1.ServiceAccountUIDKey: "12345",
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
"token": []byte("ABC"),
"ca.crt": []byte("CA Data"),
"namespace": []byte("default"),
},
}
saIns := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
UID: "12345",
Namespace: "default",
ResourceVersion: "1",
},
}
saInsNameNotEqual := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "non-default",
UID: "12345",
Namespace: "default",
ResourceVersion: "1",
},
}
saInsUIDNotEqual := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
UID: "67890",
Namespace: "default",
ResourceVersion: "1",
},
}
tests := map[string]struct {
secret *v1.Secret
sa *v1.ServiceAccount
expect bool
}{
"correct service account": {
secret: secretIns,
sa: saIns,
expect: true,
},
"service account name not equal": {
secret: secretIns,
sa: saInsNameNotEqual,
expect: false,
},
"service account uid not equal": {
secret: secretIns,
sa: saInsUIDNotEqual,
expect: false,
},
}
for k, v := range tests {
actual := IsServiceAccountToken(v.secret, v.sa)
if actual != v.expect {
t.Errorf("%s failed, expected %t but received %t", k, v.expect, actual)
}
}
}