Skip to content

Commit

Permalink
Add client cert authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
liggitt committed Apr 1, 2015
1 parent 92b6f49 commit c797a91
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 13 deletions.
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
}

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) {
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
}

0 comments on commit c797a91

Please sign in to comment.