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, }