diff --git a/common/template/funcs.go b/common/template/funcs.go index bf1de23c6..f086cdf1c 100644 --- a/common/template/funcs.go +++ b/common/template/funcs.go @@ -1,8 +1,14 @@ package template import ( + "errors" + "fmt" "log" + "os" + "strings" "sync" + + vaultapi "github.com/hashicorp/vault/api" ) // DeprecatedTemplateFunc wraps a template func to warn users that it's @@ -17,3 +23,41 @@ func DeprecatedTemplateFunc(funcName, useInstead string, deprecated func(string) return deprecated(in) } } + +// Vault retrieves a secret from an HC vault KV store +func Vault(path string, key string) (string, error) { + + if token := os.Getenv("VAULT_TOKEN"); token == "" { + return "", errors.New("Must set VAULT_TOKEN env var in order to use vault template function") + } + + vaultConfig := vaultapi.DefaultConfig() + cli, err := vaultapi.NewClient(vaultConfig) + if err != nil { + return "", fmt.Errorf("Error getting Vault client: %s", err) + } + secret, err := cli.Logical().Read(path) + if err != nil { + return "", fmt.Errorf("Error reading vault secret: %s", err) + } + if secret == nil { + return "", errors.New("Vault Secret does not exist at the given path") + } + + data, ok := secret.Data["data"] + if !ok { + // maybe ths is v1, not v2 kv store + value, ok := secret.Data[key] + if ok { + return value.(string), nil + } + + // neither v1 nor v2 proudced a valid value + return "", fmt.Errorf("Vault data was empty at the given path. Warnings: %s", strings.Join(secret.Warnings, "; ")) + } + + if val, ok := data.(map[string]interface{})[key]; ok { + return val.(string), nil + } + return "", errors.New("Vault path does not contain the requested key") +} diff --git a/go.sum b/go.sum index 49f8e6c60..182db3f56 100644 --- a/go.sum +++ b/go.sum @@ -623,6 +623,7 @@ github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.4.0 h1:+q+tmgyUB94HIdH/uVTIi/+kt3pt4sHwEZAcTyLoGsQ= github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= +github.com/zclconf/go-cty v1.5.1 h1:oALUZX+aJeEBUe2a1+uD2+UTaYfEjnKFDEMRydkGvWE= github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8= github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= diff --git a/hcl2template/function/vault.go b/hcl2template/function/vault.go new file mode 100644 index 000000000..6bec45935 --- /dev/null +++ b/hcl2template/function/vault.go @@ -0,0 +1,31 @@ +package function + +import ( + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + commontpl "github.com/hashicorp/packer/common/template" +) + +// VaultFunc constructs a function that retrieves KV secrets from HC vault +var VaultFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + { + Name: "key", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + path := args[0].AsString() + key := args[1].AsString() + + val, err := commontpl.Vault(path, key) + + return cty.StringVal(val), err + }, +}) diff --git a/hcl2template/functions.go b/hcl2template/functions.go index 821c525ba..eca271e9c 100644 --- a/hcl2template/functions.go +++ b/hcl2template/functions.go @@ -106,6 +106,7 @@ func Functions(basedir string) map[string]function.Function { "uuidv4": uuid.V4Func, "uuidv5": uuid.V5Func, "values": stdlib.ValuesFunc, + "vault": pkrfunction.VaultFunc, "yamldecode": ctyyaml.YAMLDecodeFunc, "yamlencode": ctyyaml.YAMLEncodeFunc, "zipmap": stdlib.ZipmapFunc, diff --git a/template/interpolate/funcs.go b/template/interpolate/funcs.go index 7885ae0c4..6f2c8fce1 100644 --- a/template/interpolate/funcs.go +++ b/template/interpolate/funcs.go @@ -11,11 +11,11 @@ import ( "time" consulapi "github.com/hashicorp/consul/api" + commontpl "github.com/hashicorp/packer/common/template" "github.com/hashicorp/packer/common/uuid" "github.com/hashicorp/packer/helper/common" awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager" "github.com/hashicorp/packer/version" - vaultapi "github.com/hashicorp/vault/api" strftime "github.com/jehiah/go-strftime" ) @@ -289,40 +289,8 @@ func funcGenVault(ctx *Context) interface{} { // semantic checks should catch this. return "", errors.New("Vault vars are only allowed in the variables section") } - if token := os.Getenv("VAULT_TOKEN"); token == "" { - return "", errors.New("Must set VAULT_TOKEN env var in order to " + - "use vault template function") - } - // const EnvVaultAddress = "VAULT_ADDR" - // const EnvVaultToken = "VAULT_TOKEN" - vaultConfig := vaultapi.DefaultConfig() - cli, err := vaultapi.NewClient(vaultConfig) - if err != nil { - return "", fmt.Errorf("Error getting Vault client: %s", err) - } - secret, err := cli.Logical().Read(path) - if err != nil { - return "", fmt.Errorf("Error reading vault secret: %s", err) - } - if secret == nil { - return "", errors.New("Vault Secret does not exist at the given path") - } - data, ok := secret.Data["data"] - if !ok { - // maybe ths is v1, not v2 kv store - value, ok := secret.Data[key] - if ok { - return value.(string), nil - } - - // neither v1 nor v2 proudced a valid value - return "", fmt.Errorf("Vault data was empty at the "+ - "given path. Warnings: %s", strings.Join(secret.Warnings, "; ")) - } - - value := data.(map[string]interface{})[key].(string) - return value, nil + return commontpl.Vault(path, key) } } diff --git a/website/pages/docs/from-1.5/functions/vault/index.mdx b/website/pages/docs/from-1.5/functions/vault/index.mdx new file mode 100644 index 000000000..59fb78ef5 --- /dev/null +++ b/website/pages/docs/from-1.5/functions/vault/index.mdx @@ -0,0 +1,6 @@ +--- +layout: docs +page_title: vault - Functions - Configuration Language +sidebar_title: vault Functions +description: Overview of available vault functions +--- \ No newline at end of file diff --git a/website/pages/docs/from-1.5/functions/vault/vault.mdx b/website/pages/docs/from-1.5/functions/vault/vault.mdx new file mode 100644 index 000000000..1bf1ff1be --- /dev/null +++ b/website/pages/docs/from-1.5/functions/vault/vault.mdx @@ -0,0 +1,76 @@ +--- +layout: docs +page_title: vault - Functions - Configuration Language +sidebar_title: vault +description: The vault function retrieves secrets from HashiCorp Vault KV stores. +--- + +# `vault` Function + +Secrets can be read from [Vault](https://www.vaultproject.io/) and used within +your template as user variables. the `vault` function is available _only_ +within the default value of a user variable, allowing you to default a user +variable to a vault secret. + +An example of using a v2 kv engine: + +If you store a value in vault using `vault kv put secret/hello foo=world`, you +can access it using the following: + +```hcl +locals { + foo = vault("/secret/data/hello" "foo") +} +``` + +which will assign `local.foo` with the value "world" + +An example of using a v1 kv engine: + +If you store a value in vault using: + + vault secrets enable -version=1 -path=secrets kv + vault kv put secrets/hello foo=world + +You can access it using the following: + +```hcl +locals { + foo = vault("secrets/hello", "foo") +} +``` + +This example accesses the Vault path `secret/foo` and returns the value +stored at the key `foo`, storing it as the local variable `local.foo`. + +In order for this to work, you must set the environment variables `VAULT_TOKEN` +and `VAULT_ADDR` to valid values. + +-> **NOTE:** HCL functions can be used in local variable definitions or inline +with a provisioner/post-processor. They cannot be used in global variable definitions. + +The api tool we use allows for more custom configuration of the Vault client via +environment variables. + +The full list of available environment variables is: + +```text +"VAULT_ADDR" +"VAULT_AGENT_ADDR" +"VAULT_CACERT" +"VAULT_CAPATH" +"VAULT_CLIENT_CERT" +"VAULT_CLIENT_KEY" +"VAULT_CLIENT_TIMEOUT" +"VAULT_SKIP_VERIFY" +"VAULT_NAMESPACE" +"VAULT_TLS_SERVER_NAME" +"VAULT_WRAP_TTL" +"VAULT_MAX_RETRIES" +"VAULT_TOKEN" +"VAULT_MFA" +"VAULT_RATE_LIMIT" +``` + +and detailed documentation for usage of each of those variables can be found +[here](https://www.vaultproject.io/docs/commands/#environment-variables). \ No newline at end of file