From c797a91e3605ff4dfa5401cfa5c2f61276765fd0 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 30 Mar 2015 17:24:22 -0400 Subject: [PATCH] Add client cert authentication --- cmd/kube-apiserver/app/server.go | 20 ++++++++-- docs/authentication.md | 10 ++++- pkg/apiserver/authn.go | 57 +++++++++++++++++++++++++---- pkg/util/crypto.go | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 13 deletions(-) diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index e77d86d05c900..acfe1805b3f13 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -60,6 +60,7 @@ type APIServer struct { CloudProvider string CloudConfigFile string EventTTL time.Duration + ClientCAFile string TokenAuthFile string AuthorizationMode string AuthorizationPolicyFile string @@ -139,6 +140,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.CloudProvider, "cloud_provider", s.CloudProvider, "The provider for cloud services. Empty string for no provider.") fs.StringVar(&s.CloudConfigFile, "cloud_config", s.CloudConfigFile, "The path to the cloud provider configuration file. Empty string for no configuration file.") fs.DurationVar(&s.EventTTL, "event_ttl", s.EventTTL, "Amount of time to retain events. Default 1 hour.") + fs.StringVar(&s.ClientCAFile, "client_ca_file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client_ca_file is authenticated with an identity corresponding to the CommonName of the client certificate.") fs.StringVar(&s.TokenAuthFile, "token_auth_file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.") fs.StringVar(&s.AuthorizationMode, "authorization_mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) fs.StringVar(&s.AuthorizationPolicyFile, "authorization_policy_file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization_mode=ABAC, on the secure port.") @@ -222,7 +224,7 @@ func (s *APIServer) Run(_ []string) error { n := net.IPNet(s.PortalNet) - authenticator, err := apiserver.NewAuthenticatorFromTokenFile(s.TokenAuthFile) + authenticator, err := apiserver.NewAuthenticator(s.ClientCAFile, s.TokenAuthFile) if err != nil { glog.Fatalf("Invalid Authentication Config: %v", err) } @@ -330,11 +332,21 @@ func (s *APIServer) Run(_ []string) error { TLSConfig: &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) MinVersion: tls.VersionTLS10, - // Populate PeerCertificates in requests, but don't reject connections without certificates - // This allows certificates to be validated by authenticators, while still allowing other auth types - ClientAuth: tls.RequestClientCert, }, } + + if len(s.ClientCAFile) > 0 { + clientCAs, err := util.CertPoolFromFile(s.ClientCAFile) + if err != nil { + glog.Fatalf("unable to load client CA file: %v", err) + } + // Populate PeerCertificates in requests, but don't reject connections without certificates + // This allows certificates to be validated by authenticators, while still allowing other auth types + secureServer.TLSConfig.ClientAuth = tls.RequestClientCert + // Specify allowed CAs for client certificates + secureServer.TLSConfig.ClientCAs = clientCAs + } + glog.Infof("Serving securely on %s", secureLocation) go func() { defer util.HandleCrash() diff --git a/docs/authentication.md b/docs/authentication.md index 70e2c98d39806..dd90f89728ef6 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,8 +1,14 @@ # Authentication Plugins -Kubernetes uses tokens to authenticate users for API calls. +Kubernetes uses tokens or client certificates to authenticate users for API calls. -Authentication is enabled by passing the `--token_auth_file=SOMEFILE` option +Client certificate authentication is enabled by passing the `--client_ca_file=SOMEFILE` +option to apiserver. The referenced file must contain one or more certificates authorities +to use to validate client certificates presented to the apiserver. If a client certificate +is presented and verified, the common name of the subject is used as the user name for the +request. + +Token authentication is enabled by passing the `--token_auth_file=SOMEFILE` option to apiserver. Currently, tokens last indefinitely, and the token list cannot be changed without restarting apiserver. We plan in the future for tokens to be short-lived, and to be generated as needed rather than stored in a file. diff --git a/pkg/apiserver/authn.go b/pkg/apiserver/authn.go index bf1ebdac7d9f0..f606ba4bc4d4e 100644 --- a/pkg/apiserver/authn.go +++ b/pkg/apiserver/authn.go @@ -19,18 +19,61 @@ package apiserver import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/union" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/x509" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile" ) -// NewAuthenticatorFromTokenFile returns an authenticator.Request or an error -func NewAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request, error) { - var authenticator authenticator.Request - if len(tokenAuthFile) != 0 { - tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile) +// NewAuthenticator returns an authenticator.Request or an error +func NewAuthenticator(clientCAFile string, tokenFile string) (authenticator.Request, error) { + authenticators := []authenticator.Request{} + + if len(clientCAFile) > 0 { + certAuth, err := newAuthenticatorFromClientCAFile(clientCAFile) + if err != nil { + return nil, err + } + authenticators = append(authenticators, certAuth) + } + + if len(tokenFile) > 0 { + tokenAuth, err := newAuthenticatorFromTokenFile(tokenFile) if err != nil { return nil, err } - authenticator = bearertoken.New(tokenAuthenticator) + authenticators = append(authenticators, tokenAuth) + } + + if len(authenticators) == 0 { + return nil, nil } - return authenticator, nil + if len(authenticators) == 1 { + return authenticators[1], nil + } + return union.New(authenticators...), nil + +} + +// newAuthenticatorFromTokenFile returns an authenticator.Request or an error +func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request, error) { + tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile) + if err != nil { + return nil, err + } + + return bearertoken.New(tokenAuthenticator), nil +} + +// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error +func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) { + roots, err := util.CertPoolFromFile(clientCAFile) + if err != nil { + return nil, err + } + + opts := x509.DefaultVerifyOptions() + opts.Roots = roots + + return x509.New(opts, x509.CommonNameUserConversion), nil } diff --git a/pkg/util/crypto.go b/pkg/util/crypto.go index 87a485a991789..39e0724fac855 100644 --- a/pkg/util/crypto.go +++ b/pkg/util/crypto.go @@ -23,6 +23,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "errors" "fmt" "io/ioutil" "math/big" @@ -97,3 +98,65 @@ func GenerateSelfSignedCert(host, certPath, keyPath string) error { return nil } + +// CertPoolFromFile returns an x509.CertPool containing the certificates in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func CertPoolFromFile(filename string) (*x509.CertPool, error) { + certs, err := certificatesFromFile(filename) + if err != nil { + return nil, err + } + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(cert) + } + return pool, nil +} + +// certificatesFromFile returns the x509.Certificates contained in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func certificatesFromFile(file string) ([]*x509.Certificate, error) { + if len(file) == 0 { + return nil, errors.New("error reading certificates from an empty filename") + } + pemBlock, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + certs, err := certsFromPEM(pemBlock) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", file, err) + } + return certs, nil +} + +// certsFromPEM returns the x509.Certificates contained in the given PEM-encoded byte array +// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates +func certsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) { + ok := false + certs := []*x509.Certificate{} + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + // Only use PEM "CERTIFICATE" blocks without extra headers + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, err + } + + certs = append(certs, cert) + ok = true + } + + if !ok { + return certs, errors.New("could not read any certificates") + } + return certs, nil +}