From 13b869e22a059f3891192ba74d68c5afbf7a0a77 Mon Sep 17 00:00:00 2001 From: "Jim Wang @ Intel" Date: Mon, 22 Feb 2021 15:57:54 -0700 Subject: [PATCH] feat(security): Enable Vault's Consul secrets engine (#3179) * feat(security): Enable Vault's consul secrets engine - Add Secret Engine Enabler so that it can be re-used for both of Consul and KV secrets engines - Hookup the code in secretstore-setup so that both KV and Consul secret engines are enabled Closes: #3154 * refactor: move secretsengine's enabler to secretstore-setup and resovled merging conflicts Refactor to move enabler to secretstore-setup in edgex-go Rebased and resolved the merging conflicts Use the secret client from go-mod-secret Signed-off-by: Jim Wang --- internal/security/secretstore/constants.go | 6 + internal/security/secretstore/init.go | 43 ++---- .../secretstore/secretsengine/enabler.go | 93 ++++++++++++ .../secretstore/secretsengine/enabler_test.go | 137 ++++++++++++++++++ 4 files changed, 250 insertions(+), 29 deletions(-) create mode 100644 internal/security/secretstore/secretsengine/enabler.go create mode 100644 internal/security/secretstore/secretsengine/enabler_test.go diff --git a/internal/security/secretstore/constants.go b/internal/security/secretstore/constants.go index 7caaef66a1..def38e9daf 100644 --- a/internal/security/secretstore/constants.go +++ b/internal/security/secretstore/constants.go @@ -1,4 +1,5 @@ /******************************************************************************* + * Copyright 2021 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -18,6 +19,11 @@ package secretstore const ( + // KVSecretsEngineMountPoint is the name of the mount point base for Vault's key-value secrets engine + KVSecretsEngineMountPoint = "secret" + // ConsulSecretEngineMountPoint is the name of the mount point base for Vault's Consul secrets engine + ConsulSecretEngineMountPoint = "consul" + VaultToken = "X-Vault-Token" TokenCreatorPolicyName = "privileged-token-creator" diff --git a/internal/security/secretstore/init.go b/internal/security/secretstore/init.go index 8c363c24f2..64942603fe 100644 --- a/internal/security/secretstore/init.go +++ b/internal/security/secretstore/init.go @@ -1,6 +1,6 @@ /******************************************************************************* + * Copyright 2021 Intel Corporation * Copyright 2019 Dell Inc. - * Copyright 2021 Intel Inc. * * 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 @@ -34,9 +34,7 @@ import ( "github.com/edgexfoundry/edgex-go/internal/security/pipedhexreader" "github.com/edgexfoundry/edgex-go/internal/security/secretstore/config" "github.com/edgexfoundry/edgex-go/internal/security/secretstore/container" - "github.com/edgexfoundry/go-mod-secrets/v2/pkg" - "github.com/edgexfoundry/go-mod-secrets/v2/pkg/types" - "github.com/edgexfoundry/go-mod-secrets/v2/secrets" + "github.com/edgexfoundry/edgex-go/internal/security/secretstore/secretsengine" bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" @@ -44,7 +42,10 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/v2/pkg" "github.com/edgexfoundry/go-mod-secrets/v2/pkg/token/fileioperformer" + "github.com/edgexfoundry/go-mod-secrets/v2/pkg/types" + "github.com/edgexfoundry/go-mod-secrets/v2/secrets" ) type Bootstrap struct { @@ -317,11 +318,19 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s } // Enable KV secret engine - if err := enableKVSecretsEngine(lc, client, rootToken); err != nil { + if err := secretsengine.New(KVSecretsEngineMountPoint, secretsengine.KeyValue). + Enable(&rootToken, lc, client); err != nil { lc.Errorf("failed to enable KV secrets engine: %s", err.Error()) os.Exit(1) } + // Enable Consul secret engine + if err := secretsengine.New(ConsulSecretEngineMountPoint, secretsengine.Consul). + Enable(&rootToken, lc, client); err != nil { + lc.Errorf("failed to enable Consul secrets engine: %s", err.Error()) + os.Exit(1) + } + // credential creation gen := NewPasswordGenerator(lc, secretStoreConfig.PasswordProvider, secretStoreConfig.PasswordProviderArgs) cred := NewCred(httpCaller, rootToken, gen, secretStoreConfig.GetBaseURL(), lc) @@ -528,30 +537,6 @@ func makeTokenIssuingToken( return revokeIssuingTokenFuc, nil } -func enableKVSecretsEngine( - lc logger.LoggingClient, - client secrets.SecretStoreClient, - rootToken string) error { - - installed, err := client.CheckSecretEngineInstalled(rootToken, "secret/", "kv") - if err != nil { - lc.Errorf("failed call to check if kv secrets engine is installed: %s", err.Error()) - return err - } - if !installed { - lc.Info("enabling KV secrets engine for the first time...") - // Enable KV version 1 at /v1/secret path (/v1 prefix supplied by Vault) - err := client.EnableKVSecretEngine(rootToken, "secret", "1") - if err != nil { - lc.Errorf("failed call to enable KV secrets engine: %s", err.Error()) - return err - } - } else { - lc.Info("KV secrets engine already enabled...") - } - return nil -} - func loadInitResponse( lc logger.LoggingClient, fileOpener fileioperformer.FileIoPerformer, diff --git a/internal/security/secretstore/secretsengine/enabler.go b/internal/security/secretstore/secretsengine/enabler.go new file mode 100644 index 0000000000..0e60e09cbb --- /dev/null +++ b/internal/security/secretstore/secretsengine/enabler.go @@ -0,0 +1,93 @@ +/******************************************************************************* +* Copyright 2021 Intel Corporation +* +* 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 secretsengine + +import ( + "fmt" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/v2/secrets" +) + +const ( + // Vault's secrets engine type related constants + KeyValue = "kv" + Consul = "consul" + + // kvVersion is the version of key-value secret storage used + // currently we use version 1 from Vault + kvVersion = "1" + + // defaultConsulTokenLeaseTtl is the default time-to-live value for consul token + // currently we don't set any lease time-to-live limit for Consul tokens + // this will be changed in future for phase 3 based on the ADR + defaultConsulTokenLeaseTtl = "0" +) + +// SecretsEngine is the metadata for secretstore secret engine enabler +type SecretsEngine struct { + mountPoint string + engineType string +} + +// New creates an instance for SecretsEngine with mountPoint and engineType +func New(mountPoint string, engineType string) SecretsEngine { + return SecretsEngine{mountPoint: mountPoint, engineType: engineType} +} + +// Enable enables the specified secrets engine for the secretstore +// the rootToken is required and returns error if not provided or invalid token provided +// also returns error if unsupported / unknown secretsEngineType is used +func (eng SecretsEngine) Enable(rootToken *string, + lc logger.LoggingClient, + client secrets.SecretStoreClient) error { + if rootToken == nil { + return fmt.Errorf("rootToken is required") + } + + // the data returned from GET of check installed secrets engine API of Vault is + // the mountPoint with trailing slash(/), eg. "secret/" for kv's mountPoint "secret" + checkMountPoint := eng.mountPoint + "/" + installed, err := client.CheckSecretEngineInstalled(*rootToken, checkMountPoint, eng.engineType) + if err != nil { + return fmt.Errorf("failed call to check if %s secrets engine is installed: %s", + eng.engineType, err.Error()) + } + + if !installed { + lc.Infof("enabling %s secrets engine for the first time...", eng.engineType) + switch eng.engineType { + case KeyValue: + // Enable KV storage version 1 at /v1/{eng.path} path (/v1 prefix supplied by Vault) + if err := client.EnableKVSecretEngine(*rootToken, eng.mountPoint, kvVersion); err != nil { + return fmt.Errorf("failed to enable KV version %s secrets engine: %s", kvVersion, err.Error()) + } + lc.Infof("KeyValue secrets engine with version %s enabled", kvVersion) + case Consul: + // Enable Consul secrets storage at /consul path + if err := client.EnableConsulSecretEngine(*rootToken, + eng.mountPoint, defaultConsulTokenLeaseTtl); err != nil { + return fmt.Errorf("failed to enable Consul secrets engine: %s", err.Error()) + } + lc.Infof("Consul secrets engine with config default_ttl = %s enabled", defaultConsulTokenLeaseTtl) + default: + return fmt.Errorf("Unsupported secrets engine type: %s", eng.engineType) + } + } else { + lc.Infof("%s secrets engine already enabled...", eng.engineType) + } + return nil +} diff --git a/internal/security/secretstore/secretsengine/enabler_test.go b/internal/security/secretstore/secretsengine/enabler_test.go new file mode 100644 index 0000000000..3690bcda54 --- /dev/null +++ b/internal/security/secretstore/secretsengine/enabler_test.go @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 secretsengine + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/v2/secrets/mocks" +) + +func TestNewSecretsEngine(t *testing.T) { + tests := []struct { + name string + mountPath string + engineType string + }{ + {"New kv type of secrets engine", "kv-1-test/", KeyValue}, + {"New consul type of secrets engine", "consul-test/", Consul}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + instance := New(tt.mountPath, tt.engineType) + require.Equal(t, tt.mountPath, instance.mountPoint) + require.Equal(t, tt.engineType, instance.engineType) + }) + } +} + +func TestEnableSecretsEngine(t *testing.T) { + lc := logger.MockLogger{} + testToken := "fake-token" + unsupportedEngTypeErr := errors.New("Unsupported secrets engine type") + + tests := []struct { + name string + rootToken *string + mountPoint string + engineType string + kvInstalled bool + consulInstalled bool + clientCallFailed bool + expectError bool + }{ + {"Ok:Enable kv secrets engine not installed yet with client call ok", &testToken, "kv-1-test", + KeyValue, false, false, false, false}, + {"Ok:Enable consul secrets engine not installed yet with client call ok", &testToken, "consul-test", + Consul, false, false, false, false}, + {"Ok:Enable kv secrets engine already installed with client call ok (1)", &testToken, "kv-1-test", + KeyValue, true, false, false, false}, + {"Ok:Enable consul secrets engine already installed with client call ok (1)", &testToken, "consul-test", + Consul, false, true, false, false}, + {"Ok:Enable kv secrets engine already installed with client call ok (2)", &testToken, "kv-1-test", + KeyValue, true, true, false, false}, + {"Ok:Enable consul secrets engine already installed with client call ok (2)", &testToken, "consul-test", + Consul, true, true, false, false}, + {"Bad:Enable kv secrets engine not installed yet but client call failed", &testToken, "kv-1-test", + KeyValue, false, false, true, true}, + {"Bad:Enable consul secrets engine not installed yet but client call failed", &testToken, "consul-test", + Consul, false, false, true, true}, + {"Bad:Enable kv secrets engine already installed but client call failed (1)", &testToken, "kv-1-test", + KeyValue, true, false, true, true}, + {"Bad:Enable consul secrets engine already installed but client call failed (1)", &testToken, "consul-test", + Consul, false, true, true, true}, + {"Bad:Enable kv secrets engine already installed but client call failed (2)", &testToken, "kv-1-test", + KeyValue, true, true, true, true}, + {"Bad:Enable consul secrets engine already installed but client call failed (2)", &testToken, "consul-test", + Consul, true, true, true, true}, + {"Bad:Enable kv secrets engine with nil token", nil, "kv-1-test", + KeyValue, false, true, false, true}, + {"Bad:Enable consul secrets engine with nil token", nil, "consul-test", + Consul, true, false, false, true}, + {"Bad:Unsupported secrets engine type", &testToken, "whatever", + "unsupported", false, false, false, true}, + } + + for _, test := range tests { + // this local copy is to ensure test is thread-safe as we are running in parallel + localTest := test + t.Run(localTest.name, func(t *testing.T) { + // run all tests in parallel + t.Parallel() + + var chkErr error + var enableClientErr error + + // to simplify testing, assume both errors when client calls failed + if localTest.clientCallFailed { + chkErr = errors.New("CheckSecretEngineInstalled called failed") + enableClientErr = errors.New("EnableKVSecretEngine called failed") + } + + mockClient := &mocks.SecretStoreClient{} + mockClient.On("CheckSecretEngineInstalled", mock.Anything, mock.Anything, KeyValue). + Return(localTest.kvInstalled, chkErr) + mockClient.On("CheckSecretEngineInstalled", mock.Anything, mock.Anything, Consul). + Return(localTest.consulInstalled, chkErr) + mockClient.On("CheckSecretEngineInstalled", mock.Anything, mock.Anything, mock.Anything). + Return(false, chkErr) + mockClient.On("EnableKVSecretEngine", mock.Anything, localTest.mountPoint, kvVersion). + Return(enableClientErr) + mockClient.On("EnableKVSecretEngine", mock.Anything, mock.Anything, mock.Anything). + Return(unsupportedEngTypeErr) + mockClient.On("EnableConsulSecretEngine", mock.Anything, localTest.mountPoint, defaultConsulTokenLeaseTtl). + Return(enableClientErr) + mockClient.On("EnableConsulSecretEngine", mock.Anything, mock.Anything, mock.Anything). + Return(unsupportedEngTypeErr) + + err := New(localTest.mountPoint, localTest.engineType). + Enable(localTest.rootToken, lc, mockClient) + + if localTest.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +}