"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.
This commit is contained in:
Martin Atkins 2016-09-30 11:44:12 -07:00
parent 23431f4246
commit b2b5831205
4 changed files with 229 additions and 0 deletions

View File

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

View File

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

View File

@ -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")
}
}

View File

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