diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c290e0eb..69c2883a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: 1.18.0 + go-version: 1.19.1 - uses: actions/cache@v2 with: path: | @@ -32,23 +32,23 @@ jobs: strategy: fail-fast: false matrix: - kind-k8s-version: [1.21.10, 1.22.7, 1.23.6, 1.24.0] + kind-k8s-version: [1.22.13, 1.23.10, 1.24.4, 1.25.0] steps: - uses: actions/checkout@v2 - name: Create K8s Kind Cluster uses: helm/kind-action@v1.2.0 with: - version: v0.13.0 + version: v0.14.0 cluster_name: vault-plugin-auth-kubernetes config: integrationtest/kind/config.yaml node_image: kindest/node:v${{ matrix.kind-k8s-version }} # Must come _after_ kind-action, because the kind step also sets up a kubectl binary. - uses: azure/setup-kubectl@v2.0 with: - version: 'v1.24.0' + version: 'v1.25.0' - uses: actions/setup-go@v2 with: - go-version: 1.18.0 + go-version: 1.19.1 - uses: actions/cache@v2 with: path: | diff --git a/Makefile b/Makefile index 4f1db014..c3957a9b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ +TESTARGS ?= '-test.v' # kind cluster name -KIND_CLUSTER_NAME?=vault-plugin-auth-kubernetes +KIND_CLUSTER_NAME ?= vault-plugin-auth-kubernetes # kind k8s version -KIND_K8S_VERSION?=v1.24.0 +KIND_K8S_VERSION ?= v1.25.0 .PHONY: default default: dev @@ -13,11 +14,11 @@ dev: .PHONY: test test: fmtcheck - CGO_ENABLED=0 go test ./... $(TESTARGS) -timeout=20m + CGO_ENABLED=0 go test $(TESTARGS) -timeout=20m ./... .PHONY: integration-test integration-test: - INTEGRATION_TESTS=true CGO_ENABLED=0 go test github.com/hashicorp/vault-plugin-auth-kubernetes/integrationtest/... $(TESTARGS) -count=1 -timeout=20m + INTEGRATION_TESTS=true CGO_ENABLED=0 go test $(TESTARGS) -count=1 -timeout=20m github.com/hashicorp/vault-plugin-auth-kubernetes/integrationtest/... .PHONY: fmtcheck fmtcheck: @@ -52,12 +53,13 @@ vault-image: setup-integration-test: teardown-integration-test vault-image kind --name ${KIND_CLUSTER_NAME} load docker-image hashicorp/vault:dev kubectl create namespace test - helm install vault vault --repo https://helm.releases.hashicorp.com --version=0.19.0 \ + helm install vault vault --repo https://helm.releases.hashicorp.com --version=0.22.0 \ --wait --timeout=5m \ --namespace=test \ --set server.dev.enabled=true \ --set server.image.tag=dev \ --set server.image.pullPolicy=Never \ + --set server.logLevel=trace \ --set injector.enabled=false \ --set server.extraArgs="-dev-plugin-dir=/vault/plugin_directory" kubectl patch --namespace=test statefulset vault --patch-file integrationtest/vault/hostPortPatch.yaml @@ -69,4 +71,4 @@ setup-integration-test: teardown-integration-test vault-image .PHONY: teardown-integration-test teardown-integration-test: helm uninstall vault --namespace=test || true - kubectl delete --ignore-not-found namespace test \ No newline at end of file + kubectl delete --ignore-not-found namespace test diff --git a/backend.go b/backend.go index 2c090c6b..da8284c2 100644 --- a/backend.go +++ b/backend.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -26,6 +27,7 @@ const ( aliasNameSourceSAUid = "serviceaccount_uid" aliasNameSourceSAName = "serviceaccount_name" aliasNameSourceDefault = aliasNameSourceSAUid + minTLSVersion = tls.VersionTLS12 ) var ( @@ -44,6 +46,17 @@ var ( // caReloadPeriod is the time period how often the in-memory copy of local // CA cert can be used, before reading it again from disk. caReloadPeriod = 1 * time.Hour + + // defaultHorizon provides the default duration to be used + // in the tlsConfigUpdater's time.Ticker, setup in runTLSConfigUpdater() + defaultHorizon = time.Second * 30 + + // defaultMinHorizon provides the minimum duration that can be specified + // in the tlsConfigUpdater's time.Ticker, setup in runTLSConfigUpdater() + defaultMinHorizon = time.Second * 5 + + errTLSConfigNotSet = errors.New("TLSConfig not set") + errHTTPClientNotSet = errors.New("http.Client not set") ) // kubeAuthBackend implements logical.Backend @@ -53,8 +66,11 @@ type kubeAuthBackend struct { // default HTTP client for connection reuse httpClient *http.Client + // tlsConfig is periodically updated whenever the CA certificate configuration changes. + tlsConfig *tls.Config + // reviewFactory is used to configure the strategy for doing a token review. - // Currently the only options are using the kubernetes API or mocking the + // Currently, the only options are using the kubernetes API or mocking the // review. Mocks should only be used in tests. reviewFactory tokenReviewFactory @@ -71,22 +87,47 @@ type kubeAuthBackend struct { // - disable_local_ca_jwt is false localCACertReader *cachingFileReader + // tlsConfigUpdaterRunning is used to signal the current state of the tlsConfig updater routine. + tlsConfigUpdaterRunning bool + + // tlsConfigUpdateCancelFunc should be called in the backend's Clean(), set in initialize(). + tlsConfigUpdateCancelFunc context.CancelFunc + l sync.RWMutex + + // tlsMu provides the lock for synchronizing updates to the tlsConfig. + tlsMu sync.RWMutex } // Factory returns a new backend as logical.Backend. func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { b := Backend() + if err := b.Setup(ctx, conf); err != nil { return nil, err } + return b, nil } +var getDefaultHTTPClient = cleanhttp.DefaultPooledClient + +func getDefaultTLSConfig() *tls.Config { + return &tls.Config{ + MinVersion: minTLSVersion, + } +} + func Backend() *kubeAuthBackend { b := &kubeAuthBackend{ localSATokenReader: newCachingFileReader(localJWTPath, jwtReloadPeriod, time.Now), localCACertReader: newCachingFileReader(localCACertPath, caReloadPeriod, time.Now), + // Set default HTTP client + httpClient: getDefaultHTTPClient(), + // Set the default TLSConfig + tlsConfig: getDefaultTLSConfig(), + // Set the review factory to default to calling into the kubernetes API. + reviewFactory: tokenReviewAPIFactory, } b.Backend = &framework.Backend{ @@ -109,46 +150,129 @@ func Backend() *kubeAuthBackend { pathsRole(b), ), InitializeFunc: b.initialize, + Clean: b.cleanup, } - // Set default HTTP client - b.httpClient = cleanhttp.DefaultPooledClient() - - // Set the review factory to default to calling into the kubernetes API. - b.reviewFactory = tokenReviewAPIFactory - return b } // initialize is used to handle the state of config values just after the K8s plugin has been mounted func (b *kubeAuthBackend) initialize(ctx context.Context, req *logical.InitializationRequest) error { - // Try to load the config on initialization - config, err := b.loadConfig(ctx, req.Storage) + updaterCtx, cancel := context.WithCancel(context.Background()) + if err := b.runTLSConfigUpdater(updaterCtx, req.Storage, defaultHorizon); err != nil { + cancel() + return err + } + + b.tlsConfigUpdateCancelFunc = cancel + + config, err := b.config(ctx, req.Storage) if err != nil { return err } - if config == nil { + + if config != nil { + if err := b.updateTLSConfig(config); err != nil { + return err + } + } + + return nil +} + +func (b *kubeAuthBackend) cleanup(_ context.Context) { + b.shutdownTLSConfigUpdater() +} + +// validateHTTPClientInit that the Backend's HTTPClient and TLSConfig has been properly instantiated. +func (b *kubeAuthBackend) validateHTTPClientInit() error { + if b.httpClient == nil { + return errHTTPClientNotSet + } + if b.tlsConfig == nil { + return errTLSConfigNotSet + } + + return nil +} + +// runTLSConfigUpdater sets up a routine that periodically calls b.updateTLSConfig(). This ensures that the +// httpClient's TLS configuration is consistent with the backend's stored configuration. +func (b *kubeAuthBackend) runTLSConfigUpdater(ctx context.Context, s logical.Storage, horizon time.Duration) error { + b.tlsMu.Lock() + defer b.tlsMu.Unlock() + + if b.tlsConfigUpdaterRunning { return nil } - b.l.Lock() - defer b.l.Unlock() - // If we have a CA cert build the TLSConfig - if len(config.CACert) > 0 { - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM([]byte(config.CACert)) + if horizon < defaultMinHorizon { + return fmt.Errorf("update horizon must be equal to or greater than %s", defaultMinHorizon) + } + + if err := b.validateHTTPClientInit(); err != nil { + return err + } + + updateTLSConfig := func(ctx context.Context, s logical.Storage) error { + config, err := b.config(ctx, s) + if err != nil { + return fmt.Errorf("failed config read, err=%w", err) + } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - RootCAs: certPool, + if config == nil { + b.Logger().Trace("Skipping TLSConfig update, no configuration set") + return nil + } + + if err := b.updateTLSConfig(config); err != nil { + return err } - b.httpClient.Transport.(*http.Transport).TLSClientConfig = tlsConfig + return nil } + var wg sync.WaitGroup + wg.Add(1) + ticker := time.NewTicker(horizon) + go func(ctx context.Context, s logical.Storage) { + defer func() { + b.tlsMu.Lock() + defer b.tlsMu.Unlock() + ticker.Stop() + b.tlsConfigUpdaterRunning = false + b.Logger().Trace("TLSConfig updater shutdown completed") + }() + + b.Logger().Trace("TLSConfig updater starting", "horizon", horizon) + b.tlsConfigUpdaterRunning = true + wg.Done() + for { + select { + case <-ctx.Done(): + b.Logger().Trace("TLSConfig updater shutting down") + return + case <-ticker.C: + if err := updateTLSConfig(ctx, s); err != nil { + b.Logger().Warn("TLSConfig update failed, retrying", + "horizon", defaultHorizon.String(), "err", err) + } + } + } + }(ctx, s) + wg.Wait() + return nil } +func (b *kubeAuthBackend) shutdownTLSConfigUpdater() { + if b.tlsConfigUpdateCancelFunc != nil { + b.Logger().Debug("TLSConfig updater shutdown requested") + b.tlsConfigUpdateCancelFunc() + b.tlsConfigUpdateCancelFunc = nil + } +} + // config takes a storage object and returns a kubeConfig object. // It does not return local token and CA file which are specific to the pod we run in. func (b *kubeAuthBackend) config(ctx context.Context, s logical.Storage) (*kubeConfig, error) { @@ -255,6 +379,70 @@ func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name stri return role, nil } +// getHTTPClient return the backend's HTTP client for connecting to the Kubernetes API. +func (b *kubeAuthBackend) getHTTPClient() (*http.Client, error) { + b.tlsMu.RLock() + defer b.tlsMu.RUnlock() + + if err := b.validateHTTPClientInit(); err != nil { + return nil, err + } + + return b.httpClient, nil +} + +// updateTLSConfig ensures that the httpClient's TLS configuration is consistent +// with the backend's stored configuration. +func (b *kubeAuthBackend) updateTLSConfig(config *kubeConfig) error { + b.tlsMu.Lock() + defer b.tlsMu.Unlock() + + if err := b.validateHTTPClientInit(); err != nil { + return err + } + + // attempt to read the CA certificates from the config directly or from the filesystem. + var caCertBytes []byte + if config.CACert != "" { + caCertBytes = []byte(config.CACert) + } else if !config.DisableLocalCAJwt && b.localCACertReader != nil { + data, err := b.localCACertReader.ReadFile() + if err != nil { + return err + } + caCertBytes = []byte(data) + } + + certPool := x509.NewCertPool() + if len(caCertBytes) > 0 { + if ok := certPool.AppendCertsFromPEM(caCertBytes); !ok { + b.Logger().Warn("Configured CA PEM data contains no valid certificates, TLS verification will fail") + } + } else { + // provide an empty certPool + b.Logger().Warn("No CA certificates configured, TLS verification will fail") + // TODO: think about supporting host root CA certificates via a configuration toggle, + // in which case RootCAs should be set to nil + } + + // only refresh the Root CAs if they have changed since the last full update. + if !b.tlsConfig.RootCAs.Equal(certPool) { + b.Logger().Trace("Root CA certificate pool has changed, updating the client's transport") + transport, ok := b.httpClient.Transport.(*http.Transport) + if !ok { + // should never happen + return fmt.Errorf("type assertion failed for %T", b.httpClient.Transport) + } + + b.tlsConfig.RootCAs = certPool + transport.TLSClientConfig = b.tlsConfig + } else { + b.Logger().Trace("Root CA certificate pool is unchanged, no update required") + } + + return nil +} + func validateAliasNameSource(source string) error { for _, s := range aliasNameSources { if s == source { diff --git a/backend_test.go b/backend_test.go new file mode 100644 index 00000000..f4f0f0de --- /dev/null +++ b/backend_test.go @@ -0,0 +1,528 @@ +package kubeauth + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +func Test_kubeAuthBackend_updateTLSConfig(t *testing.T) { + defaultCertPool := getTestCertPool(t, testCACert) + localCertPool := getTestCertPool(t, testLocalCACert) + otherCertPool := getTestCertPool(t, testOtherCACert) + + type testConfig struct { + config *kubeConfig + expectTLSConfig *tls.Config + localCACert string + wantErr bool + expectError error + } + tests := []struct { + name string + httpClient *http.Client + tlsConfig *tls.Config + wantErr bool + configs []testConfig + }{ + { + name: "fail-client-not-set", + httpClient: nil, + configs: []testConfig{ + { + wantErr: true, + expectError: errHTTPClientNotSet, + }, + }, + }, + { + name: "fail-tlsConfig-not-set", + httpClient: getDefaultHTTPClient(), + configs: []testConfig{ + { + wantErr: true, + expectError: errTLSConfigNotSet, + }, + }, + }, + { + name: "ca-certs-from-config-source", + httpClient: getDefaultHTTPClient(), + tlsConfig: getDefaultTLSConfig(), + wantErr: false, + configs: []testConfig{ + { + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + }, + { + config: &kubeConfig{ + CACert: testLocalCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: localCertPool, + }, + }, + { + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + }, + }, + }, + { + name: "ca-certs-from-file-source", + httpClient: getDefaultHTTPClient(), + tlsConfig: getDefaultTLSConfig(), + configs: []testConfig{ + { + config: &kubeConfig{ + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + localCACert: testCACert, + }, + { + config: &kubeConfig{ + DisableLocalCAJwt: false, + }, + localCACert: testLocalCACert, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: localCertPool, + }, + }, + }, + wantErr: false, + }, + { + name: "ca-certs-mixed-source", + httpClient: getDefaultHTTPClient(), + tlsConfig: getDefaultTLSConfig(), + configs: []testConfig{ + { + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + }, + { + config: &kubeConfig{ + DisableLocalCAJwt: false, + }, + localCACert: testLocalCACert, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: localCertPool, + }, + }, + { + config: &kubeConfig{ + CACert: testOtherCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: otherCertPool, + }, + }, + { + config: &kubeConfig{ + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + localCACert: testCACert, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &kubeAuthBackend{ + Backend: &framework.Backend{}, + httpClient: tt.httpClient, + tlsConfig: tt.tlsConfig, + } + + if err := b.Setup(context.Background(), + &logical.BackendConfig{ + Logger: hclog.NewNullLogger(), + }); err != nil { + t.Fatalf("failed to setup the backend, err=%v", err) + } + + localFile := filepath.Join(t.TempDir(), "ca.crt") + b.localCACertReader = &cachingFileReader{ + path: localFile, + currentTime: time.Now().UTC, + ttl: 0, + } + for idx, config := range tt.configs { + t.Run(fmt.Sprintf("config-%d", idx), func(t *testing.T) { + if config.localCACert != "" { + if err := os.WriteFile(localFile, []byte(config.localCACert), 0600); err != nil { + t.Fatalf("failed to write local file %q", localFile) + } + t.Cleanup(func() { + if err := os.Remove(localFile); err != nil { + t.Fatal(err) + } + }) + } + + err := b.updateTLSConfig(config.config) + if config.wantErr && err == nil { + t.Fatalf("updateTLSConfig() error = %v, wantErr %v", err, config.wantErr) + } + + if !reflect.DeepEqual(err, config.expectError) { + t.Fatalf("updateTLSConfig() error = %v, expectErr %v", err, config.expectError) + } + + if config.wantErr { + return + } + + assertTLSConfigEquals(t, b.tlsConfig, config.expectTLSConfig) + assertValidTransport(t, b, config.expectTLSConfig) + }) + } + }) + } +} + +func Test_kubeAuthBackend_initialize(t *testing.T) { + defaultCertPool := getTestCertPool(t, testCACert) + + tests := []struct { + name string + httpClient *http.Client + ctx context.Context + req *logical.InitializationRequest + config *kubeConfig + tlsConfig *tls.Config + expectTLSConfig *tls.Config + wantErr bool + expectErr error + }{ + { + name: "fail-client-not-set", + ctx: context.Background(), + httpClient: nil, + tlsConfig: getDefaultTLSConfig(), + req: &logical.InitializationRequest{ + Storage: &logical.InmemStorage{}, + }, + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + wantErr: true, + expectErr: errHTTPClientNotSet, + }, + { + name: "no-config", + ctx: context.Background(), + httpClient: getDefaultHTTPClient(), + tlsConfig: getDefaultTLSConfig(), + req: &logical.InitializationRequest{ + Storage: &logical.InmemStorage{}, + }, + wantErr: false, + expectErr: nil, + }, + { + name: "initialized-from-config", + ctx: context.Background(), + httpClient: getDefaultHTTPClient(), + tlsConfig: getDefaultTLSConfig(), + req: &logical.InitializationRequest{ + Storage: &logical.InmemStorage{}, + }, + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + wantErr: false, + expectErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &kubeAuthBackend{ + Backend: &framework.Backend{}, + httpClient: tt.httpClient, + tlsConfig: tt.tlsConfig, + } + + if err := b.Setup(context.Background(), + &logical.BackendConfig{ + Logger: hclog.NewNullLogger(), + StorageView: tt.req.Storage, + }); err != nil { + t.Fatalf("failed to setup the backend, err=%v", err) + } + + if tt.config != nil { + entry, err := logical.StorageEntryJSON(configPath, tt.config) + if err != nil { + t.Fatal(err) + } + + if err := tt.req.Storage.Put(tt.ctx, entry); err != nil { + t.Fatal(err) + } + } + + if b.tlsConfigUpdaterRunning { + t.Fatalf("tlsConfigUpdater started before initialize()") + } + + ctx, _ := context.WithTimeout(tt.ctx, time.Second*30) + err := b.initialize(ctx, tt.req) + if tt.wantErr && err == nil { + t.Errorf("initialize() error = %v, wantErr %v", err, tt.wantErr) + + } + + if !reflect.DeepEqual(err, tt.expectErr) { + t.Fatalf("initialize() error = %v, expectErr %v", err, tt.expectErr) + } + + if tt.wantErr { + return + } + + if tt.config != nil { + assertTLSConfigEquals(t, b.tlsConfig, tt.expectTLSConfig) + assertValidTransport(t, b, tt.expectTLSConfig) + } + + if !b.tlsConfigUpdaterRunning { + t.Fatalf("tlsConfigUpdater not started from initialize()") + } + }) + } +} + +func Test_kubeAuthBackend_runTLSConfigUpdater(t *testing.T) { + defaultCertPool := getTestCertPool(t, testCACert) + otherCertPool := getTestCertPool(t, testOtherCACert) + + type testConfig struct { + config *kubeConfig + expectTLSConfig *tls.Config + } + + tests := []struct { + name string + ctx context.Context + storage logical.Storage + tlsConfig *tls.Config + horizon time.Duration + minHorizon time.Duration + wantErr bool + expectErr error + configs []*testConfig + }{ + { + name: "initialized-from-config", + tlsConfig: getDefaultTLSConfig(), + ctx: context.Background(), + storage: &logical.InmemStorage{}, + horizon: time.Millisecond * 500, + minHorizon: time.Millisecond * 499, + wantErr: false, + expectErr: nil, + configs: []*testConfig{ + { + config: &kubeConfig{ + CACert: testCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: defaultCertPool, + }, + }, + { + config: &kubeConfig{ + CACert: testOtherCACert, + DisableLocalCAJwt: false, + }, + expectTLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + RootCAs: otherCertPool, + }, + }, + }, + }, + { + name: "fail-min-horizon", + ctx: context.Background(), + storage: &logical.InmemStorage{}, + horizon: time.Millisecond * 500, + wantErr: true, + expectErr: fmt.Errorf("update horizon must be equal to or greater than %s", defaultMinHorizon), + }, + } + + d := defaultMinHorizon + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.minHorizon > 0 { + defer (func() { + defaultMinHorizon = d + })() + defaultMinHorizon = tt.minHorizon + } + b := &kubeAuthBackend{ + Backend: &framework.Backend{}, + httpClient: getDefaultHTTPClient(), + tlsConfig: tt.tlsConfig, + } + + if err := b.Setup(context.Background(), + &logical.BackendConfig{ + Logger: hclog.NewNullLogger(), + StorageView: tt.storage, + }); err != nil { + t.Fatalf("failed to setup the backend, err=%v", err) + } + + if b.tlsConfigUpdaterRunning { + t.Fatalf("tlsConfigUpdater already started") + } + + configCount := len(tt.configs) + ctx, cancel := context.WithTimeout(tt.ctx, tt.horizon*time.Duration(configCount*2)) + defer cancel() + err := b.runTLSConfigUpdater(ctx, tt.storage, tt.horizon) + if tt.wantErr && err == nil { + t.Errorf("runTLSConfigUpdater() error = %v, wantErr %v", err, tt.wantErr) + + } + + if !reflect.DeepEqual(err, tt.expectErr) { + t.Fatalf("runTLSConfigUpdater() error = %v, expectErr %v", err, tt.expectErr) + } + + if tt.wantErr { + return + } + + if !b.tlsConfigUpdaterRunning { + t.Fatalf("tlsConfigUpdater not started") + } + + if configCount > 0 { + for idx := 0; idx < configCount; idx++ { + t.Run(fmt.Sprintf("config-%d", idx), func(t *testing.T) { + config := tt.configs[idx] + if config.config != nil { + entry, err := logical.StorageEntryJSON(configPath, config.config) + if err != nil { + t.Fatal(err) + } + + if err := tt.storage.Put(tt.ctx, entry); err != nil { + t.Fatal(err) + } + } + + time.Sleep(tt.horizon * 3) + if b.tlsConfig == nil { + t.Fatalf("runTLSConfigUpdater(), expected tlsConfig initialization") + } + assertTLSConfigEquals(t, b.tlsConfig, config.expectTLSConfig) + assertValidTransport(t, b, config.expectTLSConfig) + }) + } + } else { + if b.tlsConfig != nil { + t.Errorf("runTLSConfigUpdater(), unexpected tlsConfig initialization") + } + } + + cancel() + time.Sleep(tt.horizon) + if b.tlsConfigUpdaterRunning { + t.Fatalf("tlsConfigUpdater did not shutdown cleanly") + } + }) + } +} + +func assertTLSConfigEquals(t *testing.T, actual, expected *tls.Config) { + t.Helper() + + if !actual.RootCAs.Equal(expected.RootCAs) { + t.Errorf("updateTLSConfig() actual RootCAs = %v, expected RootCAs %v", + actual.RootCAs, expected.RootCAs) + } + if actual.MinVersion != expected.MinVersion { + t.Errorf("updateTLSConfig() actual MinVersion = %v, expected MinVersion %v", + actual.MinVersion, expected.MinVersion) + } + +} + +func assertValidTransport(t *testing.T, b *kubeAuthBackend, expected *tls.Config) { + t.Helper() + + transport, ok := b.httpClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("type assertion failed for %T", b.httpClient.Transport) + } + + assertTLSConfigEquals(t, transport.TLSClientConfig, expected) +} + +func getTestCertPool(t *testing.T, cert string) *x509.CertPool { + t.Helper() + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM([]byte(cert)); !ok { + t.Fatalf("test certificate contains no valid certificates") + } + return pool +} diff --git a/caching_file_reader_test.go b/caching_file_reader_test.go index ba282510..1935108c 100644 --- a/caching_file_reader_test.go +++ b/caching_file_reader_test.go @@ -27,7 +27,7 @@ func TestCachingFileReader(t *testing.T) { }) // Write initial content to file and check that we can read it. - ioutil.WriteFile(f.Name(), []byte(content1), 0644) + ioutil.WriteFile(f.Name(), []byte(content1), 0o644) got, err := r.ReadFile() if err != nil { t.Error(err) @@ -37,7 +37,7 @@ func TestCachingFileReader(t *testing.T) { } // Write new content to the file. - ioutil.WriteFile(f.Name(), []byte(content2), 0644) + ioutil.WriteFile(f.Name(), []byte(content2), 0o644) // Advance simulated time, but not enough for cache to expire. currentTime = currentTime.Add(30 * time.Second) diff --git a/common_test.go b/common_test.go new file mode 100644 index 00000000..7adf0195 --- /dev/null +++ b/common_test.go @@ -0,0 +1,105 @@ +package kubeauth + +const ( + testLocalCACert = `-----BEGIN CERTIFICATE----- +MIIDVDCCAjwCCQDFiyFY1M6afTANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UE +CgwXVmF1bHQgVGVzdGluZyBBdXRob3JpdHkxFDASBgNVBAMMC2V4YW1wbGUubmV0 +MB4XDTIwMDkxODAxMjkxM1oXDTQ1MDkxODAxMjkxM1owbDELMAkGA1UEBhMCVVMx +EzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoM +F1ZhdWx0IFRlc3RpbmcgQXV0aG9yaXR5MRQwEgYDVQQDDAtleGFtcGxlLm5ldDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALCA9oKv+ESRHX2e/iq1PlGr +zD23/MBS0V+fWQDY0hyEqY98CGwRtF6pEcLEYsreArj5/zznsIevLkNOD+beg43y +WpEJlCPgDhGXI/Oima6ooHVEIMaIKLjK7GrSzAb3rNRGACwrR/u/IKaFl+XJG0qx +g8mOZ3fByaAlIk+shVLUcIedNN1tNR+6/4ZpHg7PDjrZXP4XKrmKPTh4yqfu+BtZ +9IY2oyregqEsGW1/3h1NM+LHGVakTV2d/mwMYHhwoq9Y8BD+PemT5z8TmhH/cIk5 +P8Q8ud5/q6YTIJg9TELKebLAeNtRNnNoHeUoRTjiW1MBwNHtgyTTY+H3W/9Dne0C +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAXmygFkGIBnXxKlsTDiV8RW2iHLgFdZFJ +hcU8UpxZhhaL5JbQl6byfbHjrX31q7ii8uC8FcbW0AEdnEQAb9Ui6a+if7HwXNmI +DTlYl+lMlk9RtWvExw6AEEbg5nCpGaKexm7wJgzYGP9by9pQ7wX/CS7ofCzCK+Al +uSIqjPkMC201ZXH39n1lxxq6BacdYjv8wo4mMzi8iTSQGVWPdjHZVYOClFgN6hoj +8SkrrSe888a0H+i7EknRxC4sLRaMUK/FAvwtXaSZi2djruAtQzQGQ56m1phC2C/k +k9aL00AQ9Y4KTfiJD7LK8YIZDnFKLOCJhYgKCLCOVwOHb7836SNCxA== +-----END CERTIFICATE-----` + + testLocalJWT = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlZhdWx0IFRlc3QiLCJpYXQiOjExMjM1OH0.GOC8w-MyhorgojB20SPNyH_ECsBjYJH89hjntOxSywA` + + testRSACert = `-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p +a3ViZUNBMB4XDTE3MDgzMDE5MDgzNloXDTE4MDgzMDE5MDgzNlowLDEXMBUGA1UE +ChMOc3lzdGVtOm1hc3RlcnMxETAPBgNVBAMTCG1pbmlrdWJlMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxD3eM3+WNc4phxAeQxNOmcybKlNJWowuC12u +v+cGJWxxpDx/OoEIxKI5wmgHxEwFCZL545sjfLqyBcgxQR2xSCib+bYzjBtfA6uV +6d/35nurzz21okcMffc5xKMyZhEwt98WAvYWD71Bihz7iGBq5Sw9md6pqnkNoScR +Hhi3Vl94a6D6shwb6nXA2hlwYLcnoKtpe3Ptq6MW6CpfBA8C11q5eeW4xdvrwKt3 +Vd1TgFeEnnqwzUWGapU2uwwUfbRkLTDvrp6791uq0Vo7mzz00xYhV1PLCeAdpJEK +3Vr74FT7jHIbPlzi/qjRBVFKf9IRXnhbjrCl7S0Ayev1Fao4TQIDAQABo4G1MIGy +MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw +DAYDVR0TAQH/BAIwADBzBgNVHREEbDBqgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3Zj +LmNsdXN0ZXIubG9jYWyCFmt1YmVybmV0ZXMuZGVmYXVsdC5zdmOCEmt1YmVybmV0 +ZXMuZGVmYXVsdIIKa3ViZXJuZXRlc4cEwKhjZIcECgAAATANBgkqhkiG9w0BAQsF +AAOCAQEAIw8rKuryhhl527wf9q/VrWixzZ1jCLvyc/60z9rWpXxKFxT8AyCsHirM +F4fHXW4Brcoh/Dc2ci36cUbuywIyxHjgVUG45D4jPPWskY1++ZSfJfSXAuA8eFew +c+No3WPkmZB6ZOZ6q5iPY+FOgDZC7ddWmGuZrle51gBL347cU7H1BrTm6Lm6kXRs +fHRZJX2+B8lnsXsS3QF2BTU0ymuCxCCQxub/GhPZVz3nNNtro1z7/szLUVP1c1/8 +p7HP3k7caxfp346TZ/HgbV9sJEkHP7Ym7n9E7LSyUTSxXwBRPraH1WQzEgFNPSUV +V0n6FBLiejOTPKapJ2F0tIqAyJHFug== +-----END CERTIFICATE-----` + + testECCert = `-----BEGIN CERTIFICATE----- +MIICZDCCAeugAwIBAgIJALM9NbK8WRuBMAkGByqGSM49BAEwRTELMAkGA1UEBhMC +dXMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp +dHMgUHR5IEx0ZDAeFw0xNzA5MTExNzQ2NDNaFw0yNzA5MDkxNzQ2NDNaMEUxCzAJ +BgNVBAYTAnVzMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATcqsBLxKP+ +UHk7Y6ktGGFvfrIfIXHxeZe3Xwt691CWfdmJFvrGzyzW5/AbJIuO1utdOsqUStAm +W/Scfxop/FGadKqR4nAWLNBI4intgnf0r1rzBCSOmanolHqxQPqQ0UOjgacwgaQw +HQYDVR0OBBYEFHxh1pTd8ApEzg0gKMwwt01aA10TMHUGA1UdIwRuMGyAFHxh1pTd +8ApEzg0gKMwwt01aA10ToUmkRzBFMQswCQYDVQQGEwJ1czETMBEGA1UECBMKU29t +ZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkggkAsz01 +srxZG4EwDAYDVR0TBAUwAwEB/zAJBgcqhkjOPQQBA2gAMGUCMCR+CvAoNBhqSe2M +4qWWD/9XX/0qmf0O442Qowcg5MWH1+mwl1s7ozinvbTPDPaYDwIxAM54qKhuL6xt +GxqJpa7Onn15Hu8zTsdzeYBqUUXA6wtn+Pa7197CgUkfty9yc2eeQw== +-----END CERTIFICATE-----` + + testCACert = ` +-----BEGIN CERTIFICATE----- +MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p +a3ViZUNBMB4XDTE3MDgxMDIzMTQ1NVoXDTI3MDgwODIzMTQ1NVowFTETMBEGA1UE +AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN8d +w2p/KXRkm+vzOO0eT1vYBWP7fKsnng9/g5nnXAJlt9NxpOSolRcyItm/04R0E1jx +jpgsdzkybc+QU5ZiszOYN833/D5hCNVAABVivpDd2P8wVKXN46cB99e24etUVBqG +5aR0Ku3IBsJjCN9efhF+XRCA2gy/KaXMdKJhHfdtc8hCr7G9+2wO2G58FLmIfEyH +owviOGt0BSnCtMpsA8ZgGQyfqgSd5u466aCv6oj0MyzsMnfS38niM53Rlv4IY6ol +taYbWXtCNndQ2S687qE0qTCxhE95Bm2Nfkqct4R1798sJz83xNv8hALvxr/vPK/J +2XkIm3oo3YKG4n/CHXcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW +MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQCSkrhE1PczqeqXfRaWayJUbXWPwKFbszO0MhGB1zwnPZq39qjY +ySQiGvnjV3fP+N5CTQAwMNe79Xiw31fSoexgceCPJpraWrTOLdCv04SbGDBapMFM +aezBu9jzZm0CNt60jHXWXuHHVPFX6u7ZR8W+RiBvsT8GZ5U6sNs3aN3M9Vym06BL +aSphIw1v+hRlPfnrlJwUnQp158DRgkt/9ncTa/k88KoIoZAbulaiGB4zHxxkbura +GSlgpZzhHSrBDLuXf65GHwwGxSExhgY5AA/n8rumGVvE8IYohS9yg/jOG0xP2WQH +u/ABoYtOyseO+lgElA8R4PB9MtwgN6c/b0xH +-----END CERTIFICATE-----` + + testOtherCACert = `-----BEGIN CERTIFICATE----- +MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl +cm5ldGVzMB4XDTIyMTEyOTIwNDYwNVoXDTMyMTEyNjIwNDYwNVowFTETMBEGA1UE +AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOYO +AGi6hysYe4PF3sCyVXAFM72X96F51ncmDg+Ihhvt9Touj7Yo0LLMmy1UPM3YzEdV +z2IV1iksLIBObkct2eBGt2SFjLMhphZdA34mPAzFZhpvNgn1U2uUSqfp5phg00Mg +DdXLE0LFIVqAGkoBysr89l6P2MaTibcKoZwAhpMbATLcn1QcXF/NLzYuO9FPvrUL +mAR/HslDz10LBsMjtgRKXd2dX4yrQlYSB7YmLlu/bLKdjiE1a/+EO4wNcl/JJ+vu +fzPwxWALej/k61mcP+4JxfjgY53AM7vaZ+P9Yb0bGw7r1bozgP7+FGL1f6onTxeG +7+FECpErmrv9IQocgh8CAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFE0Y+CYtusQDfr8tqSZZ7PEcDJjwMBUGA1UdEQQO +MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAJK57wm9rH0yVmjmY1ES +kE8e+pTnXZkKaqUce/d7cPRn1+0Dtutvxl/j3P4DUjba7lVGYYNKp1xy2xVvg7Dl +mXyJigvBoTGyzJzNDIUJz8Kgse4eCrwl59WP94K83cVRLeUq+3amLwzubUNbezsW +QwcCyACuzTetR5ZXEg7iIS4HDy+ER2yjuY6d0GPLG+FH02WMrlE7mmxNfZOSy/5E +pEDcN/HcXM47TP7XgWW0rfQli3RucuqMV7LHvvpiGIWwfutrK9g7Py91W2JbQCA0 +D14XDzgsruCwlWAP1FMvLMIPhSknpIJd9Xql+0/Ae1yl9f3Uamj3mDtBKg8/U5nJ +0wU= +-----END CERTIFICATE----- +` +) diff --git a/go.mod b/go.mod index 5f51623e..78f4ffc2 100644 --- a/go.mod +++ b/go.mod @@ -59,9 +59,9 @@ require ( github.com/ryanuber/go-glob v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect + golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 45c93469..bb4c3e33 100644 --- a/go.sum +++ b/go.sum @@ -462,8 +462,9 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -524,9 +525,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/integrationtest/integration_test.go b/integrationtest/integration_test.go index b2d16e0c..b994c25e 100644 --- a/integrationtest/integration_test.go +++ b/integrationtest/integration_test.go @@ -17,11 +17,12 @@ import ( // Set the environment variable INTEGRATION_TESTS to any non-empty value to run // the tests in this package. The test assumes it has available: -// - kubectl -// - A Kubernetes cluster in which: -// - it can use the `test` namespace -// - Vault is deployed and accessible -// - There is a serviceaccount called test-token-reviewer-account with access to the TokenReview API +// - kubectl +// - A Kubernetes cluster in which: +// - it can use the `test` namespace +// - Vault is deployed and accessible +// - There is a serviceaccount called test-token-reviewer-account with access to the TokenReview API +// // See `make setup-integration-test` for manual testing. func TestMain(m *testing.M) { if os.Getenv("INTEGRATION_TESTS") != "" { diff --git a/integrationtest/vault/Dockerfile b/integrationtest/vault/Dockerfile index 1e117bba..f7f4ba63 100644 --- a/integrationtest/vault/Dockerfile +++ b/integrationtest/vault/Dockerfile @@ -1,5 +1,5 @@ -FROM docker.mirror.hashicorp.services/hashicorp/vault:1.10.0 +FROM docker.mirror.hashicorp.services/hashicorp/vault:1.11.3 # Don't use `kubernetes` as plugin name to ensure we don't silently fall back to # the built-in kubernetes auth plugin if something goes wrong. -COPY --chown=vault:vault vault-plugin-auth-kubernetes /vault/plugin_directory/kubernetes-dev \ No newline at end of file +COPY --chown=vault:vault vault-plugin-auth-kubernetes /vault/plugin_directory/kubernetes-dev diff --git a/path_config.go b/path_config.go index 2c46b9c0..40af5270 100644 --- a/path_config.go +++ b/path_config.go @@ -5,11 +5,9 @@ import ( "crypto" "crypto/ecdsa" "crypto/rsa" - "crypto/tls" "crypto/x509" "encoding/pem" "errors" - "net/http" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" @@ -122,6 +120,9 @@ func (b *kubeAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reque // pathConfigWrite handles create and update commands to the config func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.l.Lock() + defer b.l.Unlock() + host := data.Get("kubernetes_host").(string) if host == "" { return logical.ErrorResponse("no host provided"), nil @@ -157,32 +158,6 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ DisableLocalCAJwt: disableLocalJWT, } - b.l.Lock() - defer b.l.Unlock() - - // Determine if we load the local CA cert or the CA cert provided - // by the kubernetes_ca_cert path into the backend's HTTP client - certPool := x509.NewCertPool() - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - } - if disableLocalJWT || len(caCert) > 0 { - certPool.AppendCertsFromPEM([]byte(config.CACert)) - tlsConfig.RootCAs = certPool - - b.httpClient.Transport.(*http.Transport).TLSClientConfig = tlsConfig - } else { - localCACert, err := b.localCACertReader.ReadFile() - if err != nil { - return nil, err - } - - certPool.AppendCertsFromPEM([]byte(localCACert)) - tlsConfig.RootCAs = certPool - - b.httpClient.Transport.(*http.Transport).TLSClientConfig = tlsConfig - } - var err error for i, pem := range pemList { config.PublicKeys[i], err = parsePublicKeyPEM([]byte(pem)) @@ -191,6 +166,10 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ } } + if err := b.updateTLSConfig(config); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + entry, err := logical.StorageEntryJSON(configPath, config) if err != nil { return nil, err @@ -199,6 +178,7 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } + return nil, nil } diff --git a/path_config_test.go b/path_config_test.go index da03a39f..335c49c1 100644 --- a/path_config_test.go +++ b/path_config_test.go @@ -481,7 +481,7 @@ func TestConfig_LocalJWTRenewal(t *testing.T) { token2 := "after-renewal" // Write initial token to the temp file. - ioutil.WriteFile(f.Name(), []byte(token1), 0644) + ioutil.WriteFile(f.Name(), []byte(token1), 0o644) data := map[string]interface{}{ "kubernetes_host": "host", @@ -510,7 +510,7 @@ func TestConfig_LocalJWTRenewal(t *testing.T) { } // Write new value to the token file to simulate renewal. - ioutil.WriteFile(f.Name(), []byte(token2), 0644) + ioutil.WriteFile(f.Name(), []byte(token2), 0o644) // Load again to check we still got the old cached token from memory. conf, err = b.(*kubeAuthBackend).loadConfig(context.Background(), storage) @@ -535,84 +535,3 @@ func TestConfig_LocalJWTRenewal(t *testing.T) { t.Fatalf("got unexpected JWT: expected %#v\n got %#v\n", token2, conf.TokenReviewerJWT) } } - -var testLocalCACert string = `-----BEGIN CERTIFICATE----- -MIIDVDCCAjwCCQDFiyFY1M6afTANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJV -UzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UE -CgwXVmF1bHQgVGVzdGluZyBBdXRob3JpdHkxFDASBgNVBAMMC2V4YW1wbGUubmV0 -MB4XDTIwMDkxODAxMjkxM1oXDTQ1MDkxODAxMjkxM1owbDELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoM -F1ZhdWx0IFRlc3RpbmcgQXV0aG9yaXR5MRQwEgYDVQQDDAtleGFtcGxlLm5ldDCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALCA9oKv+ESRHX2e/iq1PlGr -zD23/MBS0V+fWQDY0hyEqY98CGwRtF6pEcLEYsreArj5/zznsIevLkNOD+beg43y -WpEJlCPgDhGXI/Oima6ooHVEIMaIKLjK7GrSzAb3rNRGACwrR/u/IKaFl+XJG0qx -g8mOZ3fByaAlIk+shVLUcIedNN1tNR+6/4ZpHg7PDjrZXP4XKrmKPTh4yqfu+BtZ -9IY2oyregqEsGW1/3h1NM+LHGVakTV2d/mwMYHhwoq9Y8BD+PemT5z8TmhH/cIk5 -P8Q8ud5/q6YTIJg9TELKebLAeNtRNnNoHeUoRTjiW1MBwNHtgyTTY+H3W/9Dne0C -AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAXmygFkGIBnXxKlsTDiV8RW2iHLgFdZFJ -hcU8UpxZhhaL5JbQl6byfbHjrX31q7ii8uC8FcbW0AEdnEQAb9Ui6a+if7HwXNmI -DTlYl+lMlk9RtWvExw6AEEbg5nCpGaKexm7wJgzYGP9by9pQ7wX/CS7ofCzCK+Al -uSIqjPkMC201ZXH39n1lxxq6BacdYjv8wo4mMzi8iTSQGVWPdjHZVYOClFgN6hoj -8SkrrSe888a0H+i7EknRxC4sLRaMUK/FAvwtXaSZi2djruAtQzQGQ56m1phC2C/k -k9aL00AQ9Y4KTfiJD7LK8YIZDnFKLOCJhYgKCLCOVwOHb7836SNCxA== ------END CERTIFICATE-----` - -var testLocalJWT string = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlZhdWx0IFRlc3QiLCJpYXQiOjExMjM1OH0.GOC8w-MyhorgojB20SPNyH_ECsBjYJH89hjntOxSywA` - -var testRSACert string = `-----BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p -a3ViZUNBMB4XDTE3MDgzMDE5MDgzNloXDTE4MDgzMDE5MDgzNlowLDEXMBUGA1UE -ChMOc3lzdGVtOm1hc3RlcnMxETAPBgNVBAMTCG1pbmlrdWJlMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxD3eM3+WNc4phxAeQxNOmcybKlNJWowuC12u -v+cGJWxxpDx/OoEIxKI5wmgHxEwFCZL545sjfLqyBcgxQR2xSCib+bYzjBtfA6uV -6d/35nurzz21okcMffc5xKMyZhEwt98WAvYWD71Bihz7iGBq5Sw9md6pqnkNoScR -Hhi3Vl94a6D6shwb6nXA2hlwYLcnoKtpe3Ptq6MW6CpfBA8C11q5eeW4xdvrwKt3 -Vd1TgFeEnnqwzUWGapU2uwwUfbRkLTDvrp6791uq0Vo7mzz00xYhV1PLCeAdpJEK -3Vr74FT7jHIbPlzi/qjRBVFKf9IRXnhbjrCl7S0Ayev1Fao4TQIDAQABo4G1MIGy -MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw -DAYDVR0TAQH/BAIwADBzBgNVHREEbDBqgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3Zj -LmNsdXN0ZXIubG9jYWyCFmt1YmVybmV0ZXMuZGVmYXVsdC5zdmOCEmt1YmVybmV0 -ZXMuZGVmYXVsdIIKa3ViZXJuZXRlc4cEwKhjZIcECgAAATANBgkqhkiG9w0BAQsF -AAOCAQEAIw8rKuryhhl527wf9q/VrWixzZ1jCLvyc/60z9rWpXxKFxT8AyCsHirM -F4fHXW4Brcoh/Dc2ci36cUbuywIyxHjgVUG45D4jPPWskY1++ZSfJfSXAuA8eFew -c+No3WPkmZB6ZOZ6q5iPY+FOgDZC7ddWmGuZrle51gBL347cU7H1BrTm6Lm6kXRs -fHRZJX2+B8lnsXsS3QF2BTU0ymuCxCCQxub/GhPZVz3nNNtro1z7/szLUVP1c1/8 -p7HP3k7caxfp346TZ/HgbV9sJEkHP7Ym7n9E7LSyUTSxXwBRPraH1WQzEgFNPSUV -V0n6FBLiejOTPKapJ2F0tIqAyJHFug== ------END CERTIFICATE-----` - -var testECCert string = `-----BEGIN CERTIFICATE----- -MIICZDCCAeugAwIBAgIJALM9NbK8WRuBMAkGByqGSM49BAEwRTELMAkGA1UEBhMC -dXMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp -dHMgUHR5IEx0ZDAeFw0xNzA5MTExNzQ2NDNaFw0yNzA5MDkxNzQ2NDNaMEUxCzAJ -BgNVBAYTAnVzMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l -dCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATcqsBLxKP+ -UHk7Y6ktGGFvfrIfIXHxeZe3Xwt691CWfdmJFvrGzyzW5/AbJIuO1utdOsqUStAm -W/Scfxop/FGadKqR4nAWLNBI4intgnf0r1rzBCSOmanolHqxQPqQ0UOjgacwgaQw -HQYDVR0OBBYEFHxh1pTd8ApEzg0gKMwwt01aA10TMHUGA1UdIwRuMGyAFHxh1pTd -8ApEzg0gKMwwt01aA10ToUmkRzBFMQswCQYDVQQGEwJ1czETMBEGA1UECBMKU29t -ZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkggkAsz01 -srxZG4EwDAYDVR0TBAUwAwEB/zAJBgcqhkjOPQQBA2gAMGUCMCR+CvAoNBhqSe2M -4qWWD/9XX/0qmf0O442Qowcg5MWH1+mwl1s7ozinvbTPDPaYDwIxAM54qKhuL6xt -GxqJpa7Onn15Hu8zTsdzeYBqUUXA6wtn+Pa7197CgUkfty9yc2eeQw== ------END CERTIFICATE-----` - -var testCACert string = ` ------BEGIN CERTIFICATE----- -MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p -a3ViZUNBMB4XDTE3MDgxMDIzMTQ1NVoXDTI3MDgwODIzMTQ1NVowFTETMBEGA1UE -AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN8d -w2p/KXRkm+vzOO0eT1vYBWP7fKsnng9/g5nnXAJlt9NxpOSolRcyItm/04R0E1jx -jpgsdzkybc+QU5ZiszOYN833/D5hCNVAABVivpDd2P8wVKXN46cB99e24etUVBqG -5aR0Ku3IBsJjCN9efhF+XRCA2gy/KaXMdKJhHfdtc8hCr7G9+2wO2G58FLmIfEyH -owviOGt0BSnCtMpsA8ZgGQyfqgSd5u466aCv6oj0MyzsMnfS38niM53Rlv4IY6ol -taYbWXtCNndQ2S687qE0qTCxhE95Bm2Nfkqct4R1798sJz83xNv8hALvxr/vPK/J -2XkIm3oo3YKG4n/CHXcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW -MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQCSkrhE1PczqeqXfRaWayJUbXWPwKFbszO0MhGB1zwnPZq39qjY -ySQiGvnjV3fP+N5CTQAwMNe79Xiw31fSoexgceCPJpraWrTOLdCv04SbGDBapMFM -aezBu9jzZm0CNt60jHXWXuHHVPFX6u7ZR8W+RiBvsT8GZ5U6sNs3aN3M9Vym06BL -aSphIw1v+hRlPfnrlJwUnQp158DRgkt/9ncTa/k88KoIoZAbulaiGB4zHxxkbura -GSlgpZzhHSrBDLuXf65GHwwGxSExhgY5AA/n8rumGVvE8IYohS9yg/jOG0xP2WQH -u/ABoYtOyseO+lgElA8R4PB9MtwgN6c/b0xH ------END CERTIFICATE-----` diff --git a/path_login.go b/path_login.go index d5a95d87..2ad1b6f9 100644 --- a/path_login.go +++ b/path_login.go @@ -107,8 +107,14 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d return nil, err } + client, err := b.getHTTPClient() + if err != nil { + b.Logger().Error(`Failed to get the HTTP client`, "err", err) + return nil, logical.ErrUnrecoverable + } + // look up the JWT token in the kubernetes API - err = serviceAccount.lookup(ctx, b.httpClient, jwtStr, b.reviewFactory(config)) + err = serviceAccount.lookup(ctx, client, jwtStr, b.reviewFactory(config)) if err != nil { b.Logger().Debug(`login unauthorized`, "err", err) diff --git a/path_role_test.go b/path_role_test.go index 3b63113c..317b34c6 100644 --- a/path_role_test.go +++ b/path_role_test.go @@ -19,6 +19,9 @@ func getBackend(t *testing.T) (logical.Backend, logical.Storage) { defaultLeaseTTLVal := time.Hour * 12 maxLeaseTTLVal := time.Hour * 24 b := Backend() + if err := b.validateHTTPClientInit(); err != nil { + t.Fatalf("unable to create backend: %v", err) + } config := &logical.BackendConfig{ Logger: logging.NewVaultLogger(log.Trace), @@ -29,9 +32,8 @@ func getBackend(t *testing.T) (logical.Backend, logical.Storage) { }, StorageView: &logical.InmemStorage{}, } - err := b.Setup(context.Background(), config) - if err != nil { - t.Fatalf("unable to create backend: %v", err) + if err := b.Setup(context.Background(), config); err != nil { + t.Fatalf("unable to setup backend: %v", err) } return b, config.StorageView