From 83768d286ce94e21c913e8be7dd4ad353cc16c21 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Mon, 14 May 2018 13:01:42 -0700 Subject: [PATCH] vSphere Cloud Provider: add SAML token authentication support For now the config structs and validation are left as-is and the LoginByToken method is used if the username value is PEM encoded. In this case of username field configured with the public key, the password field is expected to be configured with the private key. In a follow-up PR we can look at collapsing the auth related fields into a common struct to avoid duplication of field merging and validation. And then add separate fields for the public and private keys. Fixes #63209 --- .../providers/vsphere/vclib/connection.go | 57 +++++++++++- .../providers/vsphere/vsphere_test.go | 86 +++++++++++++++++-- 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/pkg/cloudprovider/providers/vsphere/vclib/connection.go b/pkg/cloudprovider/providers/vsphere/vclib/connection.go index 6181eba3f72..b49405db517 100644 --- a/pkg/cloudprovider/providers/vsphere/vclib/connection.go +++ b/pkg/cloudprovider/providers/vsphere/vclib/connection.go @@ -18,12 +18,15 @@ package vclib import ( "context" + "crypto/tls" + "encoding/pem" "net" neturl "net/url" "sync" "github.com/golang/glog" "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/sts" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/soap" ) @@ -78,6 +81,49 @@ func (connection *VSphereConnection) Connect(ctx context.Context) error { return nil } +// login calls SessionManager.LoginByToken if certificate and private key are configured, +// otherwise calls SessionManager.Login with user and password. +func (connection *VSphereConnection) login(ctx context.Context, client *vim25.Client) error { + m := session.NewManager(client) + + // TODO: Add separate fields for certificate and private-key. + // For now we can leave the config structs and validation as-is and + // decide to use LoginByToken if the username value is PEM encoded. + b, _ := pem.Decode([]byte(connection.Username)) + if b == nil { + glog.V(3).Infof("SessionManager.Login with username '%s'", connection.Username) + return m.Login(ctx, neturl.UserPassword(connection.Username, connection.Password)) + } + + glog.V(3).Infof("SessionManager.LoginByToken with certificate '%s'", connection.Username) + + cert, err := tls.X509KeyPair([]byte(connection.Username), []byte(connection.Password)) + if err != nil { + glog.Errorf("Failed to load X509 key pair. err: %+v", err) + return err + } + + tokens, err := sts.NewClient(ctx, client) + if err != nil { + glog.Errorf("Failed to create STS client. err: %+v", err) + return err + } + + req := sts.TokenRequest{ + Certificate: &cert, + } + + signer, err := tokens.Issue(ctx, req) + if err != nil { + glog.Errorf("Failed to issue SAML token. err: %+v", err) + return err + } + + header := soap.Header{Security: signer} + + return m.LoginByToken(client.WithHeader(ctx, header)) +} + // Logout calls SessionManager.Logout for the given connection. func (connection *VSphereConnection) Logout(ctx context.Context) { m := session.NewManager(connection.Client) @@ -100,13 +146,16 @@ func (connection *VSphereConnection) NewClient(ctx context.Context) (*vim25.Clie glog.Errorf("Failed to create new client. err: %+v", err) return nil, err } - - m := session.NewManager(client) - - err = m.Login(ctx, neturl.UserPassword(connection.Username, connection.Password)) + err = connection.login(ctx, client) if err != nil { return nil, err } + if glog.V(3) { + s, err := session.NewManager(client).UserSession(ctx) + if err == nil { + glog.Infof("New session ID for '%s' = %s", s.UserName, s.Key) + } + } if connection.RoundTripperCount == 0 { connection.RoundTripperCount = RoundTripperDefaultCount diff --git a/pkg/cloudprovider/providers/vsphere/vsphere_test.go b/pkg/cloudprovider/providers/vsphere/vsphere_test.go index 35e1903aea5..ee909f628b1 100644 --- a/pkg/cloudprovider/providers/vsphere/vsphere_test.go +++ b/pkg/cloudprovider/providers/vsphere/vsphere_test.go @@ -25,13 +25,41 @@ import ( "strings" "testing" + lookup "github.com/vmware/govmomi/lookup/simulator" "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/simulator/vpx" + sts "github.com/vmware/govmomi/sts/simulator" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/cloudprovider/providers/vsphere/vclib" ) +// localhostCert was generated from crypto/tls/generate_cert.go with the following command: +// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = `-----BEGIN CERTIFICATE----- +MIIBjzCCATmgAwIBAgIRAKpi2WmTcFrVjxrl5n5YDUEwDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC +QQC9fEbRszP3t14Gr4oahV7zFObBI4TfA5i7YnlMXeLinb7MnvT4bkfOJzE6zktn +59zP7UiHs3l4YOuqrjiwM413AgMBAAGjaDBmMA4GA1UdDwEB/wQEAwICpDATBgNV +HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQnMCWCC2V4 +YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUA +A0EAUsVE6KMnza/ZbodLlyeMzdo7EM/5nb5ywyOxgIOCf0OOLHsPS9ueGLQX9HEG +//yjTXuhNcUugExIjM/AIwAZPQ== +-----END CERTIFICATE-----` + +// localhostKey is the private key for localhostCert. +var localhostKey = `-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAL18RtGzM/e3XgavihqFXvMU5sEjhN8DmLtieUxd4uKdvsye9Phu +R84nMTrOS2fn3M/tSIezeXhg66quOLAzjXcCAwEAAQJBAKcRxH9wuglYLBdI/0OT +BLzfWPZCEw1vZmMR2FF1Fm8nkNOVDPleeVGTWoOEcYYlQbpTmkGSxJ6ya+hqRi6x +goECIQDx3+X49fwpL6B5qpJIJMyZBSCuMhH4B7JevhGGFENi3wIhAMiNJN5Q3UkL +IuSvv03kaPR5XVQ99/UeEetUgGvBcABpAiBJSBzVITIVCGkGc7d+RCf49KTCIklv +bGWObufAR8Ni4QIgWpILjW8dkGg8GOUZ0zaNA6Nvt6TIv2UWGJ4v5PoV98kCIQDx +rIiZs5QbKdycsv9gQJzwQAogC8o04X3Zz3dsoX+h4A== +-----END RSA PRIVATE KEY-----` + func configFromEnv() (cfg VSphereConfig, ok bool) { var InsecureFlag bool var err error @@ -61,14 +89,9 @@ func configFromEnv() (cfg VSphereConfig, ok bool) { return } -// configFromEnvOrSim returns config from configFromEnv if set, -// otherwise starts a vcsim instance and returns config for use against the vcsim instance. -func configFromEnvOrSim() (VSphereConfig, func()) { - cfg, ok := configFromEnv() - if ok { - return cfg, func() {} - } - +// configFromSim starts a vcsim instance and returns config for use against the vcsim instance. +func configFromSim() (VSphereConfig, func()) { + var cfg VSphereConfig model := simulator.VPX() err := model.Create() @@ -79,6 +102,13 @@ func configFromEnvOrSim() (VSphereConfig, func()) { model.Service.TLS = new(tls.Config) s := model.Service.NewServer() + // STS simulator + path, handler := sts.New(s.URL, vpx.Setting) + model.Service.ServeMux.Handle(path, handler) + + // Lookup Service simulator + model.Service.RegisterSDK(lookup.New()) + cfg.Global.InsecureFlag = true cfg.Global.VCenterIP = s.URL.Hostname() cfg.Global.VCenterPort = s.URL.Port() @@ -105,6 +135,15 @@ func configFromEnvOrSim() (VSphereConfig, func()) { } } +// configFromEnvOrSim returns config from configFromEnv if set, otherwise returns configFromSim. +func configFromEnvOrSim() (VSphereConfig, func()) { + cfg, ok := configFromEnv() + if ok { + return cfg, func() {} + } + return configFromSim() +} + func TestReadConfig(t *testing.T) { _, err := readConfig(nil) if err == nil { @@ -179,7 +218,36 @@ func TestVSphereLogin(t *testing.T) { if err != nil { t.Errorf("Failed to connect to vSphere: %s", err) } - defer vcInstance.conn.Logout(ctx) + vcInstance.conn.Logout(ctx) +} + +func TestVSphereLoginByToken(t *testing.T) { + cfg, cleanup := configFromSim() + defer cleanup() + + // Configure for SAML token auth + cfg.Global.User = localhostCert + cfg.Global.Password = localhostKey + + // Create vSphere configuration object + vs, err := newControllerNode(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate vSphere: %s", err) + } + + ctx := context.Background() + + // Create vSphere client + vcInstance, ok := vs.vsphereInstanceMap[cfg.Global.VCenterIP] + if !ok { + t.Fatalf("Couldn't get vSphere instance: %s", cfg.Global.VCenterIP) + } + + err = vcInstance.conn.Connect(ctx) + if err != nil { + t.Errorf("Failed to connect to vSphere: %s", err) + } + vcInstance.conn.Logout(ctx) } func TestZones(t *testing.T) {