diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 02831b96c66..a216581a7dd 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -63,6 +63,7 @@ type APIServer struct { CloudProvider string CloudConfigFile string EventTTL time.Duration + BasicAuthFile string ClientCAFile string TokenAuthFile string AuthorizationMode string @@ -155,6 +156,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.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.") 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, ",")) @@ -242,7 +244,7 @@ func (s *APIServer) Run(_ []string) error { n := net.IPNet(s.PortalNet) - authenticator, err := apiserver.NewAuthenticator(s.ClientCAFile, s.TokenAuthFile) + authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile) if err != nil { glog.Fatalf("Invalid Authentication Config: %v", err) } diff --git a/docs/authentication.md b/docs/authentication.md index dd90f89728e..65ee07d0de5 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,6 +1,6 @@ # Authentication Plugins -Kubernetes uses tokens or client certificates to authenticate users for API calls. +Kubernetes uses client certificates, tokens, or http basic auth to authenticate users for API calls. 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 @@ -16,6 +16,16 @@ be short-lived, and to be generated as needed rather than stored in a file. The token file format is implemented in `plugin/pkg/auth/authenticator/token/tokenfile/...` and is a csv file with 3 columns: token, user name, user uid. +Basic authentication is enabled by passing the `--basic_auth_file=SOMEFILE` +option to apiserver. Currently, the basic auth credentials last indefinitely, +and the password cannot be changed without restarting apiserver. Note that basic +authentication is currently supported for convenience while we finish making the +more secure modes described above easier to use. + +The basic auth file format is implemented in `plugin/pkg/auth/authenticator/password/passwordfile/...` +and is a csv file with 3 columns: password, user name, user id. + + ## Plugin Development We plan for the Kubernetes API server to issue tokens diff --git a/pkg/apiserver/authn.go b/pkg/apiserver/authn.go index 9b516c9c4b9..66504307b21 100644 --- a/pkg/apiserver/authn.go +++ b/pkg/apiserver/authn.go @@ -20,14 +20,24 @@ 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/password/passwordfile" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth" "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" ) // NewAuthenticator returns an authenticator.Request or an error -func NewAuthenticator(clientCAFile string, tokenFile string) (authenticator.Request, error) { - authenticators := []authenticator.Request{} +func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) { + var authenticators []authenticator.Request + + if len(basicAuthFile) > 0 { + basicAuth, err := newAuthenticatorFromBasicAuthFile(basicAuthFile) + if err != nil { + return nil, err + } + authenticators = append(authenticators, basicAuth) + } if len(clientCAFile) > 0 { certAuth, err := newAuthenticatorFromClientCAFile(clientCAFile) @@ -45,14 +55,24 @@ func NewAuthenticator(clientCAFile string, tokenFile string) (authenticator.Requ authenticators = append(authenticators, tokenAuth) } - if len(authenticators) == 0 { + switch len(authenticators) { + case 0: return nil, nil - } - if len(authenticators) == 1 { + case 1: return authenticators[0], nil + default: + return union.New(authenticators...), nil } - return union.New(authenticators...), nil +} +// newAuthenticatorFromBasicAuthFile returns an authenticator.Request or an error +func newAuthenticatorFromBasicAuthFile(basicAuthFile string) (authenticator.Request, error) { + basicAuthenticator, err := passwordfile.NewCSV(basicAuthFile) + if err != nil { + return nil, err + } + + return basicauth.New(basicAuthenticator), nil } // newAuthenticatorFromTokenFile returns an authenticator.Request or an error diff --git a/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile.go b/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile.go new file mode 100644 index 00000000000..1fc7aab8320 --- /dev/null +++ b/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile.go @@ -0,0 +1,78 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package passwordfile + +import ( + "encoding/csv" + "fmt" + "io" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +type PasswordAuthenticator struct { + users map[string]*userPasswordInfo +} + +type userPasswordInfo struct { + info *user.DefaultInfo + password string +} + +// NewCSV returns a PasswordAuthenticator, populated from a CSV file. +// The CSV file must contain records in the format "password,username,useruid" +func NewCSV(path string) (*PasswordAuthenticator, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + users := make(map[string]*userPasswordInfo) + reader := csv.NewReader(file) + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if len(record) < 3 { + return nil, fmt.Errorf("password file '%s' must have at least 3 columns (password, user name, user uid), found %d", path, len(record)) + } + obj := &userPasswordInfo{ + info: &user.DefaultInfo{Name: record[1], UID: record[2]}, + password: record[0], + } + users[obj.info.Name] = obj + } + + return &PasswordAuthenticator{users}, nil +} + +func (a *PasswordAuthenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) { + user, ok := a.users[username] + if !ok { + return nil, false, nil + } + if user.password != password { + return nil, false, nil + } + return user.info, true, nil +} diff --git a/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile_test.go b/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile_test.go new file mode 100644 index 00000000000..2dfefa07b54 --- /dev/null +++ b/plugin/pkg/auth/authenticator/password/passwordfile/passwordfile_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package passwordfile + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +func TestPasswordFile(t *testing.T) { + auth, err := newWithContents(t, ` +password1,user1,uid1 +password2,user2,uid2 +`) + if err != nil { + t.Fatalf("unable to read passwordfile: %v", err) + } + + testCases := []struct { + Username string + Password string + User *user.DefaultInfo + Ok bool + Err bool + }{ + { + Username: "user1", + Password: "password1", + User: &user.DefaultInfo{Name: "user1", UID: "uid1"}, + Ok: true, + }, + { + Username: "user2", + Password: "password2", + User: &user.DefaultInfo{Name: "user2", UID: "uid2"}, + Ok: true, + }, + { + Username: "user1", + Password: "password2", + }, + { + Username: "user2", + Password: "password1", + }, + { + Username: "user3", + Password: "password3", + }, + { + Username: "user4", + Password: "password4", + }, + } + for i, testCase := range testCases { + user, ok, err := auth.AuthenticatePassword(testCase.Username, testCase.Password) + if err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + } + if testCase.User == nil { + if user != nil { + t.Errorf("%d: unexpected non-nil user %#v", i, user) + } + } else if !reflect.DeepEqual(testCase.User, user) { + t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, user) + } + if testCase.Ok != ok { + t.Errorf("%d: expected auth %v, got %v", i, testCase.Ok, ok) + } + } +} + +func TestBadPasswordFile(t *testing.T) { + if _, err := newWithContents(t, ` +password1,user1,uid1 +password2,user2,uid2 +password3,user3 +password4 +`); err == nil { + t.Fatalf("unexpected non error") + } +} + +func TestInsufficientColumnsPasswordFile(t *testing.T) { + if _, err := newWithContents(t, "password4\n"); err == nil { + t.Fatalf("unexpected non error") + } +} + +func newWithContents(t *testing.T, contents string) (auth *PasswordAuthenticator, err error) { + f, err := ioutil.TempFile("", "passwordfile_test") + if err != nil { + t.Fatalf("unexpected error creating passwordfile: %v", err) + } + f.Close() + defer os.Remove(f.Name()) + + if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil { + t.Fatalf("unexpected error writing passwordfile: %v", err) + } + + return NewCSV(f.Name()) +}