Enable service account signing key rotation

This commit is contained in:
Jordan Liggitt 2016-10-04 13:31:17 -04:00
parent 96fde0fe8d
commit 3c92eb75b3
No known key found for this signature in database
GPG Key ID: 24E7ADF9A3B42012
5 changed files with 82 additions and 39 deletions

View File

@ -37,7 +37,7 @@ type APIServer struct {
MaxConnectionBytesPerSec int64
SSHKeyfile string
SSHUser string
ServiceAccountKeyFile string
ServiceAccountKeyFiles []string
ServiceAccountLookup bool
WebhookTokenAuthnConfigFile string
WebhookTokenAuthnCacheTTL time.Duration
@ -70,9 +70,10 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.DurationVar(&s.EventTTL, "event-ttl", s.EventTTL,
"Amount of time to retain events. Default is 1h.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, ""+
"File containing PEM-encoded x509 RSA or ECDSA private or public key, used to verify "+
"ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
fs.StringArrayVar(&s.ServiceAccountKeyFiles, "service-account-key-file", s.ServiceAccountKeyFiles, ""+
"File containing PEM-encoded x509 RSA or ECDSA private or public keys, used to verify "+
"ServiceAccount tokens. If unspecified, --tls-private-key-file is used. "+
"The specified file can contain multiple keys, and the flag can be specified multiple times with different files.")
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup,
"If true, validate ServiceAccount tokens exist in etcd as part of authentication.")

View File

@ -181,11 +181,11 @@ func Run(s *options.APIServer) error {
}
// Default to the private server key for service account token signing
if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {
if len(s.ServiceAccountKeyFiles) == 0 && s.TLSPrivateKeyFile != "" {
if authenticator.IsValidServiceAccountKeyFile(s.TLSPrivateKeyFile) {
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile
s.ServiceAccountKeyFiles = []string{s.TLSPrivateKeyFile}
} else {
glog.Warning("No RSA key provided, service account token authentication disabled")
glog.Warning("No TLS key provided, service account token authentication disabled")
}
}
@ -211,7 +211,7 @@ func Run(s *options.APIServer) error {
OIDCCAFile: s.OIDCCAFile,
OIDCUsernameClaim: s.OIDCUsernameClaim,
OIDCGroupsClaim: s.OIDCGroupsClaim,
ServiceAccountKeyFile: s.ServiceAccountKeyFile,
ServiceAccountKeyFiles: s.ServiceAccountKeyFiles,
ServiceAccountLookup: s.ServiceAccountLookup,
ServiceAccountTokenGetter: serviceAccountGetter,
KeystoneURL: s.KeystoneURL,

View File

@ -48,7 +48,7 @@ type AuthenticatorConfig struct {
OIDCCAFile string
OIDCUsernameClaim string
OIDCGroupsClaim string
ServiceAccountKeyFile string
ServiceAccountKeyFiles []string
ServiceAccountLookup bool
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
KeystoneURL string
@ -94,8 +94,8 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) {
}
authenticators = append(authenticators, tokenAuth)
}
if len(config.ServiceAccountKeyFile) > 0 {
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFile, config.ServiceAccountLookup, config.ServiceAccountTokenGetter)
if len(config.ServiceAccountKeyFiles) > 0 {
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.ServiceAccountTokenGetter)
if err != nil {
return nil, err
}
@ -152,7 +152,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) {
// IsValidServiceAccountKeyFile returns true if a valid public RSA key can be read from the given file
func IsValidServiceAccountKeyFile(file string) bool {
_, err := serviceaccount.ReadPublicKey(file)
_, err := serviceaccount.ReadPublicKeys(file)
return err == nil
}
@ -198,13 +198,17 @@ func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClai
}
// newServiceAccountAuthenticator returns an authenticator.Request or an error
func newServiceAccountAuthenticator(keyfile string, lookup bool, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Request, error) {
publicKey, err := serviceaccount.ReadPublicKey(keyfile)
if err != nil {
return nil, err
func newServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Request, error) {
allPublicKeys := []interface{}{}
for _, keyfile := range keyfiles {
publicKeys, err := serviceaccount.ReadPublicKeys(keyfile)
if err != nil {
return nil, err
}
allPublicKeys = append(allPublicKeys, publicKeys...)
}
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]interface{}{publicKey}, lookup, serviceAccountGetter)
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(allPublicKeys, lookup, serviceAccountGetter)
return bearertoken.New(tokenAuthenticator), nil
}

View File

