Skip to content

Commit

Permalink
feat: Add support for HCP Vault Secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jun 28, 2023
1 parent d36ceaf commit d0a99bc
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }}
```
Original file line number Diff line number Diff line change
@@ -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") }}
```
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 11 additions & 10 deletions assets/chezmoi.io/docs/user-guide/password-managers/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }}` |
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 4 additions & 3 deletions assets/chezmoi.io/docs/what-does-chezmoi-do.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions assets/chezmoi.io/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
106 changes: 106 additions & 0 deletions pkg/cmd/hcpvaultsecretsttemplatefuncs.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions pkg/cmd/testdata/scripts/hcpvaultsecrets.txtar
Original file line number Diff line number Diff line change
@@ -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 <<EOF
{
"created_at": "2023-06-09T13:14:28.140Z",
"created_by": {
"email": "[email protected]",
"name": "example",
"type": "TYPE_USER"
},
"latest_version": "2",
"name": "username"
}
EOF
;;
"secrets get --plaintext username")
echo db-user
;;
"secrets get --plaintext --app-name app-name --project project --organization organization password")
echo password
;;
"secrets get --plaintext --app-name default-app-name --project default-project --organization default-organization password")
echo default-password
;;
"secrets get --plaintext --app-name other-app-name --project default-project --organization default-organization password")
echo other-password
;;
*)
echo "$*: unknown command"
exit 1
;;
esac
-- home/user/.keep --
-- home2/user/.config/chezmoi/chezmoi.toml --
[hcpVaultSecrets]
appName = "default-app-name"
project = "default-project"
organization = "default-organization"

0 comments on commit d0a99bc

Please sign in to comment.