Merge pull request #34029 from liggitt/service-account-rotation

Automatic merge from submit-queue

Enable service account signing key rotation

fixes #21007

```release-note
The kube-apiserver --service-account-key-file option can be specified multiple times, or can point to a file containing multiple keys, to enable rotation of signing keys.
```

This PR enables the apiserver authenticator to verify service account tokens signed by different private keys. This can be done two different ways:
* including multiple keys in the specified keyfile (e.g. `--service-account-key-file=keys.pem`)
* specifying multiple key files (e.g. `--service-account-key-file current-key.pem --service-account-key-file=old-key.pem`)

This is part of enabling signing key rotation:

1. update apiserver(s) to verify tokens signed with a new public key while still allowing tokens signed with the current public key (which is what this PR enables)
2. give controllermanager the new private key to sign new tokens with
3. remove old service account tokens (determined by verifying signature or by checking creationTimestamp) once they are no longer in use (determined using garbage collection or magic) or some other algorithm (24 hours after rotation, etc). For the deletion to immediately revoke the token, `--service-account-lookup` must be enabled on the apiserver.
4. once all old tokens are gone, update apiservers again, removing the old public key.
This commit is contained in:
Kubernetes Submit Queue 2016-10-10 21:54:03 -07:00 committed by GitHub
commit 1837914d8e
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

@ -203,11 +203,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")
} }
} }
@ -233,7 +233,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) {