Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client cert authentication #6190

Merged
merged 1 commit into from
Apr 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 16 additions & 4 deletions cmd/kube-apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type APIServer struct {
CloudProvider string
CloudConfigFile string
EventTTL time.Duration
ClientCAFile string
TokenAuthFile string
AuthorizationMode string
AuthorizationPolicyFile string
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I am accessing the apiserver via my browser and I have installed a client cert from a CA which is not in the clientCAs pool? Then I automatically get rejected. That seems wrong.

If we tried to expand the clientCAs pool to include our cluster's CA plus all known trustworthy CAs, then it would mean that anyone with a client cert from any of those CAs could authenticate to our cluster, which is also not good.

So, I think the set of allowed client CAs needs to be broader than the set of automatically-authenticating client CAs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a way to set the TLSConfig to allow connections with any cert, and do all the checking in the Authenticator, then I think that is the way to go for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You won't get rejected.

Setting ClientAuth=RequestClientCert means a client cert is requested, but not required or validated at the connection level.

Setting ClientCAs tells clients what CAs are valid to present client certificates for. If your browser doesn't have any client certificates for that CA, it won't send any. Leaving ClientCAs blank means some browsers will try to send any client cert they have.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a way to set the TLSConfig to allow connections with any cert, and do all the checking in the Authenticator, then I think that is the way to go for now?

That's exactly what this does. See "This allows certificates to be validated by authenticators, while still allowing other auth types" comment :)

Requests without client certs, with invalid client certs, or with valid client certs are all allowed at the transport layer.

Requests without a client cert will skip the client cert Authenticator.
Requests with an "invalid" client cert will fail the client cert Authenticator, but could still get an identity from the token Authenticator.
Requests with a valid client cert will get an identity from the client cert Authenticator.

}

glog.Infof("Serving securely on %s", secureLocation)
go func() {
defer util.HandleCrash()
Expand Down
10 changes: 8 additions & 2 deletions docs/authentication.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
57 changes: 50 additions & 7 deletions pkg/apiserver/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
63 changes: 63 additions & 0 deletions pkg/util/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"math/big"
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now only this function is used outside the util package. Why not restrict the visibility of the helper functions until it's necessary for outside code to call them directly?

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
}