mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
Add support for HTTP basic auth to the kube-apiserver.
This commit is contained in:
parent
4c0c04f247
commit
6d85dcb4a0
@ -63,6 +63,7 @@ type APIServer struct {
|
|||||||
CloudProvider string
|
CloudProvider string
|
||||||
CloudConfigFile string
|
CloudConfigFile string
|
||||||
EventTTL time.Duration
|
EventTTL time.Duration
|
||||||
|
BasicAuthFile string
|
||||||
ClientCAFile string
|
ClientCAFile string
|
||||||
TokenAuthFile string
|
TokenAuthFile string
|
||||||
AuthorizationMode 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.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.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.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.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.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.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)
|
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 {
|
if err != nil {
|
||||||
glog.Fatalf("Invalid Authentication Config: %v", err)
|
glog.Fatalf("Invalid Authentication Config: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Authentication Plugins
|
# 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`
|
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
|
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/...`
|
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.
|
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
|
## Plugin Development
|
||||||
|
|
||||||
We plan for the Kubernetes API server to issue tokens
|
We plan for the Kubernetes API server to issue tokens
|
||||||
|
@ -20,14 +20,24 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"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/union"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/x509"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/x509"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAuthenticator returns an authenticator.Request or an error
|
// NewAuthenticator returns an authenticator.Request or an error
|
||||||
func NewAuthenticator(clientCAFile string, tokenFile string) (authenticator.Request, error) {
|
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) {
|
||||||
authenticators := []authenticator.Request{}
|
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 {
|
if len(clientCAFile) > 0 {
|
||||||
certAuth, err := newAuthenticatorFromClientCAFile(clientCAFile)
|
certAuth, err := newAuthenticatorFromClientCAFile(clientCAFile)
|
||||||
@ -45,14 +55,24 @@ func NewAuthenticator(clientCAFile string, tokenFile string) (authenticator.Requ
|
|||||||
authenticators = append(authenticators, tokenAuth)
|
authenticators = append(authenticators, tokenAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(authenticators) == 0 {
|
switch len(authenticators) {
|
||||||
|
case 0:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
case 1:
|
||||||
if len(authenticators) == 1 {
|
|
||||||
return authenticators[0], nil
|
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
|
// newAuthenticatorFromTokenFile returns an authenticator.Request or an error
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user