Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault Provider #9158

Merged
merged 5 commits into from
Nov 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions builtin/bins/provider-vault/main.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
106 changes: 106 additions & 0 deletions builtin/providers/vault/data_source_generic_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package vault

import (
"encoding/json"
"fmt"
"log"
"time"

"github.com/hashicorp/terraform/helper/schema"

"github.com/hashicorp/vault/api"
)

func genericSecretDataSource() *schema.Resource {
return &schema.Resource{
Read: genericSecretDataSourceRead,

Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Full path from which a secret will be read.",
},

"data_json": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "JSON-encoded secret data read from Vault.",
},

"data": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
Description: "Map of strings read from Vault.",
},

"lease_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "Lease identifier assigned by vault.",
},

"lease_duration": &schema.Schema{
Type: schema.TypeInt,
Computed: true,
Description: "Lease duration in seconds relative to the time in lease_start_time.",
},

"lease_start_time": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "Time at which the lease was read, using the clock of the system where Terraform was running",
},

"lease_renewable": &schema.Schema{
Type: schema.TypeBool,
Computed: true,
Description: "True if the duration of this lease can be extended through renewal.",
},
},
}
}

func genericSecretDataSourceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)

path := d.Get("path").(string)

log.Printf("[DEBUG] Reading %s from Vault", path)
secret, err := client.Logical().Read(path)
if err != nil {
return fmt.Errorf("error reading from Vault: %s", err)
}

d.SetId(secret.RequestID)

// Ignoring error because this value came from JSON in the
// first place so no reason why it should fail to re-encode.
jsonDataBytes, _ := json.Marshal(secret.Data)
d.Set("data_json", string(jsonDataBytes))

// Since our "data" map can only contain string values, we
// will take strings from Data and write them in as-is,
// and write everything else in as a JSON serialization of
// whatever value we get so that complex types can be
// passed around and processed elsewhere if desired.
dataMap := map[string]string{}
for k, v := range secret.Data {
if vs, ok := v.(string); ok {
dataMap[k] = vs
} else {
// Again ignoring error because we know this value
// came from JSON in the first place and so must be valid.
vBytes, _ := json.Marshal(v)
dataMap[k] = string(vBytes)
}
}
d.Set("data", dataMap)

d.Set("lease_id", secret.LeaseID)
d.Set("lease_duration", secret.LeaseDuration)
d.Set("lease_start_time", time.Now().Format("RFC3339"))
d.Set("lease_renewable", secret.Renewable)

return nil
}
62 changes: 62 additions & 0 deletions builtin/providers/vault/data_source_generic_secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package vault

import (
"fmt"
"testing"

r "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestDataSourceGenericSecret(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
r.TestStep{
Config: testDataSourceGenericSecret_config,
Check: testDataSourceGenericSecret_check,
},
},
})
}

var testDataSourceGenericSecret_config = `

resource "vault_generic_secret" "test" {
path = "secret/foo"
data_json = <<EOT
{
"zip": "zap"
}
EOT
}

data "vault_generic_secret" "test" {
path = "${vault_generic_secret.test.path}"
}

`

func testDataSourceGenericSecret_check(s *terraform.State) error {
resourceState := s.Modules[0].Resources["data.vault_generic_secret.test"]
if resourceState == nil {
return fmt.Errorf("resource not found in state %v", s.Modules[0].Resources)
}

iState := resourceState.Primary
if iState == nil {
return fmt.Errorf("resource has no primary instance")
}

wantJson := `{"zip":"zap"}`
if got, want := iState.Attributes["data_json"], wantJson; got != want {
return fmt.Errorf("data_json contains %s; want %s", got, want)
}

if got, want := iState.Attributes["data.zip"], "zap"; got != want {
return fmt.Errorf("data[\"zip\"] contains %s; want %s", got, want)
}

return nil
}
160 changes: 160 additions & 0 deletions builtin/providers/vault/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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,

DataSourcesMap: map[string]*schema.Resource{
"vault_generic_secret": genericSecretDataSource(),
},

ResourcesMap: map[string]*schema.Resource{
"vault_generic_secret": genericSecretResource(),
},
}
}

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