Add support for HTTP basic auth to the kube-apiserver.

This commit is contained in:
Robert Bailey 2015-04-06 16:34:42 -07:00
parent 4c0c04f247
commit 6d85dcb4a0
5 changed files with 239 additions and 8 deletions

View File

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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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())
}