From 3fdaf864c09ee404b18228ff20979c1c6a72f211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 27 Nov 2024 21:46:04 +0100 Subject: [PATCH 1/3] chore(cmd): Introduce option for safe verbosness, and sanitation function --- cmd/generate.go | 5 ++++- pkg/utils/util.go | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cmd/generate.go b/cmd/generate.go index a6b1845d..6a842091 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -21,6 +21,7 @@ func NewGenerateCommand() *cobra.Command { const StdIn = "-" var configPath, secretName string var verboseOutput bool + var verboseUnsafe bool var disableCache bool var command = &cobra.Command{ @@ -64,6 +65,7 @@ func NewGenerateCommand() *cobra.Command { v := viper.New() viper.Set("verboseOutput", verboseOutput) + viper.Set("verboseUnsafe", verboseUnsafe) viper.Set("disableCache", disableCache) cmdConfig, err := config.New(v, &config.Options{ SecretName: secretName, @@ -117,7 +119,8 @@ func NewGenerateCommand() *cobra.Command { command.Flags().StringVarP(&configPath, "config-path", "c", "", "path to a file containing Vault configuration (YAML, JSON, envfile) to use") command.Flags().StringVarP(&secretName, "secret-name", "s", "", "name of a Kubernetes Secret in the argocd namespace containing Vault configuration data in the argocd namespace of your ArgoCD host (Only available when used in ArgoCD). The namespace can be overridden by using the format :") - command.Flags().BoolVar(&verboseOutput, "verbose-sensitive-output", false, "enable verbose mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") + command.Flags().BoolVar(&verboseOutput, "verboseOutput", false, "enable verboseOutput mode for detailed info to help with debugging. Omits sensitive data (credentials), logged to stderr") + command.Flags().BoolVar(&verboseUnsafe, "verboseOutput-sensitive-output", false, "enable verboseOutput mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") command.Flags().BoolVar(&disableCache, "disable-token-cache", false, "disable the automatic token cache feature that store tokens locally") return command } diff --git a/pkg/utils/util.go b/pkg/utils/util.go index 899d5621..a01d935c 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -63,7 +63,7 @@ func ReadExistingToken(identifier string) ([]byte, error) { func LoginWithCachedToken(vaultClient *api.Client, identifier string) error { if viper.GetBool("disableCache") { return fmt.Errorf("Token cache feature is disabled") - } else { + } else { byteValue, err := ReadExistingToken(identifier) if err != nil { return err @@ -94,7 +94,7 @@ func SetToken(vaultClient *api.Client, identifier string, token string) error { if viper.GetBool("disableCache") { return fmt.Errorf("Token cache feature is disabled") - } else { + } else { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("Could not access home directory: %s", err.Error()) @@ -147,3 +147,13 @@ func VerboseToStdErr(format string, message ...interface{}) { log.Printf(fmt.Sprintf("%s\n", format), message...) } } + +// SanitizeUnsafe replaces the message data with redacted literal unless `--verbose-sensitive-output` was passed +func SanitizeUnsafe(message interface{}) interface{} { + if viper.GetBool("verboseOutput") && !viper.GetBool("verboseUnsafe") { + messageLen := len(fmt.Sprintf("%s", message)) + return fmt.Sprintf("***REDACTED(%v characters)***", messageLen) + } else { + return message + } +} From 427d4df4efd76e12384dea852eb7531dba18c576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 27 Nov 2024 22:23:32 +0100 Subject: [PATCH 2/3] feat(cmd): Sanitize all verbose logging calls to redact the secrets --- pkg/auth/vault/approle.go | 11 +++++++---- pkg/auth/vault/github.go | 8 ++++---- pkg/auth/vault/kubernetes.go | 11 +++++++---- pkg/auth/vault/userpass.go | 11 +++++++---- pkg/backends/awssecretsmanager.go | 2 +- pkg/backends/azurekeyvault.go | 8 ++++---- pkg/backends/delineasecretsmanager.go | 4 ++-- pkg/backends/gcpsecretmanager.go | 2 +- pkg/backends/ibmsecretsmanager.go | 8 ++++---- pkg/backends/keepersecretsmanager.go | 2 +- pkg/backends/kubernetessecret.go | 4 ++-- pkg/backends/localsecretmanager.go | 2 +- pkg/backends/onepasswordconnect.go | 2 +- pkg/backends/vault.go | 2 +- pkg/backends/yandexcloudlockbox.go | 2 +- pkg/kube/util.go | 4 ++-- 16 files changed, 46 insertions(+), 37 deletions(-) diff --git a/pkg/auth/vault/approle.go b/pkg/auth/vault/approle.go index 913f2c66..26d77174 100644 --- a/pkg/auth/vault/approle.go +++ b/pkg/auth/vault/approle.go @@ -35,7 +35,7 @@ func NewAppRoleAuth(roleID, secretID, mountPath string) *AppRoleAuth { func (a *AppRoleAuth) Authenticate(vaultClient *api.Client) error { err := utils.LoginWithCachedToken(vaultClient, fmt.Sprintf("approle_%s", a.RoleID)) if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) + utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", utils.SanitizeUnsafe(err)) } else { return nil } @@ -45,18 +45,21 @@ func (a *AppRoleAuth) Authenticate(vaultClient *api.Client) error { "secret_id": a.SecretID, } - utils.VerboseToStdErr("Hashicorp Vault authenticating with role ID %s and secret ID %s at path %s", a.RoleID, a.SecretID, a.MountPath) + utils.VerboseToStdErr( + "Hashicorp Vault authenticating with role ID %s and secret ID %s at path %s", + utils.SanitizeUnsafe(a.RoleID), utils.SanitizeUnsafe(a.SecretID), a.MountPath, + ) data, err := vaultClient.Logical().Write(fmt.Sprintf("%s/login", a.MountPath), payload) if err != nil { return err } - utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) + utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", utils.SanitizeUnsafe(data)) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. err = utils.SetToken(vaultClient, fmt.Sprintf("approle_%s", a.RoleID), data.Auth.ClientToken) if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) + utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", utils.SanitizeUnsafe(err)) } return nil diff --git a/pkg/auth/vault/github.go b/pkg/auth/vault/github.go index dfd6403e..d0464e15 100644 --- a/pkg/auth/vault/github.go +++ b/pkg/auth/vault/github.go @@ -34,7 +34,7 @@ func NewGithubAuth(token, mountPath string) *GithubAuth { func (g *GithubAuth) Authenticate(vaultClient *api.Client) error { err := utils.LoginWithCachedToken(vaultClient, "github") if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) + utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", utils.SanitizeUnsafe(err)) } else { return nil } @@ -43,18 +43,18 @@ func (g *GithubAuth) Authenticate(vaultClient *api.Client) error { "token": g.AccessToken, } - utils.VerboseToStdErr("Hashicorp Vault authenticating with Github token %s", g.AccessToken) + utils.VerboseToStdErr("Hashicorp Vault authenticating with Github token %s", utils.SanitizeUnsafe(g.AccessToken)) data, err := vaultClient.Logical().Write(fmt.Sprintf("%s/login", g.MountPath), payload) if err != nil { return err } - utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) + utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", utils.SanitizeUnsafe(data)) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. err = utils.SetToken(vaultClient, "github", data.Auth.ClientToken) if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) + utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", utils.SanitizeUnsafe(err)) } return nil diff --git a/pkg/auth/vault/kubernetes.go b/pkg/auth/vault/kubernetes.go index 196404df..7bb5cff8 100644 --- a/pkg/auth/vault/kubernetes.go +++ b/pkg/auth/vault/kubernetes.go @@ -41,7 +41,7 @@ func NewK8sAuth(role, mountPath, tokenPath string) *K8sAuth { func (k *K8sAuth) Authenticate(vaultClient *api.Client) error { err := utils.LoginWithCachedToken(vaultClient, "kubernetes") if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) + utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", utils.SanitizeUnsafe(err)) } else { return nil } @@ -61,18 +61,21 @@ func (k *K8sAuth) Authenticate(vaultClient *api.Client) error { kubeAuthPath = k.MountPath } - utils.VerboseToStdErr("Hashicorp Vault authenticating with Vault role %s using Kubernetes service account token %s read from %s", k.Role, serviceAccountFile, token) + utils.VerboseToStdErr( + "Hashicorp Vault authenticating with Vault role %s using Kubernetes service account token %s read from %s", + k.Role, serviceAccountFile, utils.SanitizeUnsafe(token), + ) data, err := vaultClient.Logical().Write(fmt.Sprintf("%s/login", kubeAuthPath), payload) if err != nil { return err } - utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) + utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", utils.SanitizeUnsafe(data)) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. err = utils.SetToken(vaultClient, "kubernetes", data.Auth.ClientToken) if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) + utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", utils.SanitizeUnsafe(err)) } return nil diff --git a/pkg/auth/vault/userpass.go b/pkg/auth/vault/userpass.go index 027d20f9..3b9e3d9b 100644 --- a/pkg/auth/vault/userpass.go +++ b/pkg/auth/vault/userpass.go @@ -35,7 +35,7 @@ func NewUserPassAuth(username, password, mountPath string) *UserPassAuth { func (a *UserPassAuth) Authenticate(vaultClient *api.Client) error { err := utils.LoginWithCachedToken(vaultClient, fmt.Sprintf("userpass_%s", a.Username)) if err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) + utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", utils.SanitizeUnsafe(err)) } else { return nil } @@ -44,17 +44,20 @@ func (a *UserPassAuth) Authenticate(vaultClient *api.Client) error { "password": a.Password, } - utils.VerboseToStdErr("Hashicorp Vault authenticating with username %s and password %s", a.Username, a.Password) + utils.VerboseToStdErr( + "Hashicorp Vault authenticating with username %s and password %s", + utils.SanitizeUnsafe(a.Username), utils.SanitizeUnsafe(a.Password), + ) data, err := vaultClient.Logical().Write(fmt.Sprintf("%s/login/%s", a.MountPath, a.Username), payload) if err != nil { return err } - utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) + utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", utils.SanitizeUnsafe(data)) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. if err = utils.SetToken(vaultClient, fmt.Sprintf("userpass_%s", a.Username), data.Auth.ClientToken); err != nil { - utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) + utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", utils.SanitizeUnsafe(err)) } return nil diff --git a/pkg/backends/awssecretsmanager.go b/pkg/backends/awssecretsmanager.go index 08efb77c..981da225 100644 --- a/pkg/backends/awssecretsmanager.go +++ b/pkg/backends/awssecretsmanager.go @@ -70,7 +70,7 @@ func (a *AWSSecretsManager) GetSecrets(path string, version string, annotations return nil, err } - utils.VerboseToStdErr("AWS Secrets Manager get secret response %v", result) + utils.VerboseToStdErr("AWS Secrets Manager get secret response %v", utils.SanitizeUnsafe(result)) var dat map[string]interface{} diff --git a/pkg/backends/azurekeyvault.go b/pkg/backends/azurekeyvault.go index ec68ad29..0d96be36 100644 --- a/pkg/backends/azurekeyvault.go +++ b/pkg/backends/azurekeyvault.go @@ -73,16 +73,16 @@ func (a *AzureKeyVault) GetSecrets(kvpath string, version string, _ map[string]s if err != nil { return nil, err } - utils.VerboseToStdErr("Azure Key Vault get secret response %v", secret) + utils.VerboseToStdErr("Azure Key Vault get secret response %v", utils.SanitizeUnsafe(secret)) data[name] = *secret.Value } else { verboseOptionalVersion("Azure Key Vault getting secret %s from vault %s", version, name, kvpath) secret, err := client.GetSecret(ctx, name, version, nil) if err != nil || !*secretVersion.Attributes.Enabled { - utils.VerboseToStdErr("Azure Key Vault get versioned secret not found %s", err) + utils.VerboseToStdErr("Azure Key Vault get versioned secret not found %s", utils.SanitizeUnsafe(err)) continue } - utils.VerboseToStdErr("Azure Key Vault get versioned secret response %v", secret) + utils.VerboseToStdErr("Azure Key Vault get versioned secret response %v", utils.SanitizeUnsafe(secret)) data[name] = *secret.Value } } @@ -110,7 +110,7 @@ func (a *AzureKeyVault) GetIndividualSecret(kvpath, secret, version string, anno return nil, err } - utils.VerboseToStdErr("Azure Key Vault get individual secret response %v", data) + utils.VerboseToStdErr("Azure Key Vault get individual secret response %v", utils.SanitizeUnsafe(data)) return *data.Value, nil } diff --git a/pkg/backends/delineasecretsmanager.go b/pkg/backends/delineasecretsmanager.go index fbf398dc..2592b227 100644 --- a/pkg/backends/delineasecretsmanager.go +++ b/pkg/backends/delineasecretsmanager.go @@ -60,7 +60,7 @@ func (a *DelineaSecretServer) GetSecrets(path string, version string, annotation return nil, fmt.Errorf("could not decode secret json %s", secret_json) } - utils.VerboseToStdErr("Delinea Secret Server decoding json %s", secret) + utils.VerboseToStdErr("Delinea Secret Server decoding json %s", utils.SanitizeUnsafe(secret)) secret_map := make(map[string]interface{}) @@ -69,7 +69,7 @@ func (a *DelineaSecretServer) GetSecrets(path string, version string, annotation secret_map[secret.Fields[index].Slug] = secret.Fields[index].ItemValue } - utils.VerboseToStdErr("Delinea Secret Server constructed map %s", secret_map) + utils.VerboseToStdErr("Delinea Secret Server constructed map %s", utils.SanitizeUnsafe(secret_map)) return secret_map, nil } diff --git a/pkg/backends/gcpsecretmanager.go b/pkg/backends/gcpsecretmanager.go index e18e97a7..f7d0f3c0 100644 --- a/pkg/backends/gcpsecretmanager.go +++ b/pkg/backends/gcpsecretmanager.go @@ -57,7 +57,7 @@ func (a *GCPSecretManager) GetSecrets(path string, version string, annotations m return nil, fmt.Errorf("Could not find secret: %v", err) } - utils.VerboseToStdErr("GCP Secret Manager access secret version response %v", result) + utils.VerboseToStdErr("GCP Secret Manager access secret version response %v", utils.SanitizeUnsafe(result)) data := make(map[string]interface{}) diff --git a/pkg/backends/ibmsecretsmanager.go b/pkg/backends/ibmsecretsmanager.go index c6d34f8c..4c0abc5f 100644 --- a/pkg/backends/ibmsecretsmanager.go +++ b/pkg/backends/ibmsecretsmanager.go @@ -384,7 +384,7 @@ func (i *IBMSecretsManager) getSecretVersionedOrNot(id, stype, version string) ( return nil, fmt.Errorf("Could not retrieve secret %s after %d retries, statuscode %d", id, types.IBMMaxRetries, httpResponse.GetStatusCode()) } - utils.VerboseToStdErr("IBM Cloud Secrets Manager get versioned secret %s HTTP response: %v", id, httpResponse) + utils.VerboseToStdErr("IBM Cloud Secrets Manager get versioned secret %s HTTP response: %v", id, utils.SanitizeUnsafe(httpResponse)) result, err = NewIBMVersionedSecretData(secretVersion).GetSecret() if err != nil { @@ -402,7 +402,7 @@ func (i *IBMSecretsManager) getSecretVersionedOrNot(id, stype, version string) ( return nil, fmt.Errorf("Could not retrieve secret %s after %d retries, statuscode %d", id, types.IBMMaxRetries, httpResponse.GetStatusCode()) } - utils.VerboseToStdErr("IBM Cloud Secrets Manager get unversioned secret %s HTTP response: %v", id, httpResponse) + utils.VerboseToStdErr("IBM Cloud Secrets Manager get unversioned secret %s HTTP response: %v", id, utils.SanitizeUnsafe(httpResponse)) result, err = NewIBMSecretData(secretRes).GetSecret() if err != nil { @@ -500,7 +500,7 @@ func (i *IBMSecretsManager) listSecretsInGroup(groupId, secretType string) (map[ return nil, fmt.Errorf("Could not list secrets for secret group %s: %d\n%s", groupId, details.GetStatusCode(), details.String()) } - utils.VerboseToStdErr("IBM Cloud Secrets Manager list secrets in group HTTP response: %v", details) + utils.VerboseToStdErr("IBM Cloud Secrets Manager list secrets in group HTTP response: %v", utils.SanitizeUnsafe(details)) for _, secret := range res.Secrets { var name, ttype string @@ -508,7 +508,7 @@ func (i *IBMSecretsManager) listSecretsInGroup(groupId, secretType string) (map[ data, err := v.GetMetadata() if err != nil { - utils.VerboseToStdErr("Skipping a secret in group %s: %s", groupId, err) + utils.VerboseToStdErr("Skipping a secret in group %s: %s", groupId, utils.SanitizeUnsafe(err)) } name = data["name"] diff --git a/pkg/backends/keepersecretsmanager.go b/pkg/backends/keepersecretsmanager.go index d2e3d3e3..7a983111 100644 --- a/pkg/backends/keepersecretsmanager.go +++ b/pkg/backends/keepersecretsmanager.go @@ -140,7 +140,7 @@ func (a *KeeperSecretsManager) GetSecrets(path string, version string, annotatio } } - utils.VerboseToStdErr("Keeper Secrets Manager constructed map %s", secretMap) + utils.VerboseToStdErr("Keeper Secrets Manager constructed map %s", utils.SanitizeUnsafe(secretMap)) return secretMap, nil diff --git a/pkg/backends/kubernetessecret.go b/pkg/backends/kubernetessecret.go index fadb1fe7..12e87a16 100644 --- a/pkg/backends/kubernetessecret.go +++ b/pkg/backends/kubernetessecret.go @@ -43,7 +43,7 @@ func (k *KubernetesSecret) GetSecrets(path string, version string, annotations m out[k] = string(v) } - utils.VerboseToStdErr("K8s Secret get secret response: %v", out) + utils.VerboseToStdErr("K8s Secret get secret response: %v", utils.SanitizeUnsafe(out)) return out, nil } @@ -51,7 +51,7 @@ func (k *KubernetesSecret) GetSecrets(path string, version string, annotations m // Kubernetes Secrets can only be wholly read, // So, we use GetSecrets and extract the specific placeholder we want func (k *KubernetesSecret) GetIndividualSecret(path, secret, version string, annotations map[string]string) (interface{}, error) { - utils.VerboseToStdErr("K8s Secret getting secret %s and key %s", path, secret) + utils.VerboseToStdErr("K8s Secret getting secret %s and key %s", path, utils.SanitizeUnsafe(secret)) data, err := k.GetSecrets(path, version, annotations) if err != nil { return nil, err diff --git a/pkg/backends/localsecretmanager.go b/pkg/backends/localsecretmanager.go index 5fa15e42..db37dcf7 100644 --- a/pkg/backends/localsecretmanager.go +++ b/pkg/backends/localsecretmanager.go @@ -32,7 +32,7 @@ func (a *LocalSecretManager) GetSecrets(path string, version string, annotations utils.VerboseToStdErr("Local secret manager getting secret %s at version %s", path, version) cleartext, err := a.Decrypt(path, "yaml") - utils.VerboseToStdErr("Local secret manager get secret response: %v", cleartext) + utils.VerboseToStdErr("Local secret manager get secret response: %v", utils.SanitizeUnsafe(cleartext)) var dat map[string]interface{} diff --git a/pkg/backends/onepasswordconnect.go b/pkg/backends/onepasswordconnect.go index 8c15a1f8..227efcf1 100644 --- a/pkg/backends/onepasswordconnect.go +++ b/pkg/backends/onepasswordconnect.go @@ -37,7 +37,7 @@ func (a *OnePasswordConnect) GetSecrets(path string, version string, annotations return nil, err } - utils.VerboseToStdErr("OnePassword Connect get secret response: %v", result) + utils.VerboseToStdErr("OnePassword Connect get secret response: %v", utils.SanitizeUnsafe(result)) data := make(map[string]interface{}) diff --git a/pkg/backends/vault.go b/pkg/backends/vault.go index 8d90151e..a685e4d2 100644 --- a/pkg/backends/vault.go +++ b/pkg/backends/vault.go @@ -61,7 +61,7 @@ func (v *Vault) GetSecrets(path string, version string, annotations map[string]s return nil, err } - utils.VerboseToStdErr("Hashicorp Vault get kv pairs response: %v", secret) + utils.VerboseToStdErr("Hashicorp Vault get kv pairs response: %v", utils.SanitizeUnsafe(secret)) if secret == nil { // Do not mention `version` in error message when it's not honored (KV-V1) diff --git a/pkg/backends/yandexcloudlockbox.go b/pkg/backends/yandexcloudlockbox.go index 7e07091d..f0c74cb4 100644 --- a/pkg/backends/yandexcloudlockbox.go +++ b/pkg/backends/yandexcloudlockbox.go @@ -41,7 +41,7 @@ func (ycl *YandexCloudLockbox) GetSecrets(secretID string, version string, _ map return nil, err } - utils.VerboseToStdErr("Yandex Cloud Lockbox get secret response %v", resp) + utils.VerboseToStdErr("Yandex Cloud Lockbox get secret response %v", utils.SanitizeUnsafe(resp)) result := make(map[string]interface{}, len(resp.GetEntries())) for _, v := range resp.GetEntries() { diff --git a/pkg/kube/util.go b/pkg/kube/util.go index aeaccc20..c6bf707d 100644 --- a/pkg/kube/util.go +++ b/pkg/kube/util.go @@ -210,7 +210,7 @@ func configReplacement(key, value string, resource Resource) (interface{}, []err // configMap data values must be strings - utils.VerboseToStdErr("key %s comes from ConfigMap manifest, stringifying value %s to fit", key, value) + utils.VerboseToStdErr("key %s comes from ConfigMap manifest, stringifying value %s to fit", key, utils.SanitizeUnsafe(value)) return stringify(res), err } @@ -219,7 +219,7 @@ func secretReplacement(key, value string, resource Resource) (interface{}, []err if err == nil && genericPlaceholder.Match(decoded) { res, err := genericReplacement(key, string(decoded), resource) - utils.VerboseToStdErr("key %s comes from Secret manifest, base64 encoding value %s to fit", key, value) + utils.VerboseToStdErr("key %s comes from Secret manifest, base64 encoding value %s to fit", key, utils.SanitizeUnsafe(value)) return base64.StdEncoding.EncodeToString([]byte(stringify(res))), err } From e73978f1c68f32bf31a7114924d73890411cc167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Fri, 29 Nov 2024 17:36:38 +0100 Subject: [PATCH 3/3] feat(cmd): Test --verbose and --verbose-sensitive-output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- cmd/generate.go | 15 ++++++---- cmd/generate_test.go | 58 +++++++++++++++++++++++++++++++++++++ pkg/config/config_test.go | 18 ++---------- pkg/helpers/test_helpers.go | 16 ++++++++++ pkg/utils/util.go | 8 +++-- 5 files changed, 90 insertions(+), 25 deletions(-) diff --git a/cmd/generate.go b/cmd/generate.go index 6a842091..8ced04f7 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -20,8 +20,8 @@ import ( func NewGenerateCommand() *cobra.Command { const StdIn = "-" var configPath, secretName string - var verboseOutput bool - var verboseUnsafe bool + var verboseSafe bool + var verboseSensitive bool var disableCache bool var command = &cobra.Command{ @@ -64,9 +64,12 @@ func NewGenerateCommand() *cobra.Command { } v := viper.New() - viper.Set("verboseOutput", verboseOutput) - viper.Set("verboseUnsafe", verboseUnsafe) + viper.Set("verbose", verboseSafe || verboseSensitive) + viper.Set("verboseRedact", verboseSafe && !verboseSensitive) viper.Set("disableCache", disableCache) + if verboseSensitive { + utils.VerboseToStdErr("Running with --verbose-sensitive-output. Sensitive information will be printed to standard error!") + } cmdConfig, err := config.New(v, &config.Options{ SecretName: secretName, ConfigPath: configPath, @@ -119,8 +122,8 @@ func NewGenerateCommand() *cobra.Command { command.Flags().StringVarP(&configPath, "config-path", "c", "", "path to a file containing Vault configuration (YAML, JSON, envfile) to use") command.Flags().StringVarP(&secretName, "secret-name", "s", "", "name of a Kubernetes Secret in the argocd namespace containing Vault configuration data in the argocd namespace of your ArgoCD host (Only available when used in ArgoCD). The namespace can be overridden by using the format :") - command.Flags().BoolVar(&verboseOutput, "verboseOutput", false, "enable verboseOutput mode for detailed info to help with debugging. Omits sensitive data (credentials), logged to stderr") - command.Flags().BoolVar(&verboseUnsafe, "verboseOutput-sensitive-output", false, "enable verboseOutput mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") + command.Flags().BoolVar(&verboseSafe, "verbose", false, "enable verbose mode for detailed info to help with debugging. Omits sensitive data (credentials), logged to stderr") + command.Flags().BoolVar(&verboseSensitive, "verbose-sensitive-output", false, "enable verbose mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") command.Flags().BoolVar(&disableCache, "disable-token-cache", false, "disable the automatic token cache feature that store tokens locally") return command } diff --git a/cmd/generate_test.go b/cmd/generate_test.go index f428c690..4f914c4e 100644 --- a/cmd/generate_test.go +++ b/cmd/generate_test.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "fmt" + "github.com/stretchr/testify/assert" "io" "os" "strings" @@ -308,3 +309,60 @@ func TestMain(t *testing.T) { os.Unsetenv("VAULT_SKIP_VERIFY") os.Unsetenv("AVP_PATH_VALIDATION") } + +func TestVerboseness(t *testing.T) { + cluster, roleid, secretid = helpers.CreateTestAppRoleVault(t) + os.Setenv("AVP_TYPE", "vault") + os.Setenv("VAULT_ADDR", cluster.Cores[0].Client.Address()) + os.Setenv("AVP_AUTH_TYPE", "approle") + os.Setenv("AVP_SECRET_ID", "broken_but_secret") + os.Setenv("AVP_ROLE_ID", "broken_but_secret") + os.Setenv("VAULT_SKIP_VERIFY", "true") + + t.Run("Quiet", func(t *testing.T) { + cmd := NewGenerateCommand() + cmd.SetArgs([]string{"../fixtures/input/nonempty/secret_path.yaml"}) + cmd.SetOut(bytes.NewBufferString("")) + cmd.SetErr(bytes.NewBufferString("")) + logOut := helpers.CaptureOutput(func() { + cmd.Execute() + }) + + assert.Equal(t, "", logOut) + }) + + t.Run("Safe verbose", func(t *testing.T) { + cmd := NewGenerateCommand() + cmd.SetArgs([]string{"../fixtures/input/nonempty/secret_path.yaml", "--verbose"}) + cmd.SetOut(bytes.NewBufferString("")) + cmd.SetErr(bytes.NewBufferString("")) + logOut := helpers.CaptureOutput(func() { + cmd.Execute() + }) + + assert.Contains(t, logOut, "Hashicorp Vault authenticating with role ID ***REDACTED(17 characters)*** and secret ID ***REDACTED(17 characters)*** at path auth/approle") + assert.NotContains(t, logOut, "broken_but_secret") + }) + + t.Run("Sensitive verbose", func(t *testing.T) { + cmd := NewGenerateCommand() + cmd.SetArgs([]string{"../fixtures/input/nonempty/secret_path.yaml", "--verbose-sensitive-output"}) + cmd.SetOut(bytes.NewBufferString("")) + cmd.SetErr(bytes.NewBufferString("")) + logOut := helpers.CaptureOutput(func() { + cmd.Execute() + }) + + assert.Contains(t, logOut, "Running with --verbose-sensitive-output. Sensitive information will be printed to standard error!") + assert.Contains(t, logOut, "Hashicorp Vault authenticating with role ID broken_but_secret and secret ID broken_but_secret at path auth/approle") + assert.NotContains(t, logOut, "***REDACTED") + }) + + os.Unsetenv("AVP_TYPE") + os.Unsetenv("VAULT_ADDR") + os.Unsetenv("AVP_AUTH_TYPE") + os.Unsetenv("AVP_SECRET_ID") + os.Unsetenv("AVP_ROLE_ID") + os.Unsetenv("VAULT_SKIP_VERIFY") + os.Unsetenv("AVP_PATH_VALIDATION") +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c7925b9d..f956512f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,14 +1,13 @@ package config_test import ( - "bytes" "fmt" - "log" "os" "strings" "testing" "github.com/argoproj-labs/argocd-vault-plugin/pkg/config" + "github.com/argoproj-labs/argocd-vault-plugin/pkg/helpers" "github.com/spf13/viper" ) @@ -267,19 +266,6 @@ func TestNewConfigNoAuthType(t *testing.T) { os.Unsetenv("AVP_TYPE") } -// Helper function that captures log output from a function call into a string -// Adapted from https://stackoverflow.com/a/26806093/170154 -func captureOutput(f func()) string { - var buf bytes.Buffer - flags := log.Flags() - log.SetOutput(&buf) - log.SetFlags(0) // don't include any date or time in the logging messages - f() - log.SetOutput(os.Stderr) - log.SetFlags(flags) - return buf.String() -} - func TestNewConfigAwsRegionWarning(t *testing.T) { testCases := []struct { environment map[string]interface{} @@ -314,7 +300,7 @@ func TestNewConfigAwsRegionWarning(t *testing.T) { viper.Set("verboseOutput", true) v := viper.New() - output := captureOutput(func() { + output := helpers.CaptureOutput(func() { config, err := config.New(v, &config.Options{}) if err != nil { t.Error(err) diff --git a/pkg/helpers/test_helpers.go b/pkg/helpers/test_helpers.go index e12f8636..337f53d3 100644 --- a/pkg/helpers/test_helpers.go +++ b/pkg/helpers/test_helpers.go @@ -1,8 +1,11 @@ package helpers import ( + "bytes" "fmt" + "log" "net" + "os" "strconv" "testing" @@ -543,3 +546,16 @@ func (v *MockVault) GetIndividualSecret(path, secret, version string, annotation num, _ := strconv.ParseInt(version, 10, 0) return v.Data[num-1][secret], nil } + +// Helper function that captures log output from a function call into a string +// Adapted from https://stackoverflow.com/a/26806093/170154 +func CaptureOutput(f func()) string { + var buf bytes.Buffer + flags := log.Flags() + log.SetOutput(&buf) + log.SetFlags(0) // don't include any date or time in the logging messages + f() + log.SetOutput(os.Stderr) + log.SetFlags(flags) + return buf.String() +} diff --git a/pkg/utils/util.go b/pkg/utils/util.go index a01d935c..7ef4dc93 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -142,15 +142,17 @@ func DefaultHttpClient() *http.Client { return httpClient } +// VerboseToStdErr formatand prints message to stderr, if either `--verbose` or `--verbose-sensitive-output` were passed. +// It is a responsibility of the user to call SanitizeUnsafe on all arguments that can contain sensitive data. func VerboseToStdErr(format string, message ...interface{}) { - if viper.GetBool("verboseOutput") { + if viper.GetBool("verbose") { log.Printf(fmt.Sprintf("%s\n", format), message...) } } -// SanitizeUnsafe replaces the message data with redacted literal unless `--verbose-sensitive-output` was passed +// SanitizeUnsafe replaces the message data with redacted literal unless `--verbose-sensitive-output` was passed. func SanitizeUnsafe(message interface{}) interface{} { - if viper.GetBool("verboseOutput") && !viper.GetBool("verboseUnsafe") { + if viper.GetBool("verboseRedact") { messageLen := len(fmt.Sprintf("%s", message)) return fmt.Sprintf("***REDACTED(%v characters)***", messageLen) } else {