From b2b5831205125a1c2bb6e7edca610d63eb584edc Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 30 Sep 2016 11:44:12 -0700 Subject: [PATCH] "vault" provider registration To reduce the risk of secret exposure via Terraform state and log output, we default to creating a relatively-short-lived token (20 minutes) such that Vault can, where possible, automatically revoke any retrieved secrets shortly after Terraform has finished running. This has some implications for usage of this provider that will be spelled out in more detail in the docs that will be added in a later commit, but the most significant implication is that a plan created by "terraform plan" that includes secrets leased from Vault must be *applied* before the lease period expires to ensure that the issued secrets remain valid. No resources yet. They will follow in subsequent commits. --- builtin/bins/provider-vault/main.go | 12 ++ builtin/providers/vault/provider.go | 155 +++++++++++++++++++++++ builtin/providers/vault/provider_test.go | 60 +++++++++ command/internal_plugin_list.go | 2 + 4 files changed, 229 insertions(+) create mode 100644 builtin/bins/provider-vault/main.go create mode 100644 builtin/providers/vault/provider.go create mode 100644 builtin/providers/vault/provider_test.go diff --git a/builtin/bins/provider-vault/main.go b/builtin/bins/provider-vault/main.go new file mode 100644 index 000000000..720a9a513 --- /dev/null +++ b/builtin/bins/provider-vault/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/vault" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: vault.Provider, + }) +} diff --git a/builtin/providers/vault/provider.go b/builtin/providers/vault/provider.go new file mode 100644 index 000000000..157299e3b --- /dev/null +++ b/builtin/providers/vault/provider.go @@ -0,0 +1,155 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_ADDR", nil), + Description: "URL of the root of the target Vault server.", + }, + "token": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_TOKEN", nil), + Description: "Token to use to authenticate to Vault.", + }, + "ca_cert_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ca_cert_dir"}, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CACERT", nil), + Description: "Path to a CA certificate file to validate the server's certificate.", + }, + "ca_cert_dir": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ca_cert_file"}, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CAPATH", nil), + Description: "Path to directory containing CA certificate files to validate the server's certificate.", + }, + "client_auth": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Client authentication credentials.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cert_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CLIENT_CERT", nil), + Description: "Path to a file containing the client certificate.", + }, + "key_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CLIENT_KEY", nil), + Description: "Path to a file containing the private key that the certificate was issued for.", + }, + }, + }, + }, + "skip_tls_verify": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_SKIP_VERIFY", nil), + Description: "Set this to true only if the target Vault server is an insecure development instance.", + }, + "max_lease_ttl_seconds": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + + // Default is 20min, which is intended to be enough time for + // a reasonable Terraform run can complete but not + // significantly longer, so that any leases are revoked shortly + // after Terraform has finished running. + DefaultFunc: schema.EnvDefaultFunc("TERRAFORM_VAULT_MAX_TTL", 1200), + + Description: "Maximum TTL for secret leases requested by this provider", + }, + }, + + ConfigureFunc: providerConfigure, + + ResourcesMap: map[string]*schema.Resource{ + }, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := &api.Config{ + Address: d.Get("address").(string), + } + + clientAuthI := d.Get("client_auth").([]interface{}) + if len(clientAuthI) > 1 { + return nil, fmt.Errorf("client_auth block may appear only once") + } + + clientAuthCert := "" + clientAuthKey := "" + if len(clientAuthI) == 1 { + clientAuth := clientAuthI[0].(map[string]interface{}) + clientAuthCert = clientAuth["cert_file"].(string) + clientAuthKey = clientAuth["key_file"].(string) + } + + config.ConfigureTLS(&api.TLSConfig{ + CACert: d.Get("ca_cert_file").(string), + CAPath: d.Get("ca_cert_dir").(string), + Insecure: d.Get("skip_tls_verify").(bool), + + ClientCert: clientAuthCert, + ClientKey: clientAuthKey, + }) + + client, err := api.NewClient(config) + if err != nil { + return nil, fmt.Errorf("failed to configure Vault API: %s", err) + } + + // In order to enforce our relatively-short lease TTL, we derive a + // temporary child token that inherits all of the policies of the + // token we were given but expires after max_lease_ttl_seconds. + // + // The intent here is that Terraform will need to re-fetch any + // secrets on each run and so we limit the exposure risk of secrets + // that end up stored in the Terraform state, assuming that they are + // credentials that Vault is able to revoke. + // + // Caution is still required with state files since not all secrets + // can explicitly be revoked, and this limited scope won't apply to + // any secrets that are *written* by Terraform to Vault. + + client.SetToken(d.Get("token").(string)) + renewable := false + childTokenLease, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + DisplayName: "terraform", + TTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds").(int)), + ExplicitMaxTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds").(int)), + Renewable: &renewable, + }) + if err != nil { + return nil, fmt.Errorf("failed to create limited child token: %s", err) + } + + childToken := childTokenLease.Auth.ClientToken + policies := childTokenLease.Auth.Policies + + log.Printf("[INFO] Using Vault token with the following policies: %s", strings.Join(policies, ", ")) + + client.SetToken(childToken) + + return client, nil +} diff --git a/builtin/providers/vault/provider_test.go b/builtin/providers/vault/provider_test.go new file mode 100644 index 000000000..f26d163e1 --- /dev/null +++ b/builtin/providers/vault/provider_test.go @@ -0,0 +1,60 @@ +package vault + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// How to run the acceptance tests for this provider: +// +// - Obtain an official Vault release from the Vault website at +// https://vaultproject.io/ and extract the "vault" binary +// somewhere. +// +// - Run the following to start the Vault server in development mode: +// vault server -dev +// +// - Take the "Root Token" value printed by Vault as the server started +// up and set it as the value of the VAULT_TOKEN environment variable +// in a new shell whose current working directory is the root of the +// Terraform repository. +// +// - As directed by the Vault server output, set the VAULT_ADDR environment +// variable. e.g.: +// export VAULT_ADDR='http://127.0.0.1:8200' +// +// - Run the Terraform acceptance tests as usual: +// make testacc TEST=./builtin/providers/vault +// +// The tests expect to be run in a fresh, empty Vault and thus do not attempt +// to randomize or otherwise make the generated resource paths unique on +// each run. In case of weird behavior, restart the Vault dev server to +// start over with a fresh Vault. (Remember to reset VAULT_TOKEN.) + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +var testProvider *schema.Provider +var testProviders map[string]terraform.ResourceProvider + +func init() { + testProvider = Provider().(*schema.Provider) + testProviders = map[string]terraform.ResourceProvider{ + "vault": testProvider, + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("VAULT_ADDR"); v == "" { + t.Fatal("VAULT_ADDR must be set for acceptance tests") + } + if v := os.Getenv("VAULT_TOKEN"); v == "" { + t.Fatal("VAULT_TOKEN must be set for acceptance tests") + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index e40de2664..e808dc813 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -52,6 +52,7 @@ import ( tlsprovider "github.com/hashicorp/terraform/builtin/providers/tls" tritonprovider "github.com/hashicorp/terraform/builtin/providers/triton" ultradnsprovider "github.com/hashicorp/terraform/builtin/providers/ultradns" + vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault" vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd" vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere" chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" @@ -110,6 +111,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "tls": tlsprovider.Provider, "triton": tritonprovider.Provider, "ultradns": ultradnsprovider.Provider, + "vault": vaultprovider.Provider, "vcd": vcdprovider.Provider, "vsphere": vsphereprovider.Provider, }