From d0a99bc9157a2a2f8534026fe3caec581cba727e Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Wed, 28 Jun 2023 02:13:12 +0200 Subject: [PATCH] feat: Add support for HCP Vault Secrets --- .../configuration-file/variables.md.yaml | 16 +++ .../hcpVaultSecret.md | 16 +++ .../hcpVaultSecretJson.md | 16 +++ .../hcp-vault-secrets-functions/index.md | 10 ++ .../user-guide/password-managers/custom.md | 21 ++-- .../password-managers/hcp-vault-secrets.md | 10 ++ .../chezmoi.io/docs/what-does-chezmoi-do.md | 7 +- assets/chezmoi.io/mkdocs.yml | 5 + pkg/cmd/config.go | 6 + pkg/cmd/hcpvaultsecretsttemplatefuncs.go | 106 ++++++++++++++++++ .../testdata/scripts/hcpvaultsecrets.txtar | 70 ++++++++++++ 11 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecret.md create mode 100644 assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecretJson.md create mode 100644 assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/index.md create mode 100644 assets/chezmoi.io/docs/user-guide/password-managers/hcp-vault-secrets.md create mode 100644 pkg/cmd/hcpvaultsecretsttemplatefuncs.go create mode 100644 pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar diff --git a/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml b/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml index 3d6275fa70cd..37a9d750c8a3 100644 --- a/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml +++ b/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml @@ -215,6 +215,22 @@ sections: symmetric: type: bool description: Use symmetric GPG encryption + hcpVaultSecrets: + appName: + type: string + description: Default app name if non is specified + args: + type: '[]string' + description: Extra args to HCP Vault Secrets CLI command + command: + default: '`vlt`' + description: HCP Vault Secrets CLI command + organization: + type: string + description: Default organization if non is specified + project: + type: string + description: Default project if non is specified hooks: '*command*`.post.args`': type: '[]string' diff --git a/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecret.md b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecret.md new file mode 100644 index 000000000000..6308a4c40ff4 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecret.md @@ -0,0 +1,16 @@ +# `hcpVaultSecret` *key* [*app-name* [*project* [*organization*]]] + +`hcpVaultSecret` returns the plaintext secret from [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using `vlt +--plaintext`. + +If any of *app-name*, *project*, or *organization* are empty or omitted, then +chezmoi will use the value from the `hcpVaultSecret.appName`, +`hcpVaultSecret.project`, and `hcpVaultSecret.organization` config variables +if they are set and not empty. + +!!! example + + ``` + {{ hcpVaultSecret "username" }} + ``` diff --git a/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecretJson.md b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecretJson.md new file mode 100644 index 000000000000..55bb2d667536 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/hcpVaultSecretJson.md @@ -0,0 +1,16 @@ +# `hcpVaultSecretJson` *key* [*app-name* [*project* [*organization*]]] + +`hcpVaultSecretJson` returns structured data from [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using `vlt +--format=json`. + +If any of *app-name*, *project*, or *organization* are empty or omitted, then +chezmoi will use the value from the `hcpVaultSecret.appName`, +`hcpVaultSecret.project`, and `hcpVaultSecret.organization` config variables +if they are set and not empty. + +!!! example + + ``` + {{ (hcpVaultSecretJson "username") }} + ``` diff --git a/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/index.md b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/index.md new file mode 100644 index 000000000000..d4f5bf0afc98 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/hcp-vault-secrets-functions/index.md @@ -0,0 +1,10 @@ +# HCP Vault Secrets + +chezmoi includes support for [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using the `vlt` +CLI to expose data through the `hcpVaultSecret` and `hcpVaultSecretJson` +template functions. + +!!! warning + + HCP Vault Secrets is in beta and chezmoi's support for it may change at any time. diff --git a/assets/chezmoi.io/docs/user-guide/password-managers/custom.md b/assets/chezmoi.io/docs/user-guide/password-managers/custom.md index bd0834165b33..2b3b3bbd4eeb 100644 --- a/assets/chezmoi.io/docs/user-guide/password-managers/custom.md +++ b/assets/chezmoi.io/docs/user-guide/password-managers/custom.md @@ -7,13 +7,14 @@ configuration file. You can then invoke this command with the `secret` and output respectively. All of the above secret managers can be supported in this way: -| Secret Manager | `secret.command` | Template skeleton | -| --------------- | ---------------- | -------------------------------------------------- | -| 1Password | `op` | `{{ secretJSON "get" "item" "$ID" }}` | -| Bitwarden | `bw` | `{{ secretJSON "get" "$ID" }}` | -| HashiCorp Vault | `vault` | `{{ secretJSON "kv" "get" "-format=json" "$ID" }}` | -| LastPass | `lpass` | `{{ secretJSON "show" "--json" "$ID" }}` | -| KeePassXC | `keepassxc-cli` | Not possible (interactive command only) | -| Keeper | `keeper` | `{{ secretJSON "get" "--format=json" "$ID" }}` | -| pass | `pass` | `{{ secret "show" "$ID" }}` | -| passhole | `ph` | `{{ secret "$ID" "password" }}` | +| Secret Manager | `secret.command` | Template skeleton | +| ----------------- | ---------------- | -------------------------------------------------- | +| 1Password | `op` | `{{ secretJSON "get" "item" "$ID" }}` | +| Bitwarden | `bw` | `{{ secretJSON "get" "$ID" }}` | +| HashiCorp Vault | `vault` | `{{ secretJSON "kv" "get" "-format=json" "$ID" }}` | +| HCP Vault Secrets | `vlt` | `{{ secret "secrets" "get" "--plaintext" "$ID }}` | +| LastPass | `lpass` | `{{ secretJSON "show" "--json" "$ID" }}` | +| KeePassXC | `keepassxc-cli` | Not possible (interactive command only) | +| Keeper | `keeper` | `{{ secretJSON "get" "--format=json" "$ID" }}` | +| pass | `pass` | `{{ secret "show" "$ID" }}` | +| passhole | `ph` | `{{ secret "$ID" "password" }}` | diff --git a/assets/chezmoi.io/docs/user-guide/password-managers/hcp-vault-secrets.md b/assets/chezmoi.io/docs/user-guide/password-managers/hcp-vault-secrets.md new file mode 100644 index 000000000000..d4f5bf0afc98 --- /dev/null +++ b/assets/chezmoi.io/docs/user-guide/password-managers/hcp-vault-secrets.md @@ -0,0 +1,10 @@ +# HCP Vault Secrets + +chezmoi includes support for [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using the `vlt` +CLI to expose data through the `hcpVaultSecret` and `hcpVaultSecretJson` +template functions. + +!!! warning + + HCP Vault Secrets is in beta and chezmoi's support for it may change at any time. diff --git a/assets/chezmoi.io/docs/what-does-chezmoi-do.md b/assets/chezmoi.io/docs/what-does-chezmoi-do.md index 10f5b9f93d72..6ec37aa9b718 100644 --- a/assets/chezmoi.io/docs/what-does-chezmoi-do.md +++ b/assets/chezmoi.io/docs/what-does-chezmoi-do.md @@ -45,9 +45,10 @@ format of your choice. chezmoi can retrieve secrets from [1Password](https://1password.com/), [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), [Bitwarden](https://bitwarden.com/), [Dashlane](https://www.dashlane.com/), -[gopass](https://www.gopass.pw/), [KeePassXC](https://keepassxc.org/), -[Keeper](https://www.keepersecurity.com/), [LastPass](https://lastpass.com/), -[pass](https://www.passwordstore.org/), +[gopass](https://www.gopass.pw/), [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets), +[KeePassXC](https://keepassxc.org/), [Keeper](https://www.keepersecurity.com/), +[LastPass](https://lastpass.com/), [pass](https://www.passwordstore.org/), [passhole](https://github.com/Evidlo/passhole), [Vault](https://www.vaultproject.io/), Keychain, [Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), or any command-line diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index ed075f4fb68e..89739d276a3a 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -65,6 +65,7 @@ nav: - Dashlane: user-guide/password-managers/dashlane.md - ejson: user-guide/password-managers/ejson.md - gopass: user-guide/password-managers/gopass.md + - Hashicorp Vault Secrets: user-guide/password-managers/hcp-vault-secrets.md - KeePassXC: user-guide/password-managers/keepassxc.md - Keychain and Windows Credentials Manager: user-guide/password-managers/keychain-and-windows-credentials-manager.md - Keeper: user-guide/password-managers/keeper.md @@ -254,6 +255,10 @@ nav: - reference/templates/gopass-functions/index.md - gopass: reference/templates/gopass-functions/gopass.md - gopassRaw: reference/templates/gopass-functions/gopassRaw.md + - HCP Vault Secrets functions: + - reference/templates/hcp-vault-secrets-functions/index.md + - hcpVaultSecret: reference/templates/hcp-vault-secrets-functions/hcpVaultSecret.md + - hcpVaultSecretJson: reference/templates/hcp-vault-secrets-functions/hcpVaultSecretJson.md - KeePassXC functions: - reference/templates/keepassxc-functions/index.md - keepassxc: reference/templates/keepassxc-functions/keepassxc.md diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index 8413b99d905c..3a6acce7d88b 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -124,6 +124,7 @@ type ConfigFile struct { Dashlane dashlaneConfig `json:"dashlane" mapstructure:"dashlane" yaml:"dashlane"` Ejson ejsonConfig `json:"ejson" mapstructure:"ejson" yaml:"ejson"` Gopass gopassConfig `json:"gopass" mapstructure:"gopass" yaml:"gopass"` + HCPVaultSecrets hcpVaultSecretConfig `json:"hcpVaultSecrets" mapstructure:"hcpVaultSecrets" yaml:"hcpVaultSecrets"` Keepassxc keepassxcConfig `json:"keepassxc" mapstructure:"keepassxc" yaml:"keepassxc"` Keeper keeperConfig `json:"keeper" mapstructure:"keeper" yaml:"keeper"` Lastpass lastpassConfig `json:"lastpass" mapstructure:"lastpass" yaml:"lastpass"` @@ -402,6 +403,8 @@ func newConfig(options ...configOption) (*Config, error) { "glob": c.globTemplateFunc, "gopass": c.gopassTemplateFunc, "gopassRaw": c.gopassRawTemplateFunc, + "hcpVaultSecret": c.hcpVaultSecretTemplateFunc, + "hcpVaultSecretJson": c.hcpVaultSecretJSONTemplateFunc, "hexDecode": c.hexDecodeTemplateFunc, "hexEncode": c.hexEncodeTemplateFunc, "include": c.includeTemplateFunc, @@ -2582,6 +2585,9 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile { Gopass: gopassConfig{ Command: "gopass", }, + HCPVaultSecrets: hcpVaultSecretConfig{ + Command: "vlt", + }, Keepassxc: keepassxcConfig{ Command: "keepassxc-cli", Prompt: true, diff --git a/pkg/cmd/hcpvaultsecretsttemplatefuncs.go b/pkg/cmd/hcpvaultsecretsttemplatefuncs.go new file mode 100644 index 000000000000..2f5de6c55652 --- /dev/null +++ b/pkg/cmd/hcpvaultsecretsttemplatefuncs.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + + "golang.org/x/exp/slices" + + "github.com/twpayne/chezmoi/v2/pkg/chezmoilog" +) + +type hcpVaultSecretConfig struct { + Command string `json:"command" mapstructure:"command" yaml:"command"` + Args []string `json:"args" mapstructure:"args" yaml:"args"` + AppName string `json:"appName" mapstructure:"appName" yaml:"appName"` + Organization string `json:"organization" mapstructure:"organization" yaml:"organization"` + Project string `json:"project" mapstructure:"project" yaml:"project"` + outputCache map[string][]byte +} + +func (c *Config) hcpVaultSecretTemplateFunc(key string, additionalArgs ...string) string { + args, err := c.appendHCPVaultSecretsAdditionalArgs( + []string{"secrets", "get", "--plaintext"}, + additionalArgs, + ) + if err != nil { + panic(err) + } + output, err := c.vltOutput(append(args, key)) + if err != nil { + panic(err) + } + return string(output) +} + +func (c *Config) hcpVaultSecretJSONTemplateFunc(key string, additionalArgs ...string) any { + args, err := c.appendHCPVaultSecretsAdditionalArgs( + []string{"secrets", "get", "--format", "json"}, + additionalArgs, + ) + if err != nil { + panic(err) + } + data, err := c.vltOutput(append(args, key)) + if err != nil { + panic(err) + } + var value any + if err := json.Unmarshal(data, &value); err != nil { + panic(err) + } + return value +} + +func (c *Config) appendHCPVaultSecretsAdditionalArgs( + args, additionalArgs []string, +) ([]string, error) { + if len(additionalArgs) > 0 && additionalArgs[0] != "" { + args = append(args, "--app-name", additionalArgs[0]) + } else if c.HCPVaultSecrets.AppName != "" { + args = append(args, "--app-name", c.HCPVaultSecrets.AppName) + } + if len(additionalArgs) > 1 && additionalArgs[1] != "" { + args = append(args, "--project", additionalArgs[1]) + } else if c.HCPVaultSecrets.Project != "" { + args = append(args, "--project", c.HCPVaultSecrets.Project) + } + if len(additionalArgs) > 2 && additionalArgs[2] != "" { + args = append(args, "--organization", additionalArgs[2]) + } else if c.HCPVaultSecrets.Organization != "" { + args = append(args, "--organization", c.HCPVaultSecrets.Organization) + } + if len(additionalArgs) > 3 { + // Add one to the number of received arguments as the hcpVaultSecret + // and hcpVaultSecretJson template functions report this error and take + // the key as the first argument. + return nil, fmt.Errorf("expected 1 to 4 arguments, got %d", len(additionalArgs)+1) + } + return args, nil +} + +func (c *Config) vltOutput(args []string) ([]byte, error) { + args = append(slices.Clone(c.HCPVaultSecrets.Args), args...) + key := strings.Join(args, "\x00") + if data, ok := c.HCPVaultSecrets.outputCache[key]; ok { + return data, nil + } + + cmd := exec.Command(c.HCPVaultSecrets.Command, args...) //nolint:gosec + cmd.Dir = c.DestDirAbsPath.String() + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + output, err := chezmoilog.LogCmdOutput(cmd) + if err != nil { + return nil, newCmdOutputError(cmd, output, err) + } + + if c.HCPVaultSecrets.outputCache == nil { + c.HCPVaultSecrets.outputCache = make(map[string][]byte) + } + c.HCPVaultSecrets.outputCache[key] = output + return output, nil +} diff --git a/pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar b/pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar new file mode 100644 index 000000000000..10cce2f552bd --- /dev/null +++ b/pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar @@ -0,0 +1,70 @@ +[windows] skip 'UNIX only' +[!windows] chmod 755 bin/vlt + +# test hcpVaultSecret template function +exec chezmoi execute-template '{{ hcpVaultSecret "username" }}' +stdout ^db-user$ + +# test hcpVaultSecret template function with app name, project, and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecret "password" "app-name" "project" "organization" }}' +stdout ^password$ + +# test hcpVaultSecret template function with empty app name, project, and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecret "username" "" "" "" }}' +stdout ^db-user$ + +# test hcpVaultSecretJson template function +exec chezmoi execute-template '{{ (hcpVaultSecretJson "username").created_by.email }}' +stdout ^username@example\.com$ + +chhome home2/user + +# test hcpVaultSecret template function with default app name, project, and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecret "password" }}' +stdout ^default-password$ + +# test hcpVaultSecretJson template function with default project and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecret "password" "other-app-name" }}' +stdout ^other-password$ + +-- bin/vlt -- +#!/bin/sh + +case "$*" in +"secrets get --format json username") + cat <