Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for HCP Vault Secrets #3067

Merged
merged 1 commit into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
applicationName:
type: string
description: Default application name if none is specified
args:
type: '[]string'
description: Extra args to HCP Vault Secrets CLI command
command:
default: '`vlt`'
description: HCP Vault Secrets CLI command
organizationId:
type: string
description: Default organization ID if none is specified
projectId:
type: string
description: Default project ID if none 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* [*application-name* [*project-id* [*organization-id*]]]

`hcpVaultSecret` returns the plaintext secret from [HCP Vault
Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using `vlt
secrets get --plaintext`.

If any of *application-name*, *project-id*, or *organization-id* are empty or
omitted, then chezmoi will use the value from the
`hcpVaultSecret.applicationName`, `hcpVaultSecret.projectId`, and
`hcpVaultSecret.organizationId` 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* [*application-name* [*project-id* [*organization-id*]]]

`hcpVaultSecretJson` returns structured data from [HCP Vault
Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) using `vlt
secrets get --format=json`.

If any of *application-name*, *project-id*, or *organization-id* are empty or omitted, then
chezmoi will use the value from the `hcpVaultSecret.applicationName`,
`hcpVaultSecret.projectId`, and `hcpVaultSecret.organizationId` config variables
if they are set and not empty.

!!! example

```
{{ (hcpVaultSecretJson "secret_name" "application_name").created_by.email }}
```
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 interface to it may change.
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,47 @@
# 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 interface to it may change.

Log in using:

```console
$ vlt login
```

The output of the `vlt secrets get --plaintext $SECRET_NAME` is available as the
`hcpVaultSecret` function, for example:

```
{{ hcpVaultSecret "secret_name" "application_name" "project_id" "organization_id" }}
```

You can set the default values for the application name, project ID, and
organization ID in your config file, for example:

```toml title="~/.config/chezmoi/chezmoi.toml"
[hcpVaultSecrets]
organizationId = "bf479eab-a292-4b46-92df-e22f5c47eadc"
projectId = "5907a2fa-d26a-462a-8705-74dfe967e87d"
applicationName = "my-application"
```

With these default values, you can omit them in the call to `hcpVaultSecret`, for example:

```
{{ hcpVaultSecret "secret_name" }}
{{ hcpVaultSecret "other_secret_name" "other_application_name" }}
```

Structured data from `vlt secrets get --format=json $SECRET_NAME` is available
as the `hcpVaultSecretJson` template function, for example:

```
{{ (hcpVaultSecretJson "secret_name").created_by.email }}
```
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 @@ -2577,6 +2580,9 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile {
Gopass: gopassConfig{
Command: "gopass",
},
HCPVaultSecrets: hcpVaultSecretConfig{
Command: "vlt",
},
Keepassxc: keepassxcConfig{
Command: "keepassxc-cli",
Prompt: true,
Expand Down
9 changes: 9 additions & 0 deletions pkg/cmd/doctorcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,15 @@ func (c *Config) runDoctorCmd(cmd *cobra.Command, args []string) error {
versionArgs: []string{"version"},
versionRx: regexp.MustCompile(`^Vault\s+v(\d+\.\d+\.\d+)`),
},
&binaryCheck{
name: "vlt-command",
binaryname: c.HCPVaultSecrets.Command,
ifNotSet: checkResultWarning,
ifNotExist: checkResultInfo,
versionArgs: []string{"version"},
versionRx: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
minVersion: &vltMinVersion,
},
&binaryCheck{
name: "secret-command",
binaryname: c.Secret.Command,
Expand Down
111 changes: 111 additions & 0 deletions pkg/cmd/hcpvaultsecretsttemplatefuncs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"github.com/coreos/go-semver/semver"
"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"`
ApplicationName string `json:"applicationName" mapstructure:"applicationName" yaml:"applicationName"`
OrganizationID string `json:"organizationId" mapstructure:"organizationId" yaml:"organizationId"`
ProjectID string `json:"projectId" mapstructure:"projectId" yaml:"projectId"`
outputCache map[string][]byte
}

var vltMinVersion = semver.Version{Major: 0, Minor: 2, Patch: 1}

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.ApplicationName != "" {
args = append(args, "--app-name", c.HCPVaultSecrets.ApplicationName)
}
if len(additionalArgs) > 1 && additionalArgs[1] != "" {
args = append(args, "--project", additionalArgs[1])
} else if c.HCPVaultSecrets.ProjectID != "" {
args = append(args, "--project", c.HCPVaultSecrets.ProjectID)
}
if len(additionalArgs) > 2 && additionalArgs[2] != "" {
args = append(args, "--organization", additionalArgs[2])
} else if c.HCPVaultSecrets.OrganizationID != "" {
args = append(args, "--organization", c.HCPVaultSecrets.OrganizationID)
}
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
// Always run the vlt command in the destination path because vlt uses
// relative paths to find its .vlt.json config file.
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
}
8 changes: 7 additions & 1 deletion pkg/cmd/testdata/scripts/doctor_unix.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ chmod 755 bin/secret
chmod 755 bin/shell
chmod 755 bin/vault
chmod 755 bin/vimdiff
chmod 755 bin/vlt

mkhomedir
mksourcedir
Expand Down Expand Up @@ -54,8 +55,9 @@ stdout '^ok\s+keeper-command\s+'
stdout '^ok\s+passhole-command\s+'
stdout '^ok\s+lastpass-command\s+'
stdout '^ok\s+pass-command\s+'
stdout '^ok\s+vault-command\s+'
stdout '^ok\s+rbw-command\s+'
stdout '^ok\s+vault-command\s+'
stdout '^ok\s+vlt-command\s+'
stdout '^ok\s+secret-command\s+'

chhome home2/user
Expand Down Expand Up @@ -171,6 +173,10 @@ echo rbw 1.7.0
echo "Vault v1.5.5 ('f5d1ddb3750e7c28e25036e1ef26a4c02379fc01+CHANGES')"
-- bin/vimdiff --
#!/bin/sh
-- bin/vlt --
#!/bin/sh

echo "0.2.1, git sha (8d9af42c8b98c9527741a239b23a3e384812f514), go1.20.4 arm64"
-- home/user/.config/chezmoi/chezmoi.toml --
[keepassxc]
command = "keepassxc"
Expand Down
Loading
Loading