| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "os" |
| paths "path" |
| "sort" |
| "strings" |
| "unicode" |
| |
| "github.com/hashicorp/hcl/v2/gohcl" |
| "github.com/hashicorp/hcl/v2/hclwrite" |
| "github.com/hashicorp/vault/api" |
| "github.com/mitchellh/cli" |
| "github.com/mitchellh/go-homedir" |
| "github.com/posener/complete" |
| ) |
| |
| var ( |
| _ cli.Command = (*AgentGenerateConfigCommand)(nil) |
| _ cli.CommandAutocomplete = (*AgentGenerateConfigCommand)(nil) |
| ) |
| |
| type AgentGenerateConfigCommand struct { |
| *BaseCommand |
| |
| flagType string |
| flagPaths []string |
| flagExec string |
| } |
| |
| func (c *AgentGenerateConfigCommand) Synopsis() string { |
| return "Generate a Vault Agent configuration file." |
| } |
| |
| func (c *AgentGenerateConfigCommand) Help() string { |
| helpText := ` |
| Usage: vault agent generate-config [options] [path/to/config.hcl] |
| |
| Generates a simple Vault Agent configuration file from the given parameters. |
| |
| Currently, the only supported configuration type is 'env-template', which |
| helps you generate a configuration file with environment variable templates |
| for running Vault Agent in process supervisor mode. |
| |
| For every specified secret -path, the command will attempt to generate one or |
| multiple 'env_template' entries based on the JSON key(s) stored in the |
| specified secret. If the secret -path ends with '/*', the command will |
| attempt to recurse through the secrets tree rooted at the given path, |
| generating 'env_template' entries for each encountered secret. Currently, |
| only kv-v1 and kv-v2 paths are supported. |
| |
| The command specified in the '-exec' option will be used to generate an |
| 'exec' entry, which will tell Vault Agent which child process to run. |
| |
| In addition to env_template entries, the command generates an 'auto_auth' |
| section with 'token_file' authentication method. While this method is very |
| convenient for local testing, it should NOT be used in production. Please |
| see https://developer.hashicorp.com/vault/docs/agent-and-proxy/autoauth/methods |
| for a list of production-ready auto_auth methods that you can use instead. |
| |
| By default, the file will be generated in the local directory as 'agent.hcl' |
| unless a path is specified as an argument. |
| |
| Generate a simple environment variable template configuration: |
| |
| $ vault agent generate-config -type="env-template" \ |
| -exec="./my-app arg1 arg2" \ |
| -path="secret/foo" |
| |
| Generate an environment variable template configuration for multiple secrets: |
| |
| $ vault agent generate-config -type="env-template" \ |
| -exec="./my-app arg1 arg2" \ |
| -path="secret/foo" \ |
| -path="secret/bar" \ |
| -path="secret/my-app/*" |
| |
| ` + c.Flags().Help() |
| |
| return strings.TrimSpace(helpText) |
| } |
| |
| func (c *AgentGenerateConfigCommand) Flags() *FlagSets { |
| // Include client-modifying flags (-address, -namespace, etc.) |
| set := c.flagSet(FlagSetHTTP) |
| |
| // Common Options |
| f := set.NewFlagSet("Command Options") |
| |
| f.StringVar(&StringVar{ |
| Name: "type", |
| Target: &c.flagType, |
| Usage: "Type of configuration file to generate; currently, only 'env-template' is supported.", |
| Completion: complete.PredictSet( |
| "env-template", |
| ), |
| }) |
| |
| f.StringSliceVar(&StringSliceVar{ |
| Name: "path", |
| Target: &c.flagPaths, |
| Usage: "Path to a kv-v1 or kv-v2 secret (e.g. secret/data/foo, kv-v2/prefix/*); multiple secrets and tail '*' wildcards are allowed.", |
| Completion: c.PredictVaultFolders(), |
| }) |
| |
| f.StringVar(&StringVar{ |
| Name: "exec", |
| Target: &c.flagExec, |
| Default: "env", |
| Usage: "The command to execute in agent process supervisor mode.", |
| }) |
| |
| return set |
| } |
| |
| func (c *AgentGenerateConfigCommand) AutocompleteArgs() complete.Predictor { |
| return complete.PredictNothing |
| } |
| |
| func (c *AgentGenerateConfigCommand) AutocompleteFlags() complete.Flags { |
| return c.Flags().Completions() |
| } |
| |
| func (c *AgentGenerateConfigCommand) Run(args []string) int { |
| flags := c.Flags() |
| |
| if err := flags.Parse(args); err != nil { |
| c.UI.Error(err.Error()) |
| return 1 |
| } |
| |
| args = flags.Args() |
| |
| if len(args) > 1 { |
| c.UI.Error(fmt.Sprintf("Too many arguments (expected at most 1, got %d)", len(args))) |
| return 1 |
| } |
| |
| if c.flagType == "" { |
| c.UI.Error(`Please specify a -type flag; currently only -type="env-template" is supported.`) |
| return 1 |
| } |
| |
| if c.flagType != "env-template" { |
| c.UI.Error(fmt.Sprintf(`%q is not a supported configuration type; currently only -type="env-template" is supported.`, c.flagType)) |
| return 1 |
| } |
| |
| client, err := c.Client() |
| if err != nil { |
| c.UI.Error(err.Error()) |
| return 2 |
| } |
| |
| config, err := generateConfiguration(context.Background(), client, c.flagExec, c.flagPaths) |
| if err != nil { |
| c.UI.Error(fmt.Sprintf("Error: %v", err)) |
| return 2 |
| } |
| |
| var configPath string |
| if len(args) == 1 { |
| configPath = args[0] |
| } else { |
| configPath = "agent.hcl" |
| } |
| |
| f, err := os.Create(configPath) |
| if err != nil { |
| c.UI.Error(fmt.Sprintf("Could not create configuration file %q: %v", configPath, err)) |
| return 3 |
| } |
| defer func() { |
| if err := f.Close(); err != nil { |
| c.UI.Error(fmt.Sprintf("Could not close configuration file %q: %v", configPath, err)) |
| } |
| }() |
| |
| if _, err := config.WriteTo(f); err != nil { |
| c.UI.Error(fmt.Sprintf("Could not write to configuration file %q: %v", configPath, err)) |
| return 3 |
| } |
| |
| c.UI.Info(fmt.Sprintf("Successfully generated %q configuration file!", configPath)) |
| |
| c.UI.Warn("Warning: the generated file uses 'token_file' authentication method, which is not suitable for production environments.") |
| |
| return 0 |
| } |
| |
| func generateConfiguration(ctx context.Context, client *api.Client, flagExec string, flagPaths []string) (io.WriterTo, error) { |
| var execCommand []string |
| if flagExec != "" { |
| execCommand = strings.Split(flagExec, " ") |
| } else { |
| execCommand = []string{"env"} |
| } |
| |
| tokenPath, err := homedir.Expand("~/.vault-token") |
| if err != nil { |
| return nil, fmt.Errorf("could not expand home directory: %w", err) |
| } |
| |
| templates, err := constructTemplates(ctx, client, flagPaths) |
| if err != nil { |
| return nil, fmt.Errorf("could not generate templates: %w", err) |
| } |
| |
| config := generatedConfig{ |
| AutoAuth: generatedConfigAutoAuth{ |
| Method: generatedConfigAutoAuthMethod{ |
| Type: "token_file", |
| Config: generatedConfigAutoAuthMethodConfig{ |
| TokenFilePath: tokenPath, |
| }, |
| }, |
| }, |
| TemplateConfig: generatedConfigTemplateConfig{ |
| StaticSecretRenderInterval: "5m", |
| ExitOnRetryFailure: true, |
| }, |
| Vault: generatedConfigVault{ |
| Address: client.Address(), |
| }, |
| Exec: generatedConfigExec{ |
| Command: execCommand, |
| RestartOnSecretChanges: "always", |
| RestartStopSignal: "SIGTERM", |
| }, |
| EnvTemplates: templates, |
| } |
| |
| contents := hclwrite.NewEmptyFile() |
| |
| gohcl.EncodeIntoBody(&config, contents.Body()) |
| |
| return contents, nil |
| } |
| |
| func constructTemplates(ctx context.Context, client *api.Client, paths []string) ([]generatedConfigEnvTemplate, error) { |
| var templates []generatedConfigEnvTemplate |
| |
| for _, path := range paths { |
| path = sanitizePath(path) |
| |
| mountPath, v2, err := isKVv2(path, client) |
| if err != nil { |
| return nil, fmt.Errorf("could not validate secret path %q: %w", path, err) |
| } |
| |
| switch { |
| case strings.HasSuffix(path, "/*"): |
| // this path contains a tail wildcard, attempt to walk the tree |
| t, err := constructTemplatesFromTree(ctx, client, path[:len(path)-2], mountPath, v2) |
| if err != nil { |
| return nil, fmt.Errorf("could not traverse sercet at %q: %w", path, err) |
| } |
| templates = append(templates, t...) |
| |
| case strings.Contains(path, "*"): |
| // don't allow any other wildcards |
| return nil, fmt.Errorf("the path %q cannot contain '*' wildcard characters except as the last element of the path", path) |
| |
| default: |
| // regular secret path |
| t, err := constructTemplatesFromSecret(ctx, client, path, mountPath, v2) |
| if err != nil { |
| return nil, fmt.Errorf("could not read secret at %q: %v", path, err) |
| } |
| templates = append(templates, t...) |
| } |
| } |
| |
| return templates, nil |
| } |
| |
| func constructTemplatesFromTree(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { |
| var templates []generatedConfigEnvTemplate |
| |
| if v2 { |
| metadataPath := strings.Replace( |
| path, |
| paths.Join(mountPath, "data"), |
| paths.Join(mountPath, "metadata"), |
| 1, |
| ) |
| if path != metadataPath { |
| path = metadataPath |
| } else { |
| path = addPrefixToKVPath(path, mountPath, "metadata", true) |
| } |
| } |
| |
| err := walkSecretsTree(ctx, client, path, func(child string, directory bool) error { |
| if directory { |
| return nil |
| } |
| |
| dataPath := strings.Replace( |
| child, |
| paths.Join(mountPath, "metadata"), |
| paths.Join(mountPath, "data"), |
| 1, |
| ) |
| |
| t, err := constructTemplatesFromSecret(ctx, client, dataPath, mountPath, v2) |
| if err != nil { |
| return err |
| } |
| templates = append(templates, t...) |
| |
| return nil |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| return templates, nil |
| } |
| |
| func constructTemplatesFromSecret(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { |
| var templates []generatedConfigEnvTemplate |
| |
| if v2 { |
| path = addPrefixToKVPath(path, mountPath, "data", true) |
| } |
| |
| resp, err := client.Logical().ReadWithContext(ctx, path) |
| if err != nil { |
| return nil, fmt.Errorf("error querying: %w", err) |
| } |
| if resp == nil { |
| return nil, fmt.Errorf("secret not found") |
| } |
| |
| var data map[string]interface{} |
| if v2 { |
| internal, ok := resp.Data["data"] |
| if !ok { |
| return nil, fmt.Errorf("secret.Data not found") |
| } |
| data = internal.(map[string]interface{}) |
| } else { |
| data = resp.Data |
| } |
| |
| fields := make([]string, 0, len(data)) |
| |
| for field := range data { |
| fields = append(fields, field) |
| } |
| |
| // sort for a deterministic output |
| sort.Strings(fields) |
| |
| var dataContents string |
| if v2 { |
| dataContents = ".Data.data" |
| } else { |
| dataContents = ".Data" |
| } |
| |
| for _, field := range fields { |
| templates = append(templates, generatedConfigEnvTemplate{ |
| Name: constructDefaultEnvironmentKey(path, field), |
| Contents: fmt.Sprintf(`{{ with secret "%s" }}{{ %s.%s }}{{ end }}`, path, dataContents, field), |
| ErrorOnMissingKey: true, |
| }) |
| } |
| |
| return templates, nil |
| } |
| |
| func constructDefaultEnvironmentKey(path string, field string) string { |
| pathParts := strings.Split(path, "/") |
| pathPartsLast := pathParts[len(pathParts)-1] |
| |
| notLetterOrNumber := func(r rune) bool { |
| return !unicode.IsLetter(r) && !unicode.IsNumber(r) |
| } |
| |
| p1 := strings.FieldsFunc(pathPartsLast, notLetterOrNumber) |
| p2 := strings.FieldsFunc(field, notLetterOrNumber) |
| |
| keyParts := append(p1, p2...) |
| |
| return strings.ToUpper(strings.Join(keyParts, "_")) |
| } |
| |
| // Below, we are redefining a subset of the configuration-related structures |
| // defined under command/agent/config. Using these structures we can tailor the |
| // output of the generated config, while using the original structures would |
| // have produced an HCL document with many empty fields. The structures below |
| // should not be used for anything other than generation. |
| |
| type generatedConfig struct { |
| AutoAuth generatedConfigAutoAuth `hcl:"auto_auth,block"` |
| TemplateConfig generatedConfigTemplateConfig `hcl:"template_config,block"` |
| Vault generatedConfigVault `hcl:"vault,block"` |
| EnvTemplates []generatedConfigEnvTemplate `hcl:"env_template,block"` |
| Exec generatedConfigExec `hcl:"exec,block"` |
| } |
| |
| type generatedConfigTemplateConfig struct { |
| StaticSecretRenderInterval string `hcl:"static_secret_render_interval"` |
| ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"` |
| } |
| |
| type generatedConfigExec struct { |
| Command []string `hcl:"command"` |
| RestartOnSecretChanges string `hcl:"restart_on_secret_changes"` |
| RestartStopSignal string `hcl:"restart_stop_signal"` |
| } |
| |
| type generatedConfigEnvTemplate struct { |
| Name string `hcl:"name,label"` |
| Contents string `hcl:"contents,attr"` |
| ErrorOnMissingKey bool `hcl:"error_on_missing_key"` |
| } |
| |
| type generatedConfigVault struct { |
| Address string `hcl:"address"` |
| } |
| |
| type generatedConfigAutoAuth struct { |
| Method generatedConfigAutoAuthMethod `hcl:"method,block"` |
| } |
| |
| type generatedConfigAutoAuthMethod struct { |
| Type string `hcl:"type"` |
| Config generatedConfigAutoAuthMethodConfig `hcl:"config,block"` |
| } |
| |
| type generatedConfigAutoAuthMethodConfig struct { |
| TokenFilePath string `hcl:"token_file_path"` |
| } |