diff --git a/e2e/bao-provider.bats b/e2e/bao-provider.bats index a9d162e..e520b86 100644 --- a/e2e/bao-provider.bats +++ b/e2e/bao-provider.bats @@ -157,7 +157,7 @@ assert_output_contains() { run_output=$(./secret-init env | grep 'API_KEY\|RABBITMQ_USERNAME\|RABBITMQ_PASSWORD') assert_success - + # Get the lease ID after renewing the secret secret_info_after=$(docker exec "$bao_container_name" bao read -format=json database/creds/my-role) lease_id_after=$(echo "$secret_info_after" | jq -r '.lease_id') @@ -173,12 +173,13 @@ assert_output_contains() { } @test "secrets successfully loaded from bao using BAO_FROM_PATH" { + setup_bao_provider + # unset env vars to ensure secret-init will utilize BAO_FROM_PATH unset API_KEY unset RABBITMQ_USERNAME unset RABBITMQ_PASSWORD - setup_bao_provider set_bao_token 227e1cce-6bf7-30bb-2d2a-acc854318caf add_secrets_to_bao export BAO_FROM_PATH="secret/data/test/api,secret/data/test/rabbitmq" diff --git a/e2e/vault-provider.bats b/e2e/vault-provider.bats index a12565a..a65fff5 100644 --- a/e2e/vault-provider.bats +++ b/e2e/vault-provider.bats @@ -157,7 +157,7 @@ assert_output_contains() { run_output=$(./secret-init env | grep 'MYSQL_PASSWORD\|AWS_SECRET_ACCESS_KEY\|AWS_ACCESS_KEY_ID') assert_success - + # Get the lease duration after renewal secret_info_after=$(docker exec "$vault_container_name" vault read -format=json database/creds/my-role) lease_id_after=$(echo "$secret_info_after" | jq -r '.lease_id') @@ -173,12 +173,13 @@ assert_output_contains() { } @test "secrets successfully loaded from vault using VAULT_FROM_PATH" { + setup_vault_provider + # unset env vars to ensure secret-init will utilize VAULT_FROM_PATH unset MYSQL_PASSWORD unset AWS_SECRET_ACCESS_KEY unset AWS_ACCESS_KEY_ID - setup_vault_provider set_vault_token 227e1cce-6bf7-30bb-2d2a-acc854318caf add_secrets_to_vault export VAULT_FROM_PATH="secret/data/test/mysql,secret/data/test/aws" diff --git a/env_store.go b/env_store.go index 958b543..70c5685 100644 --- a/env_store.go +++ b/env_store.go @@ -97,6 +97,7 @@ func (s *EnvStore) GetSecretReferences() map[string][]string { } } } + checkFromPath(s.data, &secretReferences) return secretReferences } @@ -190,10 +191,30 @@ func (s *EnvStore) workaroundForBao(ctx context.Context, vaultPaths []string) ([ // ConvertProviderSecrets converts the loaded secrets to environment variables func (s *EnvStore) ConvertProviderSecrets(providerSecrets []provider.Secret) []string { var secretsEnv []string - for _, secret := range providerSecrets { secretsEnv = append(secretsEnv, fmt.Sprintf("%s=%s", secret.Key, secret.Value)) } return secretsEnv } + +// Handle the edge case where *_FROM_PATH is defined but no direct env-var references are present +// in this case the provider should be created with an empty list of secret references +// leaving the secret injection to the provider +func checkFromPath(environ map[string]string, secretReferences *map[string][]string) { + if environ == nil || secretReferences == nil { + return + } + + if _, ok := (*secretReferences)[vault.ProviderType]; !ok { + if _, ok := environ[vault.FromPathEnv]; ok { + (*secretReferences)[vault.ProviderType] = []string{} + } + } + + if _, ok := (*secretReferences)[bao.ProviderType]; !ok { + if _, ok := environ[bao.FromPathEnv]; ok { + (*secretReferences)[bao.ProviderType] = []string{} + } + } +} diff --git a/pkg/provider/bao/bao.go b/pkg/provider/bao/bao.go index 6d98061..eae968d 100644 --- a/pkg/provider/bao/bao.go +++ b/pkg/provider/bao/bao.go @@ -28,6 +28,7 @@ import ( "github.com/bank-vaults/secret-init/pkg/common" "github.com/bank-vaults/secret-init/pkg/provider" + "github.com/bank-vaults/secret-init/pkg/utils" ) const ( @@ -125,14 +126,18 @@ func NewProvider(_ context.Context, appConfig *common.Config) (provider.Provider // returns: []provider.Secret{provider.Secret{Path: "MYSQL_PASSWORD", Value: "password"}} func (p *Provider) LoadSecrets(ctx context.Context, paths []string) ([]provider.Secret, error) { sanitized := sanitized{login: p.isLogin} - baoEnviron := parsePathsToMap(paths) - secretInjector := injector.NewSecretInjector(p.injectorConfig, p.client, p.secretRenewer, slog.Default()) inject := func(key, value string) { + // Check for key duplication + if utils.IsKeyDuplicated(&sanitized.secrets, key) { + slog.Warn(fmt.Sprintf("Deduplication detected for key: %s", key)) + return + } + sanitized.append(key, value) } - err := secretInjector.InjectSecretsFromBao(baoEnviron, inject) + err := secretInjector.InjectSecretsFromBao(parsePathsToMap(paths), inject) if err != nil { return nil, fmt.Errorf("failed to inject secrets from bao: %w", err) } @@ -145,7 +150,7 @@ func (p *Provider) LoadSecrets(ctx context.Context, paths []string) ([]provider. } if p.revokeToken { - // ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self- + // ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self err := p.client.RawClient().Auth().Token().RevokeSelfWithContext(ctx, p.client.RawClient().Token()) if err != nil { // Do not exit on error, token revoking can be denied by policy diff --git a/pkg/provider/bao/config.go b/pkg/provider/bao/config.go index 427ca66..b198a04 100644 --- a/pkg/provider/bao/config.go +++ b/pkg/provider/bao/config.go @@ -58,7 +58,7 @@ const ( passthroughEnv = "BAO_PASSTHROUGH" logLevelEnv = "BAO_LOG_LEVEL" revokeTokenEnv = "BAO_REVOKE_TOKEN" - fromPathEnv = "BAO_FROM_PATH" + FromPathEnv = "BAO_FROM_PATH" ) type Config struct { @@ -110,7 +110,7 @@ var sanitizeEnvmap = map[string]envType{ passthroughEnv: {login: false}, logLevelEnv: {login: false}, revokeTokenEnv: {login: false}, - fromPathEnv: {login: false}, + FromPathEnv: {login: false}, } func LoadConfig() (*Config, error) { @@ -182,7 +182,7 @@ func LoadConfig() (*Config, error) { TransitPath: os.Getenv(transitPathEnv), TransitBatchSize: cast.ToInt(os.Getenv(transitBatchSizeEnv)), IgnoreMissingSecrets: cast.ToBool(os.Getenv(ignoreMissingSecretsEnv)), // Used both for reading secrets and transit encryption - FromPath: os.Getenv(fromPathEnv), + FromPath: os.Getenv(FromPathEnv), RevokeToken: cast.ToBool(os.Getenv(revokeTokenEnv)), }, nil } diff --git a/pkg/provider/bao/config_test.go b/pkg/provider/bao/config_test.go index a25b6e5..510a39f 100644 --- a/pkg/provider/bao/config_test.go +++ b/pkg/provider/bao/config_test.go @@ -43,7 +43,7 @@ func TestConfig(t *testing.T) { transitBatchSizeEnv: "10", ignoreMissingSecretsEnv: "true", revokeTokenEnv: "true", - fromPathEnv: "secret/data/test", + FromPathEnv: "secret/data/test", }, wantConfig: &Config{ IsLogin: true, diff --git a/pkg/provider/vault/config.go b/pkg/provider/vault/config.go index 5b5711b..6179e28 100644 --- a/pkg/provider/vault/config.go +++ b/pkg/provider/vault/config.go @@ -58,7 +58,7 @@ const ( passthroughEnv = "VAULT_PASSTHROUGH" logLevelEnv = "VAULT_LOG_LEVEL" revokeTokenEnv = "VAULT_REVOKE_TOKEN" - fromPathEnv = "VAULT_FROM_PATH" + FromPathEnv = "VAULT_FROM_PATH" ) type Config struct { @@ -110,7 +110,7 @@ var sanitizeEnvmap = map[string]envType{ passthroughEnv: {login: false}, logLevelEnv: {login: false}, revokeTokenEnv: {login: false}, - fromPathEnv: {login: false}, + FromPathEnv: {login: false}, } func LoadConfig() (*Config, error) { @@ -176,7 +176,7 @@ func LoadConfig() (*Config, error) { TransitPath: os.Getenv(transitPathEnv), TransitBatchSize: cast.ToInt(os.Getenv(transitBatchSizeEnv)), IgnoreMissingSecrets: cast.ToBool(os.Getenv(ignoreMissingSecretsEnv)), // Used both for reading secrets and transit encryption - FromPath: os.Getenv(fromPathEnv), + FromPath: os.Getenv(FromPathEnv), RevokeToken: cast.ToBool(os.Getenv(revokeTokenEnv)), }, nil } diff --git a/pkg/provider/vault/config_test.go b/pkg/provider/vault/config_test.go index 07f75bf..9579f53 100644 --- a/pkg/provider/vault/config_test.go +++ b/pkg/provider/vault/config_test.go @@ -43,7 +43,7 @@ func TestConfig(t *testing.T) { transitBatchSizeEnv: "10", ignoreMissingSecretsEnv: "true", revokeTokenEnv: "true", - fromPathEnv: "secret/data/test", + FromPathEnv: "secret/data/test", }, wantConfig: &Config{ IsLogin: true, diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index 27e6512..1a6cb9f 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -28,6 +28,7 @@ import ( "github.com/bank-vaults/secret-init/pkg/common" "github.com/bank-vaults/secret-init/pkg/provider" + "github.com/bank-vaults/secret-init/pkg/utils" ) const ( @@ -125,14 +126,18 @@ func NewProvider(_ context.Context, appConfig *common.Config) (provider.Provider // returns: []provider.Secret{provider.Secret{Path: "MYSQL_PASSWORD", Value: "password"}} func (p *Provider) LoadSecrets(ctx context.Context, paths []string) ([]provider.Secret, error) { sanitized := sanitized{login: p.isLogin} - vaultEnviron := parsePathsToMap(paths) - secretInjector := injector.NewSecretInjector(p.injectorConfig, p.client, p.secretRenewer, slog.Default()) inject := func(key, value string) { + // Check for key duplication + if utils.IsKeyDuplicated(&sanitized.secrets, key) { + slog.Warn(fmt.Sprintf("Duplication detected for key: %s, skipping it", key)) + return + } + sanitized.append(key, value) } - err := secretInjector.InjectSecretsFromVault(vaultEnviron, inject) + err := secretInjector.InjectSecretsFromVault(parsePathsToMap(paths), inject) if err != nil { return nil, fmt.Errorf("failed to inject secrets from vault: %w", err) } @@ -145,7 +150,7 @@ func (p *Provider) LoadSecrets(ctx context.Context, paths []string) ([]provider. } if p.revokeToken { - // ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self- + // ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self err := p.client.RawClient().Auth().Token().RevokeSelfWithContext(ctx, p.client.RawClient().Token()) if err != nil { // Do not exit on error, token revoking can be denied by policy diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go new file mode 100644 index 0000000..a9f7991 --- /dev/null +++ b/pkg/utils/helpers.go @@ -0,0 +1,32 @@ +// Copyright © 2024 Bank-Vaults Maintainers +// +// 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 utils + +import "github.com/bank-vaults/secret-init/pkg/provider" + +// Checks whether a key is duplicated in the secrets slice +func IsKeyDuplicated(secrets *[]provider.Secret, searchKey string) bool { + if secrets == nil { + return false + } + + for _, secret := range *secrets { + if secret.Key == searchKey { + return true + } + } + + return false +}