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 MaxConnectionBytesPerSec int64
SSHKeyfile string SSHKeyfile string
SSHUser string SSHUser string
ServiceAccountKeyFile string ServiceAccountKeyFiles []string
ServiceAccountLookup bool ServiceAccountLookup bool
WebhookTokenAuthnConfigFile string WebhookTokenAuthnConfigFile string
WebhookTokenAuthnCacheTTL time.Duration WebhookTokenAuthnCacheTTL time.Duration
@ -70,9 +70,10 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.DurationVar(&s.EventTTL, "event-ttl", s.EventTTL, fs.DurationVar(&s.EventTTL, "event-ttl", s.EventTTL,
"Amount of time to retain events. Default is 1h.") "Amount of time to retain events. Default is 1h.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, ""+ fs.StringArrayVar(&s.ServiceAccountKeyFiles, "service-account-key-file", s.ServiceAccountKeyFiles, ""+
"File containing PEM-encoded x509 RSA or ECDSA private or public key, used to verify "+ "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.") "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, fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup,
"If true, validate ServiceAccount tokens exist in etcd as part of authentication.") "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 // 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) { if authenticator.IsValidServiceAccountKeyFile(s.TLSPrivateKeyFile) {
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile s.ServiceAccountKeyFiles = []string{s.TLSPrivateKeyFile}
} else { } 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, OIDCCAFile: s.OIDCCAFile,
OIDCUsernameClaim: s.OIDCUsernameClaim, OIDCUsernameClaim: s.OIDCUsernameClaim,
OIDCGroupsClaim: s.OIDCGroupsClaim, OIDCGroupsClaim: s.OIDCGroupsClaim,
ServiceAccountKeyFile: s.ServiceAccountKeyFile, ServiceAccountKeyFiles: s.ServiceAccountKeyFiles,
ServiceAccountLookup: s.ServiceAccountLookup, ServiceAccountLookup: s.ServiceAccountLookup,
ServiceAccountTokenGetter: serviceAccountGetter, ServiceAccountTokenGetter: serviceAccountGetter,
KeystoneURL: s.KeystoneURL, KeystoneURL: s.KeystoneURL,

View File

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

View File

@ -21,6 +21,7 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa" "crypto/rsa"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "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") 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. // 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) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
key, err := ReadPublicKeyFromPEM(data) keys, err := ReadPublicKeysFromPEM(data)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading public key file %s: %v", file, err) 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. // Reads public keys from both public and private key files.
func ReadPublicKeyFromPEM(data []byte) (interface{}, error) { func ReadPublicKeysFromPEM(data []byte) ([]interface{}, error) {
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil { var block *pem.Block
return &privateKey.PublicKey, nil keys := []interface{}{}
} for {
if publicKey, err := jwt.ParseRSAPublicKeyFromPEM(data); err == nil { // read the next block
return publicKey, nil 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 { if len(keys) == 0 {
return &privateKey.PublicKey, nil return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key")
} }
if publicKey, err := jwt.ParseECPublicKeyFromPEM(data); err == nil { return keys, nil
return publicKey, nil
}
return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key")
} }
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey. // 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{} { func getPublicKey(data string) interface{} {
key, _ := serviceaccount.ReadPublicKeyFromPEM([]byte(data)) keys, _ := serviceaccount.ReadPublicKeysFromPEM([]byte(data))
return key return keys[0]
} }
func TestReadPrivateKey(t *testing.T) { func TestReadPrivateKey(t *testing.T) {
f, err := ioutil.TempFile("", "") 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("", "") f, err := ioutil.TempFile("", "")
if err != nil { if err != nil {
t.Fatalf("error creating tmpfile: %v", err) 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 { if err := ioutil.WriteFile(f.Name(), []byte(rsaPublicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error writing public key to tmpfile: %v", err) 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) 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 { if err := ioutil.WriteFile(f.Name(), []byte(ecdsaPublicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error writing public key to tmpfile: %v", err) 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) 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) { func TestTokenGenerateAndValidate(t *testing.T) {