Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenStack Keystone Token Authentication #25391

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/kube-apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ func Run(s *options.APIServer) error {
ServiceAccountLookup: s.ServiceAccountLookup,
ServiceAccountTokenGetter: serviceAccountGetter,
KeystoneURL: s.KeystoneURL,
KeystoneConfig: s.KeystoneConfig,
KeystoneAuthMode: s.KeystoneAuthMode,
WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile,
WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL,
})
Expand All @@ -206,6 +208,10 @@ func Run(s *options.APIServer) error {
glog.Fatalf("Invalid Authentication Config: %v", err)
}

if len(s.KeystoneConfig) > 0 && (s.KeystoneAuthMode != "token" && s.KeystoneAuthMode != "password") {
glog.Fatalf("Invalid KeystoneAuthorization mode=%s. It must be either token or password", s.KeystoneAuthMode)
}

authorizationModeNames := strings.Split(s.AuthorizationMode, ",")

modeEnabled := func(mode string) bool {
Expand Down
7 changes: 2 additions & 5 deletions docs/admin/kube-apiserver.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.

<!-- TAG RELEASE_LINK, added by the munger automatically -->
<strong>
The latest release of this document can be found
[here](http://releases.k8s.io/release-1.2/docs/admin/kube-apiserver.md).

Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
Expand Down Expand Up @@ -82,6 +77,8 @@ kube-apiserver
--etcd-servers=[]: List of etcd servers to connect with (http://ip:port), comma separated.
--etcd-servers-overrides=[]: Per-resource etcd servers overrides, comma separated. The individual override format: group/resource#servers, where servers are http://ip:port, semicolon separated.
--event-ttl=1h0m0s: Amount of time to retain events. Default 1 hour.
--experimental-keystone-auth-mode="": If passed, selects between token and password modes
--experimental-keystone-config="": If passed, activates the keystone authentication plugin
--experimental-keystone-url="": If passed, activates the keystone authentication plugin
--external-hostname="": The hostname to use when generating externalized URLs for this master (e.g. Swagger API Docs.)
--google-json-key="": The Google Cloud Platform Service Account JSON Key to use for authentication.
Expand Down
1 change: 1 addition & 0 deletions federation/cmd/federation-apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func Run(s *genericoptions.ServerRunOptions) error {
OIDCUsernameClaim: s.OIDCUsernameClaim,
OIDCGroupsClaim: s.OIDCGroupsClaim,
KeystoneURL: s.KeystoneURL,
KeystoneConfig: s.KeystoneConfig,
})
if err != nil {
glog.Fatalf("Invalid Authentication Config: %v", err)
Expand Down
2 changes: 2 additions & 0 deletions hack/verify-flags/known-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ executor-path
executor-suicide-timeout
experimental-flannel-overlay
experimental-keystone-url
experimental-keystone-auth-mode
experimental-keystone-config
experimental-nvidia-gpus
experimental-prefix
external-hostname
Expand Down
42 changes: 31 additions & 11 deletions pkg/apiserver/authenticator/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import (
"k8s.io/kubernetes/pkg/auth/authenticator/bearertoken"
"k8s.io/kubernetes/pkg/serviceaccount"
"k8s.io/kubernetes/pkg/util/crypto"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/keystone"
keystonePassword "k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/keystone"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/union"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509"
keystoneToken "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/keystone"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/webhook"
Expand All @@ -46,9 +47,11 @@ type AuthenticatorConfig struct {
ServiceAccountKeyFile string
ServiceAccountLookup bool
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
KeystoneURL string
WebhookTokenAuthnConfigFile string
WebhookTokenAuthnCacheTTL time.Duration
KeystoneURL string
KeystoneConfig string
KeystoneAuthMode string
}

// New returns an authenticator.Request or an error that supports the standard
Expand Down Expand Up @@ -96,12 +99,20 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) {
authenticators = append(authenticators, serviceAccountAuth)
}

if len(config.KeystoneURL) > 0 {
keystoneAuth, err := newAuthenticatorFromKeystoneURL(config.KeystoneURL)
if err != nil {
return nil, err
if len(config.KeystoneConfig) > 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why password and token auth are mutually exclusive here... I'd sort of expect to be able to set up keystone basic auth (with just the URL, as you can today?) and/or keystone token auth (by providing a config file like this?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that originally, but it would try and authenticate to both, not just one, which always broke. If there is a way to enable them both at the same time, that would be great. Maybe I missed something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'll try each in turn until one succeeds... if a bearer token is passed, the token auth would get called, if basic auth is passed, the password one would get called.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. but it wasn't doing that. At the time, I think I had both the token plugin and the basicauth plugin combined into one plugin with both types of validators in it. Maybe that was the problem?

if config.KeystoneAuthMode == "token" {
keystoneTokenAuth, err := newTokenAuthenticatorFromKeystoneConfig(config.KeystoneConfig)
if err != nil {
return nil, err
}
authenticators = append(authenticators, keystoneTokenAuth)
} else if config.KeystoneAuthMode == "password" {
keystonePasswordAuth, err := newPasswordAuthenticatorFromKeystoneURL(config.KeystoneURL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is weird... you have set KeystoneConfig to a non-empty value to get here, but then it is ignored and KeystoneURL is used instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your right. This is a left over from when I tried to unify the two configs but then was asked to not do that in this patchset. I'll fix it.

if err != nil {
return nil, err
}
authenticators = append(authenticators, keystonePasswordAuth)
}
authenticators = append(authenticators, keystoneAuth)
}

if len(config.WebhookTokenAuthnConfigFile) > 0 {
Expand Down Expand Up @@ -190,14 +201,23 @@ func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Reques
return x509.New(opts, x509.CommonNameUserConversion), nil
}

// newAuthenticatorFromTokenFile returns an authenticator.Request or an error
func newAuthenticatorFromKeystoneURL(keystoneURL string) (authenticator.Request, error) {
keystoneAuthenticator, err := keystone.NewKeystoneAuthenticator(keystoneURL)
// newPasswordAuthenticatorFromTokenFile returns an authenticator.Request or an error
func newPasswordAuthenticatorFromKeystoneURL(keystoneURL string) (authenticator.Request, error) {
keystonePasswordAuthenticator, err := keystonePassword.NewKeystoneAuthenticator(keystoneURL)
if err != nil {
return nil, err
}
return basicauth.New(keystonePasswordAuthenticator), nil
}

// newTokenAuthenticatorFromTokenFile returns an authenticator.Request or an error
func newTokenAuthenticatorFromKeystoneConfig(keystoneConfigFile string) (authenticator.Request, error) {
keystoneTokenAuthenticator, err := keystoneToken.NewKeystoneAuthenticator(keystoneConfigFile)
if err != nil {
return nil, err
}

return basicauth.New(keystoneAuthenticator), nil
return bearertoken.New(keystoneTokenAuthenticator), nil
}

func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration) (authenticator.Request, error) {
Expand Down
54 changes: 17 additions & 37 deletions pkg/cloudprovider/providers/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/cloudprovider"
openstackutil "k8s.io/kubernetes/pkg/util/openstack"
)

const ProviderName = "openstack"
Expand Down Expand Up @@ -101,48 +102,32 @@ type OpenStack struct {
}

type Config struct {
Global struct {
AuthUrl string `gcfg:"auth-url"`
Username string
UserId string `gcfg:"user-id"`
Password string
ApiKey string `gcfg:"api-key"`
TenantId string `gcfg:"tenant-id"`
TenantName string `gcfg:"tenant-name"`
DomainId string `gcfg:"domain-id"`
DomainName string `gcfg:"domain-name"`
Region string
}
Global openstackutil.KeystoneAuthOpts `gcfg:"Global"`
LoadBalancer LoadBalancerOpts
}

func ConfigToProvider(config io.Reader) (Config, *gophercloud.ProviderClient, error) {
cfg, err := readConfig(config)
if err != nil {
return cfg, nil, err
}
provider, err := openstack.AuthenticatedClient(cfg.Global.ToAuthOptions())
if err != nil {
return cfg, nil, err
}
return cfg, provider, nil
}

func init() {
cloudprovider.RegisterCloudProvider(ProviderName, func(config io.Reader) (cloudprovider.Interface, error) {
cfg, err := readConfig(config)
cfg, provider, err := ConfigToProvider(config)
if err != nil {
return nil, err
}
return newOpenStack(cfg)
return newOpenStack(cfg, provider)
})
}

func (cfg Config) toAuthOptions() gophercloud.AuthOptions {
return gophercloud.AuthOptions{
IdentityEndpoint: cfg.Global.AuthUrl,
Username: cfg.Global.Username,
UserID: cfg.Global.UserId,
Password: cfg.Global.Password,
APIKey: cfg.Global.ApiKey,
TenantID: cfg.Global.TenantId,
TenantName: cfg.Global.TenantName,
DomainID: cfg.Global.DomainId,
DomainName: cfg.Global.DomainName,

// Persistent service, so we need to be able to renew tokens.
AllowReauth: true,
}
}

func readConfig(config io.Reader) (Config, error) {
if config == nil {
err := fmt.Errorf("no OpenStack cloud provider config file given")
Expand Down Expand Up @@ -219,12 +204,7 @@ func readInstanceID() (string, error) {
return instanceID, nil
}

func newOpenStack(cfg Config) (*OpenStack, error) {
provider, err := openstack.AuthenticatedClient(cfg.toAuthOptions())
if err != nil {
return nil, err
}

func newOpenStack(cfg Config, provider *gophercloud.ProviderClient) (*OpenStack, error) {
id, err := readInstanceID()
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions pkg/genericapiserver/options/server_run_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type ServerRunOptions struct {
InsecureBindAddress net.IP
InsecurePort int
KeystoneURL string
KeystoneConfig string
KeystoneAuthMode string
KubernetesServiceNodePort int
LongRunningRequestRE string
MasterCount int
Expand Down Expand Up @@ -278,6 +280,8 @@ func (s *ServerRunOptions) AddFlags(fs *pflag.FlagSet) {
fs.MarkDeprecated("port", "see --insecure-port instead")

fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")
fs.StringVar(&s.KeystoneConfig, "experimental-keystone-config", s.KeystoneConfig, "If passed, activates the keystone authentication plugin")
fs.StringVar(&s.KeystoneAuthMode, "experimental-keystone-auth-mode", s.KeystoneAuthMode, "If passed, selects between token and password modes")

// See #14282 for details on how to test/try this option out. TODO remove this comment once this option is tested in CI.
fs.IntVar(&s.KubernetesServiceNodePort, "kubernetes-service-node-port", s.KubernetesServiceNodePort, "If non-zero, the Kubernetes master service (which apiserver creates/maintains) will be of type NodePort, using this as the value of the port. If zero, the Kubernetes master service will be of type ClusterIP.")
Expand Down
109 changes: 109 additions & 0 deletions pkg/util/openstack/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2016 The Kubernetes Authors 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 openstack

import (
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/golang/glog"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"gopkg.in/gcfg.v1"
)

type KeystoneAuthOpts struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this package seems like a weird place for this, but I'm not sure where a better place would be. does this map to a config file or struct in keystone, or is it something made up for Kubernetes' use?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was pulled out from existing code in Kubernetes that authenticated with OpenStack to do stuff like volume and load balancer management and made generic enough that it can be shared between the CloudProviders use case and the Keystone Authn/z plugin. The more specific code was in pkg/cloudprovider/providers/openstack/openstack.go

AuthUrl string `gcfg:"auth-url"`
Username string
UserId string `gcfg:"user-id"`
Password string
ApiKey string `gcfg:"api-key"`
TenantId string `gcfg:"tenant-id"`
TenantName string `gcfg:"tenant-name"`
DomainId string `gcfg:"domain-id"`
DomainName string `gcfg:"domain-name"`
Region string
}

type Config struct {
Global KeystoneAuthOpts `gcfg:"Global"`
}

func (cfg KeystoneAuthOpts) ToAuthOptions() gophercloud.AuthOptions {
return gophercloud.AuthOptions{
IdentityEndpoint: cfg.AuthUrl,
Username: cfg.Username,
UserID: cfg.UserId,
Password: cfg.Password,
APIKey: cfg.ApiKey,
TenantID: cfg.TenantId,
TenantName: cfg.TenantName,
DomainID: cfg.DomainId,
DomainName: cfg.DomainName,

// Persistent service, so we need to be able to renew tokens.
AllowReauth: true,
}
}

func ReadConfig(config io.Reader) (Config, error) {
if config == nil {
err := fmt.Errorf("no OpenStack config file given")
return Config{}, err
}

var cfg Config
err := gcfg.ReadInto(&cfg, config)
return cfg, err
}

func ConfigToProvider(config io.Reader) (Config, *gophercloud.ProviderClient, error) {
cfg, err := ReadConfig(config)
if err != nil {
return cfg, nil, err
}
provider, err := openstack.AuthenticatedClient(cfg.Global.ToAuthOptions())
if err != nil {
return cfg, nil, err
}
return cfg, provider, nil
}

func ConfigFileToProvider(configPath string) (Config, *gophercloud.ProviderClient, error) {
var cf *os.File
var err error
cf, err = os.Open(configPath)
if err != nil {
glog.Fatalf("Couldn't open configuration %s: %#v",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a util method shouldn't be making decisions about exiting the process

configPath, err)
}
defer cf.Close()
cfg, provider, err := ConfigToProvider(cf)
if err != nil {
return cfg, nil, err
}
if !strings.HasPrefix(cfg.Global.AuthUrl, "https") {
return cfg, nil, errors.New("Auth URL should be secure and start with https")
}
if cfg.Global.AuthUrl == "" {
return cfg, nil, errors.New("Auth URL is empty")
}
return cfg, provider, nil
}
Loading