diff --git a/vault/provider.go b/vault/provider.go index 2bbf23554..f6ebaf694 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -124,6 +124,7 @@ func Provider() terraform.ResourceProvider { "vault_policy": policyResource(), "vault_mount": mountResource(), "vault_audit": auditResource(), + "vault_ssh_secret_backend_ca": sshSecretBackendCAResource(), }, } } diff --git a/vault/resource_ssh_secret_backend_ca.go b/vault/resource_ssh_secret_backend_ca.go new file mode 100644 index 000000000..8c31f5d89 --- /dev/null +++ b/vault/resource_ssh_secret_backend_ca.go @@ -0,0 +1,135 @@ +package vault + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" + "log" + "strings" +) + +func sshSecretBackendCAResource() *schema.Resource { + return &schema.Resource{ + Create: sshSecretBackendCACreate, + Read: sshSecretBackendCARead, + Delete: sshSecretBackendCADelete, + Exists: sshSecretBackendCAExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Optional: true, + Default: "ssh", + ForceNew: true, + Description: "The path of the SSH Secret Backend where the CA should be configured", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "generate_signing_key": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: "Whether Vault should generate the signing key pair internally.", + }, + "private_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Sensitive: true, + Computed: true, + Description: "Private key part the SSH CA key pair; required if generate_signing_key is false.", + }, + "public_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + Description: "Public key part the SSH CA key pair; required if generate_signing_key is false.", + }, + }, + } +} + +func sshSecretBackendCACreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + backend := d.Get("backend").(string) + + data := make(map[string]interface{}) + if generateSigningKey, ok := d.Get("generate_signing_key").(bool); ok { + data["generate_signing_key"] = generateSigningKey + } + if privateKey, ok := d.Get("private_key").(string); ok { + data["private_key"] = privateKey + } + if publicKey, ok := d.Get("public_key").(string); ok { + data["public_key"] = publicKey + } + + log.Printf("[DEBUG] Writing CA information on SSH backend %q", backend) + _, err := client.Logical().Write(backend+"/config/ca", data) + if err != nil { + return fmt.Errorf("Error writing CA information for backend %q: %s", backend, err) + } + log.Printf("[DEBUG] Written CA information on SSH backend %q", backend) + + d.SetId(backend) + return sshSecretBackendCARead(d, meta) +} + +func sshSecretBackendCARead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Id() + + log.Printf("[DEBUG] Reading CA information from SSH backend %q", backend) + secret, err := client.Logical().Read(backend + "/config/ca") + if err != nil { + return fmt.Errorf("Error reading CA information from SSH backend %q: %s", backend, err) + } + log.Printf("[DEBUG] Read CA information from SSH backend %q", backend) + if secret == nil { + log.Printf("[WARN] CA information not found in SSH backend %q, removing from state", backend) + d.SetId("") + return nil + } + d.Set("public_key", secret.Data["public_key"]) + d.Set("backend", backend) + + // the API doesn't return private_key and generate_signing_key + // So... if they drift, they drift. + + return nil +} + +func sshSecretBackendCADelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Id() + log.Printf("[DEBUG] Deleting CA configuration for SSH backend %q", backend) + _, err := client.Logical().Delete(backend + "/config/ca") + if err != nil { + return fmt.Errorf("Error deleting CA configuration for SSH backend %q: %s", backend, err) + } + log.Printf("[DEBUG] Deleted CA configuration for SSH backend %q", backend) + + return nil +} + +func sshSecretBackendCAExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + backend := d.Id() + log.Printf("[DEBUG] Checking if CA information exists for backend %q ", backend) + secret, err := client.Logical().Read(backend + "/config/ca") + if err != nil { + return true, fmt.Errorf("Error checking if CA information exists for backend %q: %s", backend, err) + } + log.Printf("[DEBUG] Checked if CA information exists for backend %q", backend) + + return secret != nil, nil +} diff --git a/vault/resource_ssh_secret_backend_ca_test.go b/vault/resource_ssh_secret_backend_ca_test.go new file mode 100644 index 000000000..ad37d7b10 --- /dev/null +++ b/vault/resource_ssh_secret_backend_ca_test.go @@ -0,0 +1,144 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestAccSSHSecretBackendCA_basic(t *testing.T) { + backend := "ssh-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckSSHSecretBackendCADestroy, + Steps: []resource.TestStep{ + { + Config: testAccSSHSecretBackendCAConfigGenerated(backend), + Check: testAccSSHSecretBackendCACheck(backend), + }, + }, + }) +} + +func TestAccSSHSecretBackendCA_provided(t *testing.T) { + backend := "ssh-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckSSHSecretBackendCADestroy, + Steps: []resource.TestStep{ + { + Config: testAccSSHSecretBackendCAConfigProvided(backend), + Check: testAccSSHSecretBackendCACheck(backend), + }, + }, + }) +} + +func TestAccSSHSecretBackend_import(t *testing.T) { + backend := "ssh-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testAccSSHSecretBackendCAConfigGenerated(backend), + Check: testAccSSHSecretBackendCACheck(backend), + }, + { + ResourceName: "vault_ssh_secret_backend_ca.test", + ImportState: true, + // state cannot be verified since generate_signing_key and private_key are not returned by the API + ImportStateVerify: false, + }, + }, + }) +} + +func testAccCheckSSHSecretBackendCADestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_ssh_secret_backend_ca" { + continue + } + backend := rs.Primary.ID + secret, err := client.Logical().Read(backend + "/config/ca") + if err != nil { + return err + } + if secret != nil { + return fmt.Errorf("CA information still exists for backend %q", rs.Primary.ID) + } + } + return nil +} + +func testAccSSHSecretBackendCAConfigGenerated(backend string) string { + return fmt.Sprintf(` +resource "vault_mount" "test" { + type = "ssh" + path = "%s" +} + +resource "vault_ssh_secret_backend_ca" "test" { + backend = "${vault_mount.test.path}" + generate_signing_key = true +}`, backend) +} + +func testAccSSHSecretBackendCAConfigProvided(backend string) string { + return fmt.Sprintf(` +resource "vault_mount" "test" { + type = "ssh" + path = "%s" +} + +resource "vault_ssh_secret_backend_ca" "test" { + backend = "${vault_mount.test.path}" + private_key = < **Important** Because Vault does not support reading the private_key back from the API, Terraform cannot detect +and correct drift on `private_key`. Changing the values, however, _will_ overwrite the previously stored values. + + +## Attributes Reference + +No additional attributes are exposed by this resource. diff --git a/website/vault.erb b/website/vault.erb index 7db94bba0..809e6f3a9 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -175,6 +175,10 @@ > vault_token_auth_backend_role + > + ssh_secret_backend_ca + +