diff --git a/pkg/auth/authenticator/request/x509request/x509.go b/pkg/auth/authenticator/request/x509request/x509.go index 060389a46b70..7a54302b447a 100644 --- a/pkg/auth/authenticator/request/x509request/x509.go +++ b/pkg/auth/authenticator/request/x509request/x509.go @@ -6,7 +6,8 @@ import ( "net/http" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" + "github.com/openshift/origin/pkg/auth/authenticator" ) // UserConversion defines an interface for extracting user info from a client certificate chain @@ -60,7 +61,35 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, } } } - return nil, false, errors.NewAggregate(errlist) + return nil, false, kerrors.NewAggregate(errlist) +} + +// Verifier implements request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth +type Verifier struct { + opts x509.VerifyOptions + auth authenticator.Request +} + +func NewVerifier(opts x509.VerifyOptions, auth authenticator.Request) authenticator.Request { + return &Verifier{opts, auth} +} + +// AuthenticateRequest verifies the presented client certificates, then delegates to the wrapped auth +func (a *Verifier) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + if req.TLS == nil { + return nil, false, nil + } + + var errlist []error + for _, cert := range req.TLS.PeerCertificates { + _, err := cert.Verify(a.opts) + if err != nil { + errlist = append(errlist, err) + continue + } + return a.auth.AuthenticateRequest(req) + } + return nil, false, kerrors.NewAggregate(errlist) } // DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time, diff --git a/pkg/auth/authenticator/request/x509request/x509_test.go b/pkg/auth/authenticator/request/x509request/x509_test.go index 8ee5dac883a4..c311894ca188 100644 --- a/pkg/auth/authenticator/request/x509request/x509_test.go +++ b/pkg/auth/authenticator/request/x509request/x509_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" + "github.com/openshift/origin/pkg/auth/authenticator" ) const ( @@ -528,6 +529,127 @@ func TestX509(t *testing.T) { } } +func TestX509Verifier(t *testing.T) { + testCases := map[string]struct { + Insecure bool + Certs []*x509.Certificate + + Opts x509.VerifyOptions + + ExpectOK bool + ExpectErr bool + }{ + "non-tls": { + Insecure: true, + + ExpectOK: false, + ExpectErr: false, + }, + + "tls, no certs": { + ExpectOK: false, + ExpectErr: false, + }, + + "self signed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, selfSignedCert), + + ExpectErr: true, + }, + + "server cert disallowed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, serverCert), + + ExpectErr: true, + }, + "server cert allowing non-client cert usages": { + Opts: x509.VerifyOptions{Roots: getRootCertPool(t)}, + Certs: getCerts(t, serverCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "valid client cert": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "future cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(time.Duration(-100 * time.Hour * 24 * 365)), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + "expired cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(time.Duration(100 * time.Hour * 24 * 365)), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + } + + for k, testCase := range testCases { + req, _ := http.NewRequest("GET", "/", nil) + if !testCase.Insecure { + req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs} + } + + authCall := false + auth := authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + authCall = true + return &user.DefaultInfo{Name: "innerauth"}, true, nil + }) + + a := NewVerifier(testCase.Opts, auth) + + user, ok, err := a.AuthenticateRequest(req) + + if testCase.ExpectErr && err == nil { + t.Errorf("%s: Expected error, got none", k) + continue + } + if !testCase.ExpectErr && err != nil { + t.Errorf("%s: Got unexpected error: %v", k, err) + continue + } + + if testCase.ExpectOK != ok { + t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok) + continue + } + + if testCase.ExpectOK { + if !authCall { + t.Errorf("%s: Expected inner auth called, wasn't") + continue + } + if "innerauth" != user.GetName() { + t.Errorf("%s: Expected user.name=%v, got %v", k, "innerauth", user.GetName()) + continue + } + } else { + if authCall { + t.Errorf("%s: Expected inner auth not to be called, was") + continue + } + } + } +} + func getDefaultVerifyOptions(t *testing.T) x509.VerifyOptions { options := DefaultVerifyOptions() options.Roots = getRootCertPool(t) diff --git a/pkg/cmd/server/crypto/crypto.go b/pkg/cmd/server/crypto/crypto.go index 2688e6f5da68..343b9d0c2b2f 100644 --- a/pkg/cmd/server/crypto/crypto.go +++ b/pkg/cmd/server/crypto/crypto.go @@ -426,6 +426,34 @@ func IPAddressesDNSNames(hosts []string) ([]net.IP, []string) { return ips, dns } +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 + } + 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 +} + // Can be used as a certificate in http.Transport TLSClientConfig func newClientCertificateTemplate(subject pkix.Name) (*x509.Certificate, error) { return &x509.Certificate{ diff --git a/pkg/cmd/server/origin/auth.go b/pkg/cmd/server/origin/auth.go index 23e3efb61516..2b9087e07bc2 100644 --- a/pkg/cmd/server/origin/auth.go +++ b/pkg/cmd/server/origin/auth.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io/ioutil" "net/http" "net/url" "path" @@ -28,6 +29,7 @@ import ( "github.com/openshift/origin/pkg/auth/authenticator/request/bearertoken" "github.com/openshift/origin/pkg/auth/authenticator/request/headerrequest" "github.com/openshift/origin/pkg/auth/authenticator/request/unionrequest" + "github.com/openshift/origin/pkg/auth/authenticator/request/x509request" "github.com/openshift/origin/pkg/auth/authenticator/token/filetoken" "github.com/openshift/origin/pkg/auth/oauth/external" "github.com/openshift/origin/pkg/auth/oauth/external/github" @@ -195,6 +197,9 @@ type AuthConfig struct { // RequestHeaders lists the headers to check (in order) for a username. Used by AuthRequestHandlerRequestHeader RequestHeaders []string + // RequestHeaderCAFile specifies the path to a PEM-encoded certificate bundle. + // If set, a client certificate must be presented and validate against the CA before the request headers are checked for usernames + RequestHeaderCAFile string // SessionSecrets list the secret(s) to use to encrypt created sessions. Used by AuthRequestHandlerSession SessionSecrets []string @@ -229,7 +234,10 @@ func (c *AuthConfig) InstallAPI(container *restful.Container) []string { oauthEtcd := oauthetcd.New(c.EtcdHelper) - authRequestHandler, authHandler, authFinalizer := c.getAuthorizeAuthenticationHandlers(mux) + authRequestHandler, authHandler, authFinalizer, err := c.getAuthorizeAuthenticationHandlers(mux) + if err != nil { + glog.Fatal(err) + } storage := registrystorage.New(oauthEtcd, oauthEtcd, oauthEtcd, registry.NewUserConversion()) config := osinserver.NewDefaultServerConfig() @@ -371,12 +379,21 @@ func (c *AuthConfig) getSessionAuth() *session.Authenticator { return c.sessionAuth } -func (c *AuthConfig) getAuthorizeAuthenticationHandlers(mux cmdutil.Mux) (authenticator.Request, handlers.AuthenticationHandler, osinserver.AuthorizeHandler) { - authRequestHandler := c.getAuthenticationRequestHandler() - authHandler := c.getAuthenticationHandler(mux, handlers.EmptyError{}) - authFinalizer := c.getAuthenticationFinalizer() +func (c *AuthConfig) getAuthorizeAuthenticationHandlers(mux cmdutil.Mux) (authenticator.Request, handlers.AuthenticationHandler, osinserver.AuthorizeHandler, error) { + authRequestHandler, err := c.getAuthenticationRequestHandler() + if err != nil { + return nil, nil, nil, err + } + authHandler, err := c.getAuthenticationHandler(mux, handlers.EmptyError{}) + if err != nil { + return nil, nil, nil, err + } + authFinalizer, err := c.getAuthenticationFinalizer() + if err != nil { + return nil, nil, nil, err + } - return authRequestHandler, authHandler, authFinalizer + return authRequestHandler, authHandler, authFinalizer, nil } // getGrantHandler returns the object that handles approving or rejecting grant requests @@ -399,7 +416,7 @@ func (c *AuthConfig) getGrantHandler(mux cmdutil.Mux, auth authenticator.Request } // getAuthenticationFinalizer returns an authentication finalizer which is called just prior to writing a response to an authorization request -func (c *AuthConfig) getAuthenticationFinalizer() osinserver.AuthorizeHandler { +func (c *AuthConfig) getAuthenticationFinalizer() (osinserver.AuthorizeHandler, error) { for _, requestHandler := range c.AuthRequestHandlers { switch requestHandler { case AuthRequestHandlerSession: @@ -407,17 +424,17 @@ func (c *AuthConfig) getAuthenticationFinalizer() osinserver.AuthorizeHandler { return osinserver.AuthorizeHandlerFunc(func(ar *osin.AuthorizeRequest, w http.ResponseWriter) (bool, error) { _ = c.getSessionAuth().InvalidateAuthentication(w, ar.HttpRequest) return false, nil - }) + }), nil } } // Otherwise return a no-op finalizer return osinserver.AuthorizeHandlerFunc(func(ar *osin.AuthorizeRequest, w http.ResponseWriter) (bool, error) { return false, nil - }) + }), nil } -func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, errorHandler handlers.AuthenticationErrorHandler) handlers.AuthenticationHandler { +func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, errorHandler handlers.AuthenticationErrorHandler) (handlers.AuthenticationHandler, error) { successHandler := c.getAuthenticationSuccessHandler() // TODO presumably we'll want either a list of what we've got or a way to describe a registry of these @@ -440,13 +457,16 @@ func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, errorHandler hand state := external.DefaultState() oauthHandler, err := external.NewExternalOAuthRedirector(oauthProvider, state, c.MasterPublicAddr+callbackPath, successHandler, errorHandler, identityMapper) if err != nil { - glog.Fatalf("unexpected error: %v", err) + return nil, fmt.Errorf("unexpected error: %v", err) } mux.Handle(callbackPath, oauthHandler) authHandler = handlers.NewUnionAuthenticationHandler(nil, map[string]handlers.AuthenticationRedirector{string(authHandlerType): oauthHandler}, errorHandler) case AuthHandlerLogin: - passwordAuth := c.getPasswordAuthenticator() + passwordAuth, err := c.getPasswordAuthenticator() + if err != nil { + return nil, err + } authHandler = handlers.NewUnionAuthenticationHandler( map[string]handlers.AuthenticationChallenger{"login": passwordchallenger.NewBasicAuthChallenger("openshift")}, map[string]handlers.AuthenticationRedirector{"login": &redirector{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"}}, @@ -457,13 +477,13 @@ func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, errorHandler hand case AuthHandlerDeny: authHandler = handlers.EmptyAuth{} default: - glog.Fatalf("No AuthenticationHandler found that matches %v. The oauth server cannot start!", authHandlerType) + return nil, fmt.Errorf("No AuthenticationHandler found that matches %v. The oauth server cannot start!", authHandlerType) } - return authHandler + return authHandler, nil } -func (c *AuthConfig) getPasswordAuthenticator() authenticator.Password { +func (c *AuthConfig) getPasswordAuthenticator() (authenticator.Password, error) { // TODO presumably we'll want either a list of what we've got or a way to describe a registry of these // hard-coded strings as a stand-in until it gets sorted out passwordAuthType := c.PasswordAuth @@ -475,7 +495,7 @@ func (c *AuthConfig) getPasswordAuthenticator() authenticator.Password { case PasswordAuthBasicAuthURL: basicAuthURL := c.BasicAuthURL if len(basicAuthURL) == 0 { - glog.Fatalf("BasicAuthURL is required to support basic password auth") + return nil, fmt.Errorf("BasicAuthURL is required to support basic password auth") } passwordAuth = basicauthpassword.New(basicAuthURL, identityMapper) case PasswordAuthAnyPassword: @@ -487,19 +507,19 @@ func (c *AuthConfig) getPasswordAuthenticator() authenticator.Password { case PasswordAuthHTPasswd: htpasswdFile := c.HTPasswdFile if len(htpasswdFile) == 0 { - glog.Fatalf("HTPasswdFile is required to support htpasswd auth") + return nil, fmt.Errorf("HTPasswdFile is required to support htpasswd auth") } if htpasswordAuth, err := htpasswd.New(htpasswdFile, identityMapper); err != nil { - glog.Fatalf("Error loading htpasswd file %s: %v", htpasswdFile, err) + return nil, fmt.Errorf("Error loading htpasswd file %s: %v", htpasswdFile, err) } else { passwordAuth = htpasswordAuth } default: - glog.Fatalf("No password auth found that matches %v. The oauth server cannot start!", passwordAuthType) + return nil, fmt.Errorf("No password auth found that matches %v. The oauth server cannot start!", passwordAuthType) } - return passwordAuth + return passwordAuth, nil } func (c *AuthConfig) getAuthenticationSuccessHandler() handlers.AuthenticationSuccessHandler { @@ -523,7 +543,7 @@ func (c *AuthConfig) getAuthenticationSuccessHandler() handlers.AuthenticationSu return successHandlers } -func (c *AuthConfig) getAuthenticationRequestHandlerFromType(authRequestHandlerType AuthRequestHandlerType) authenticator.Request { +func (c *AuthConfig) getAuthenticationRequestHandlerFromType(authRequestHandlerType AuthRequestHandlerType) (authenticator.Request, error) { // TODO presumably we'll want either a list of what we've got or a way to describe a registry of these // hard-coded strings as a stand-in until it gets sorted out var authRequestHandler authenticator.Request @@ -533,17 +553,17 @@ func (c *AuthConfig) getAuthenticationRequestHandlerFromType(authRequestHandlerT case TokenStoreOAuth: tokenAuthenticator, err := GetEtcdTokenAuthenticator(c.EtcdHelper) if err != nil { - glog.Fatalf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) + return nil, fmt.Errorf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) } authRequestHandler = bearertoken.New(tokenAuthenticator, true) case TokenStoreFile: tokenAuthenticator, err := GetCSVTokenAuthenticator(c.TokenFilePath) if err != nil { - glog.Fatalf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) + return nil, fmt.Errorf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) } authRequestHandler = bearertoken.New(tokenAuthenticator, true) default: - glog.Fatalf("Unknown TokenStore %s. Must be oauth or file. The oauth server cannot start!", c.TokenStore) + return nil, fmt.Errorf("Unknown TokenStore %s. Must be oauth or file. The oauth server cannot start!", c.TokenStore) } case AuthRequestHandlerRequestHeader: userRegistry := useretcd.New(c.EtcdHelper, user.NewDefaultUserInitStrategy()) @@ -552,28 +572,50 @@ func (c *AuthConfig) getAuthenticationRequestHandlerFromType(authRequestHandlerT UserNameHeaders: c.RequestHeaders, } authRequestHandler = headerrequest.NewAuthenticator(authRequestConfig, identityMapper) + + // Wrap with an x509 verifier + if len(c.RequestHeaderCAFile) > 0 { + caData, err := ioutil.ReadFile(c.RequestHeaderCAFile) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %v", c.RequestHeaderCAFile, err) + } + opts := x509request.DefaultVerifyOptions() + opts.Roots = x509.NewCertPool() + if ok := opts.Roots.AppendCertsFromPEM(caData); !ok { + return nil, fmt.Errorf("Error loading certs from %s: %v", c.RequestHeaderCAFile, err) + } + + authRequestHandler = x509request.NewVerifier(opts, authRequestHandler) + } case AuthRequestHandlerBasicAuth: - passwordAuthenticator := c.getPasswordAuthenticator() + passwordAuthenticator, err := c.getPasswordAuthenticator() + if err != nil { + return nil, err + } authRequestHandler = basicauthrequest.NewBasicAuthAuthentication(passwordAuthenticator, true) case AuthRequestHandlerSession: authRequestHandler = c.getSessionAuth() default: - glog.Fatalf("No AuthenticationRequestHandler found that matches %v. The oauth server cannot start!", authRequestHandlerType) + return nil, fmt.Errorf("No AuthenticationRequestHandler found that matches %v. The oauth server cannot start!", authRequestHandlerType) } - return authRequestHandler + return authRequestHandler, nil } -func (c *AuthConfig) getAuthenticationRequestHandler() authenticator.Request { +func (c *AuthConfig) getAuthenticationRequestHandler() (authenticator.Request, error) { // TODO presumably we'll want either a list of what we've got or a way to describe a registry of these // hard-coded strings as a stand-in until it gets sorted out var authRequestHandlers []authenticator.Request for _, requestHandler := range c.AuthRequestHandlers { - authRequestHandlers = append(authRequestHandlers, c.getAuthenticationRequestHandlerFromType(requestHandler)) + authRequestHandler, err := c.getAuthenticationRequestHandlerFromType(requestHandler) + if err != nil { + return nil, err + } + authRequestHandlers = append(authRequestHandlers, authRequestHandler) } authRequestHandler := unionrequest.NewUnionAuthentication(authRequestHandlers...) - return authRequestHandler + return authRequestHandler, nil } func GetEtcdTokenAuthenticator(etcdHelper tools.EtcdHelper) (authenticator.Token, error) { diff --git a/pkg/cmd/server/origin/config.go b/pkg/cmd/server/origin/config.go index c4a4a729c030..0d3981ed1867 100644 --- a/pkg/cmd/server/origin/config.go +++ b/pkg/cmd/server/origin/config.go @@ -65,9 +65,12 @@ type MasterConfigParameters struct { AssetCertFile string AssetKeyFile string - // ClientCAs will be used to request client certificates in connections to the API. - // This CertPool should contain all the CAs that will be used for client certificate verification. + // ClientCAs will be used to request client certificates in connections to the API or OAuth server. + // This CertPool should contain all the CAs that will be used for client certificate verification (the union + // of APIClientCAs and OAuthClientCAs). ClientCAs *x509.CertPool + // APIClientCAs is used to verify client certificates presented for API auth + APIClientCAs *x509.CertPool MasterAuthorizationNamespace string @@ -159,7 +162,7 @@ func (c MasterConfigParameters) newAuthenticator() authenticator.Request { // build cert authenticator // TODO: add cert users to etcd? opts := x509request.DefaultVerifyOptions() - opts.Roots = c.ClientCAs + opts.Roots = c.APIClientCAs certauth := x509request.New(opts, x509request.SubjectToUserConversion) authenticators = append(authenticators, certauth) } diff --git a/pkg/cmd/server/origin_master.go b/pkg/cmd/server/origin_master.go index 9714e6897d6b..3cb30647bb7e 100644 --- a/pkg/cmd/server/origin_master.go +++ b/pkg/cmd/server/origin_master.go @@ -3,6 +3,7 @@ package server import ( "crypto/x509" "fmt" + "io/ioutil" "net" "strings" "time" @@ -65,7 +66,11 @@ func (cfg Config) BuildOriginMasterConfig() (*origin.MasterConfig, error) { return nil, err } - clientCAs, err := cfg.GetClientCAs() + clientCAs, err := cfg.GetClientCertCAPool() + if err != nil { + return nil, err + } + apiClientCAs, err := cfg.GetAPIClientCertCAPool() if err != nil { return nil, err } @@ -104,6 +109,7 @@ func (cfg Config) BuildOriginMasterConfig() (*origin.MasterConfig, error) { AssetCertFile: assetCertFile, AssetKeyFile: assetKeyFile, ClientCAs: clientCAs, + APIClientCAs: apiClientCAs, KubeClient: kubeClient, KubeClientConfig: *kubeClientConfig, @@ -135,7 +141,7 @@ func (cfg Config) BuildAuthConfig() (*origin.AuthConfig, error) { return nil, err } - clientCAs, err := cfg.GetClientCAs() + apiServerCAs, err := cfg.GetAPIServerCertCAPool() if err != nil { return nil, err } @@ -159,7 +165,7 @@ func (cfg Config) BuildAuthConfig() (*origin.AuthConfig, error) { MasterAddr: masterAddr.String(), MasterPublicAddr: masterPublicAddr.String(), AssetPublicAddresses: assetPublicAddresses, - MasterRoots: clientCAs, + MasterRoots: apiServerCAs, EtcdHelper: etcdHelper, // Max token ages @@ -170,7 +176,8 @@ func (cfg Config) BuildAuthConfig() (*origin.AuthConfig, error) { AuthHandler: origin.AuthHandlerType(env("OPENSHIFT_OAUTH_HANDLER", string(origin.AuthHandlerLogin))), GrantHandler: origin.GrantHandlerType(env("OPENSHIFT_OAUTH_GRANT_HANDLER", string(origin.GrantHandlerAuto))), // RequestHeader config - RequestHeaders: strings.Split(env("OPENSHIFT_OAUTH_REQUEST_HEADERS", "X-Remote-User"), ","), + RequestHeaders: strings.Split(env("OPENSHIFT_OAUTH_REQUEST_HEADERS", "X-Remote-User"), ","), + RequestHeaderCAFile: GetOAuthRequestHeaderCAFile(), // Session config (default to unknowable secret) SessionSecrets: []string{env("OPENSHIFT_OAUTH_SESSION_SECRET", uuid.NewUUID().String())}, SessionMaxAgeSeconds: envInt("OPENSHIFT_OAUTH_SESSION_MAX_AGE_SECONDS", 300, 1), @@ -194,6 +201,10 @@ func (cfg Config) BuildAuthConfig() (*origin.AuthConfig, error) { } +func GetOAuthRequestHeaderCAFile() string { + return env("OPENSHIFT_OAUTH_REQUEST_HEADER_CA_FILE", "") +} + func (cfg Config) newCA() (*crypto.CA, error) { masterAddr, err := cfg.GetMasterAddress() if err != nil { @@ -210,21 +221,81 @@ func (cfg Config) newCA() (*crypto.CA, error) { return ca, nil } -func (cfg Config) GetClientCAs() (*x509.CertPool, error) { - ca, err := cfg.newCA() +// GetAPIClientCertCAPool returns the cert pool used to validate client certificates to the API server +func (cfg Config) GetAPIClientCertCAPool() (*x509.CertPool, error) { + certs, err := cfg.getAPIClientCertCAs() if err != nil { return nil, err } + roots := x509.NewCertPool() + for _, root := range certs { + roots.AddCert(root) + } + return roots, nil +} - // Save cert roots +// GetClientCertCAPool returns a cert pool containing all client CAs that could be presented (union of API and OAuth) +func (cfg Config) GetClientCertCAPool() (*x509.CertPool, error) { roots := x509.NewCertPool() - for _, root := range ca.Config.Roots { + + // Add CAs for OAuth + certs, err := cfg.getOAuthClientCertCAs() + if err != nil { + return nil, err + } + for _, root := range certs { + roots.AddCert(root) + } + + // Add CAs for API + certs, err = cfg.getAPIClientCertCAs() + if err != nil { + return nil, err + } + for _, root := range certs { roots.AddCert(root) } return roots, nil } +// GetAPIServerCertCAPool returns the cert pool containing the roots for the API server cert +func (cfg Config) GetAPIServerCertCAPool() (*x509.CertPool, error) { + ca, err := cfg.newCA() + if err != nil { + return nil, err + } + roots := x509.NewCertPool() + for _, root := range ca.Config.Roots { + roots.AddCert(root) + } + return roots, nil +} + +func (cfg Config) getOAuthClientCertCAs() ([]*x509.Certificate, error) { + caFile := GetOAuthRequestHeaderCAFile() + if len(caFile) == 0 { + return nil, nil + } + caPEMBlock, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + certs, err := crypto.CertsFromPEM(caPEMBlock) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %s", caFile, err) + } + return certs, nil +} + +func (cfg Config) getAPIClientCertCAs() ([]*x509.Certificate, error) { + ca, err := cfg.newCA() + if err != nil { + return nil, err + } + return ca.Config.Roots, nil +} + func (cfg Config) GetServerCertHostnames() ([]string, error) { masterAddr, err := cfg.GetMasterAddress() if err != nil { diff --git a/test/integration/oauth_request_header_test.go b/test/integration/oauth_request_header_test.go new file mode 100644 index 000000000000..74f45a8e4def --- /dev/null +++ b/test/integration/oauth_request_header_test.go @@ -0,0 +1,192 @@ +// +build integration,!no-etcd + +package integration + +import ( + "io/ioutil" + "net/http" + "os" + "regexp" + "testing" + + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + + "github.com/openshift/origin/pkg/client" +) + +var ( + rootCACert = []byte(`-----BEGIN CERTIFICATE----- +MIIC7DCCAdagAwIBAgIBATALBgkqhkiG9w0BAQswKDEmMCQGA1UEAwwdMTAuMTMu +MTI5LjE0OTo4NDQzQDE0MjU2NzUyNzQwIBcNMTUwMzA2MjA1NDM0WhgPMjExNTAy +MTAyMDU0MzVaMCgxJjAkBgNVBAMMHTEwLjEzLjEyOS4xNDk6ODQ0M0AxNDI1Njc1 +Mjc0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuOnLZ0PgeEKnbV7D +93g6fcllMh6ngCnQpEoaWSHTWjPbv/qDU/jRQU2l/KHOkMXKsbNiasRT6ZIWlUFc +W/Jgd1Tz7zjh+pgJHLEtKdWVPwP/8ruUhQotrb1E/q1g21wqczPxfb+Z9s6+AnkF +FLooBCCRa8wpC+TtcAaT7/yEJfN6IUhcT9XFmLzKTPz76UXBHMN+KDeK0k0u77a9 +vj+eAedB6Xg9lfpvIclvjgy6cvQ9oavYTJ8Q5mYZdIdspmSzFjAyZUylgpEIpPkN +e8dcqiA0hc2Mq/pwwn/F3i4va/NO7+Od9gRkAtvuvCUASXuCmon6pRYAZEImevRt +GbRlkQIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/BAUwAwEB/zAL +BgkqhkiG9w0BAQsDggEBAD30//8aJkPLtJ0G6/0oa+zjKBZH04PyWCjTsgaDCHVm +z/AntWxKR5fc+z/NXfnhV8M8/zb4ZGHp+jczozvcXZxUgftlUFNxV7sY8NXdJNrs +t+oFURLIibIjxN0vlz7py16RxXy693t6PzfQB/69ZB/AI3VfyOdJ1cvaV/kOce21 +Kp/jmVz5DUhQI60zcUOE4at81emo3uYK7Pz9iil2Wu2lK4+1uP4LdZRRLEUXWqNb +VmAB7OAhfJ2/x/BsPIvbI1aGp7DjjQgaeBwXD/mW8AUJHHbdvWUYz1yNyQ2XDWZm +X2kxcf0iGTwuqufTmw7EcDc/dWIdJ6bsB007/M9bz3g= +-----END CERTIFICATE----- +`) + + clientCert = []byte(`-----BEGIN CERTIFICATE----- +MIIC+DCCAeKgAwIBAgIBBTALBgkqhkiG9w0BAQswKDEmMCQGA1UEAwwdMTAuMTMu +MTI5LjE0OTo4NDQzQDE0MjU2NzUyNzQwIBcNMTUwMzA2MjA1NDM2WhgPMjExNTAy +MTAyMDU0MzdaMCIxIDAeBgNVBAMTF3N5c3RlbTpvcGVuc2hpZnQtY2xpZW50MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwbkwwrV4j3xqmUhyKErAzfAI +UX5atGGJHt+oRmZ3BzeAl6CpGLGLSiYso4j5JmLo0qpvQroSw66oOoVMw2851nhI +OZHo7aGvJ9elgmwa7ghDg4DN3TUe8y9Ex+JUDnAAK5dY0DV8UK7Aa2SAxIlSMGIu +VuUcjwlC9w37D3VxDFoa6XO+SUBiRUJyjiDlLNUegyV60jimxVTZbTb8r90lbGc8 +iB5j6py5ZCF/UMRY5LEuIum/7dKvH2A03q2n3Y58qcAhWIp6lP9DeJe3CMvuK64C +BwvT9jm9TioRGZskqfV3mLYyhxp1q2FKK03umQ5KKNYqvppFUYVKdzXgFjxfMQID +AQABozUwMzAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYD +VR0TAQH/BAIwADALBgkqhkiG9w0BAQsDggEBACY2Lu5kl9a2cBi3+WB/oo7iG82S +9IdIaABDLFWCGp4e0PEGAfROlNcCrhmEizkhPXeDNrjmDKeShnu1E/8RgwBtDrym +v9WQBa/HI3ZbO3hDdR2pNo6c+y3MqDJHO8/4l7hV5DYY9ypfB85mZQ7uxKaFawqs +GqLZNJWjpG6T9yUDaj2fO+etXb5dPTZiSytw1Z4l2GDGElLRjVS4k2aP0Lo/BLXG +1nUatU3KcCEeb1ifghFjDLESo8mUwfBl9v1vO75rIFRDfoPFMqIHGhRmP+fbnCI8 +fWh90BcEhK0TheTyEHBPtpKKiYz5BWyNlkCuTmhygaRCa9SQWl2nRi2XVGY= +-----END CERTIFICATE----- +`) + + clientKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwbkwwrV4j3xqmUhyKErAzfAIUX5atGGJHt+oRmZ3BzeAl6Cp +GLGLSiYso4j5JmLo0qpvQroSw66oOoVMw2851nhIOZHo7aGvJ9elgmwa7ghDg4DN +3TUe8y9Ex+JUDnAAK5dY0DV8UK7Aa2SAxIlSMGIuVuUcjwlC9w37D3VxDFoa6XO+ +SUBiRUJyjiDlLNUegyV60jimxVTZbTb8r90lbGc8iB5j6py5ZCF/UMRY5LEuIum/ +7dKvH2A03q2n3Y58qcAhWIp6lP9DeJe3CMvuK64CBwvT9jm9TioRGZskqfV3mLYy +hxp1q2FKK03umQ5KKNYqvppFUYVKdzXgFjxfMQIDAQABAoIBADg/olXWxUO8V2Nc +crEaS3NAT9oBuyqG636IaF7Qn5z7052zK4Yc/xmvjeSJ//XSYFHS5O1WA97Hltcv +H0PbxspsMGRu5lghSy9hYRBGfWdCBQBo5N1m8C6iOfFj2Q48HQCLOGF0Nj1jEEHe +c7kdOj0MNPJMIgeyI7yCVbR+YC26dfxiaIfRtyzsScsNX/pP1AH9lEd9c6reMvms +UxjplUkYjk4gbngmKJjd2MD8dc8XqR5V+Oq1uwOQG/EZhSBlyxwRVMFwd5/opb2Q +JqMZvd458MQ2C2RSZALXDYYmCMbXU76Crg7N3+y34d+uwkIufUhFfTR7BuUkvlzt +5vb+WTECgYEAyVCSxvTbB9y3IQpsRlJeBwVnWNHclZft4g9PtqQgA6VaWqSoYvFz +t2m2/L3O39zEgalM6HesVT8EdiIWvp08eHYvFTb9jaqxHIbwdrzxtvBn5SJ6CjCX +xA+uWv3AbH+H2t/ZCiPAKebcOfefmce6/8cKrNKFXs5KojR6tpM2Lo0CgYEA9li3 +JDTRbCGLsuyVhDT3l4przzJC0DwU1j8zBJfUrBtuPMhDISHHvq9opSoTQsRXz9EH +ruQe3/XCvE/y988E3oVh+ikmBX01xCsIUB0jUhItVQ7GSacY0+UgZmH6Zw7xJjO5 +zwaYGnejOxHIs0XmeajbhYl+bAGEym+iV682rDUCgYEAn0ox2VtFNCNgg7RLmBj0 +bXnJHG5xq6xbfdO/rzSOYFQl+jLvSdrjRO1Q7QsC9f8pPa9IO2j14z3Jue+fL5Qa +lPZuqsqoNcAqA/iBrHI0kBwJGTT+e7GXZHtD6pt99luyk20rvuoq0vzopLVag8OW +I2zK9ZReE3YHd/EuZ+hzpsECgYBiTXSHliwboidE9vOTFi/W4P20aLIQtmj6Na3+ +HzhWlXuf9aoUBo7WoNh5UBjvg7omy5rtR0qqxD85Ng4WpR2kTkWStejeN+DErwda +MMZvcaF1V7f4nB1kMQKE2IQ7q9K/E9UJr+/yX9tbLvWP1EzsL12qI/u2zcRXo8R8 +iQagIQKBgQDA4Ag/ShDEb0x6rFFJwMaHUT/TT8Yv8Ul7paIcAX6c/1jwgEh9T20F +6UWl+OcIpJp7DYNNLB/hesYqs76QvZDd9nIiW1bzuC0LHuSzyEoe234gpAfrRfYs +qLwYJxjzwYTLvLYPU5vHmdg8v5wIXh0TaRTDTdKViISGD09aiXSYzw== +-----END RSA PRIVATE KEY----- +`) +) + +func TestOAuthRequestHeader(t *testing.T) { + // Write cert we're going to use to verify OAuth requestheader requests + caFile, err := ioutil.TempFile("", "test.crt") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(caFile.Name()) + if err := ioutil.WriteFile(caFile.Name(), rootCACert, os.FileMode(0600)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // This is all that should be needed to enable auth-proxy-based auth, with cert verification + // If these change, need to update documentation at http://docs.openshift.org/latest/architecture/authentication.html + os.Setenv("OPENSHIFT_OAUTH_REQUEST_HANDLERS", "requestheader") + os.Setenv("OPENSHIFT_OAUTH_REQUEST_HEADERS", "My-Remote-User,SSO-User") + os.Setenv("OPENSHIFT_OAUTH_HANDLER", "deny") + os.Setenv("OPENSHIFT_OAUTH_REQUEST_HEADER_CA_FILE", caFile.Name()) + + // Start server + startConfig, err := StartTestMaster() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, clientConfig, err := startConfig.GetOpenshiftClient() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Use the server and CA info, but no client cert info + anonConfig := kclient.Config{} + anonConfig.Host = clientConfig.Host + anonConfig.CAFile = clientConfig.CAFile + anonConfig.CAData = clientConfig.CAData + + // Build the authorize request with the My-Remote-User header + authorizeURL := clientConfig.Host + "/oauth/authorize?client_id=openshift-challenging-client&response_type=token" + req, err := http.NewRequest("GET", authorizeURL, nil) + req.Header.Set("My-Remote-User", "myuser") + + // Make the request without cert auth + transport, err := kclient.TransportFor(&anonConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resp, err := transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + redirect, err := resp.Location() + if err != nil { + t.Fatalf("expected 302 redirect, got error: %v", err) + } + if redirect.Query().Get("error") == "" { + t.Fatalf("expected unsuccessful token request, got redirected to %v", redirect.String()) + } + + // Use the server and CA info, with cert info + authProxyConfig := anonConfig + authProxyConfig.CertData = clientCert + authProxyConfig.KeyData = clientKey + + // Make the request with cert info + transport, err = kclient.TransportFor(&authProxyConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resp, err = transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + redirect, err = resp.Location() + if err != nil { + t.Fatalf("expected 302 redirect, got error: %v", err) + } + if redirect.Query().Get("error") != "" { + t.Fatalf("expected successful token request, got error %v", redirect.String()) + } + + // Extract the access_token + + // group #0 is everything. #1 #2 #3 + accessTokenRedirectRegex := regexp.MustCompile(`(^|&)access_token=([^&]+)($|&)`) + accessToken := "" + if matches := accessTokenRedirectRegex.FindStringSubmatch(redirect.Fragment); matches != nil { + accessToken = matches[2] + } + if accessToken == "" { + t.Fatalf("Expected access token, got %s", redirect.String()) + } + + // Make sure we can use the token, and it represents who we expect + userConfig := anonConfig + userConfig.BearerToken = accessToken + userClient, err := client.New(&userConfig) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + user, err := userClient.Users().Get("~") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if user.Name != "requestheader:myuser" { + t.Fatalf("Expected requestheader:myuser as the user, got %v", user) + } +}