@ -21,6 +21,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
@ -80,37 +81,60 @@ func ReadPrivateKeyFromPEM(data []byte) (interface{}, error) {
return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA private key")
}
// ReadPublicKey is a helper function for reading an rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded file.
// ReadPublicKeys is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded file.
// Reads public keys from both public and private key files.
func ReadPublicKey(file string) (interface{}, error) {
func ReadPublicKeys(file string) ([]interface{}, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
key, err := ReadPublicKeyFromPEM(data)
keys, err := ReadPublicKeysFromPEM(data)
if err != nil {
return nil, fmt.Errorf("error reading public key file %s: %v", file, err)
}
return key, nil
return keys, nil
}
// ReadPublicKeyFromPEM is a helper function for reading an rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array.
// ReadPublicKeysFromPEM is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array.
// Reads public keys from both public and private key files.
func ReadPublicKeyFromPEM(data []byte) (interface{}, error) {
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
return &privateKey.PublicKey, nil
}
if publicKey, err := jwt.ParseRSAPublicKeyFromPEM(data); err == nil {
return publicKey, nil
func ReadPublicKeysFromPEM(data []byte) ([]interface{}, error) {
var block *pem.Block
keys := []interface{}{}
for {
// read the next block
block, data = pem.Decode(data)
if block == nil {
break
}
// get PEM bytes for just this block
blockData := pem.EncodeToMemory(block)
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(blockData); err == nil {
keys = append(keys, &privateKey.PublicKey)
continue
}
if publicKey, err := jwt.ParseRSAPublicKeyFromPEM(blockData); err == nil {
keys = append(keys, publicKey)
continue
}
if privateKey, err := jwt.ParseECPrivateKeyFromPEM(blockData); err == nil {
keys = append(keys, &privateKey.PublicKey)
continue
}
if publicKey, err := jwt.ParseECPublicKeyFromPEM(blockData); err == nil {
keys = append(keys, publicKey)
continue
}
// tolerate non-key PEM blocks for backwards compatibility
// originally, only the first PEM block was parsed and expected to be a key block
}
if privateKey, err := jwt.ParseECPrivateKeyFromPEM(data); err == nil {
return &privateKey.PublicKey, nil
if len(keys) == 0 {
return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key")
}
if publicKey, err := jwt.ParseECPublicKeyFromPEM(data); err == nil {
return publicKey, nil
}
return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key")
return keys, nil
}
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.

View File

@ -98,8 +98,8 @@ func getPrivateKey(data string) interface{} {
}
func getPublicKey(data string) interface{} {
key, _ := serviceaccount.ReadPublicKeyFromPEM([]byte(data))
return key
keys, _ := serviceaccount.ReadPublicKeysFromPEM([]byte(data))
return keys[0]
}
func TestReadPrivateKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
@ -123,7 +123,7 @@ func TestReadPrivateKey(t *testing.T) {
}
}
func TestReadPublicKey(t *testing.T) {
func TestReadPublicKeys(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
@ -133,16 +133,30 @@ func TestReadPublicKey(t *testing.T) {
if err := ioutil.WriteFile(f.Name(), []byte(rsaPublicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error writing public key to tmpfile: %v", err)
}
if _, err := serviceaccount.ReadPublicKey(f.Name()); err != nil {
if keys, err := serviceaccount.ReadPublicKeys(f.Name()); err != nil {
t.Fatalf("error reading RSA public key: %v", err)
} else if len(keys) != 1 {
t.Fatalf("expected 1 key, got %d", len(keys))
}
if err := ioutil.WriteFile(f.Name(), []byte(ecdsaPublicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error writing public key to tmpfile: %v", err)
}
if _, err := serviceaccount.ReadPublicKey(f.Name()); err != nil {
if keys, err := serviceaccount.ReadPublicKeys(f.Name()); err != nil {
t.Fatalf("error reading ECDSA public key: %v", err)
} else if len(keys) != 1 {
t.Fatalf("expected 1 key, got %d", len(keys))
}
if err := ioutil.WriteFile(f.Name(), []byte(rsaPublicKey+"\n"+ecdsaPublicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error writing public key to tmpfile: %v", err)
}
if keys, err := serviceaccount.ReadPublicKeys(f.Name()); err != nil {
t.Fatalf("error reading combined RSA/ECDSA public key file: %v", err)
} else if len(keys) != 2 {
t.Fatalf("expected 2 keys, got %d", len(keys))
}
}
func TestTokenGenerateAndValidate(t *testing.T) {