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..bbbd902416ee 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,10 @@ sections: symmetric: type: bool description: Use symmetric GPG encryption + hcpVaultSecrets: + command: + default: '`vlt`' + description: HCP Vault Secrets CLI command hooks: '*command*`.post.args`': type: '[]string' diff --git a/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecrets.md b/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecrets.md new file mode 100644 index 000000000000..8b6e40ec1f61 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecrets.md @@ -0,0 +1,11 @@ +# `hcpVaultSecrets` *key* [*app-name* [*project* [*organization*]]] + +`hcpVaultSecrets` returns the plaintext secret from [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using the `vlt` +CLI. + +!!! example + + ``` + {{ hcpVaultSecrets "username" }} + ``` diff --git a/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecretsJson.md b/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecretsJson.md new file mode 100644 index 000000000000..8fe601cbc32e --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/vlt-functions/hcpVaultSecretsJson.md @@ -0,0 +1,11 @@ +# `hcpVaultSecrets` *key* [*app-name* [*project* [*organization*]]] + +`hcpVaultSecrets` returns the secret from [HCP Vault +Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using the `vlt` +CLI. + +!!! example + + ``` + {{ (hcpVaultSecretsJson "username") }} + ``` diff --git a/assets/chezmoi.io/docs/reference/templates/vlt-functions/index.md b/assets/chezmoi.io/docs/reference/templates/vlt-functions/index.md new file mode 100644 index 000000000000..77cdde826052 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/vlt-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 `hcpVaultSecrets` and `hcpVaultSecretsJson` +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..77cdde826052 --- /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 `hcpVaultSecrets` and `hcpVaultSecretsJson` +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..6368bc410282 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -72,6 +72,7 @@ nav: - pass: user-guide/password-managers/pass.md - passhole: user-guide/password-managers/passhole.md - Vault: user-guide/password-managers/vault.md + - Hashicorp Vault Secrets: user-guide/password-managers/hcp-vault-secrets.md - Custom: user-guide/password-managers/custom.md - Encryption: - user-guide/encryption/index.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 b06c66b5f762..afe97a6d80da 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -123,6 +123,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 hcpVaultSecretsConfig `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"` @@ -401,6 +402,8 @@ func newConfig(options ...configOption) (*Config, error) { "glob": c.globTemplateFunc, "gopass": c.gopassTemplateFunc, "gopassRaw": c.gopassRawTemplateFunc, + "hcpVaultSecrets": c.hcpVaultSecretsTemplateFunc, + "hcpVaultSecretsJson": c.hcpVaultSecretsJSONTemplateFunc, "hexDecode": c.hexDecodeTemplateFunc, "hexEncode": c.hexEncodeTemplateFunc, "include": c.includeTemplateFunc, @@ -2581,6 +2584,9 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile { Gopass: gopassConfig{ Command: "gopass", }, + HCPVaultSecrets: hcpVaultSecretsConfig{ + 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..b78eb304a91f --- /dev/null +++ b/pkg/cmd/hcpvaultsecretsttemplatefuncs.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/twpayne/chezmoi/v2/pkg/chezmoilog" +) + +type hcpVaultSecretsConfig struct { + Command string `json:"command" mapstructure:"command" yaml:"command"` + outputCache map[string][]byte +} + +func (c *Config) hcpVaultSecretsTemplateFunc(key string, additionalArgs ...string) string { + args, err := 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) hcpVaultSecretsJSONTemplateFunc(key string, additionalArgs ...string) any { + args, err := 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 appendHCPVaultSecretsAdditionalArgs(args, additionalArgs []string) ([]string, error) { + if len(additionalArgs) > 0 && additionalArgs[0] != "" { + args = append(args, "--app-name", additionalArgs[0]) + } + if len(additionalArgs) > 1 && additionalArgs[1] != "" { + args = append(args, "--project", additionalArgs[1]) + } + if len(additionalArgs) > 2 && additionalArgs[2] != "" { + args = append(args, "--organization", additionalArgs[2]) + } + if len(additionalArgs) > 3 { + // Add one to the number of received arguments as the hcpVaultSecrets + // and hcpVaultSecretsJson 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) { + 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.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..4017aa28456e --- /dev/null +++ b/pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar @@ -0,0 +1,48 @@ +[windows] skip 'UNIX only' +[!windows] chmod 755 bin/vlt + +# test hcpVaultSecrets template function +exec chezmoi execute-template '{{ hcpVaultSecrets "username" }}' +stdout ^db-user$ + +# test hcpVaultSecrets template function with app name, project, and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecrets "password" "app-name" "project" "organization" }}' +stdout ^password$ + +# test hcpVaultSecrets template function with empty app name, project, and organization arguments +exec chezmoi execute-template '{{ hcpVaultSecrets "username" "" "" "" }}' +stdout ^db-user$ + +# test hcpVaultSecretsJson template function +exec chezmoi execute-template '{{ (hcpVaultSecretsJson "username").created_by.email }}' +stdout ^username@example\.com$ + +-- bin/vlt -- +#!/bin/sh + +case "$*" in +"secrets get --format json username") + cat <