diff --git a/vault/provider.go b/vault/provider.go index 804148549..41751fdea 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -142,6 +142,7 @@ func Provider() terraform.ResourceProvider { "vault_mount": mountResource(), "vault_audit": auditResource(), "vault_ssh_secret_backend_ca": sshSecretBackendCAResource(), + "vault_ssh_secret_backend_role": sshSecretBackendRoleResource(), "vault_identity_entity": identityEntityResource(), "vault_identity_entity_alias": identityEntityAliasResource(), "vault_identity_group": identityGroupResource(), diff --git a/vault/resource_ssh_secret_backend_role.go b/vault/resource_ssh_secret_backend_role.go new file mode 100644 index 000000000..e971c6b05 --- /dev/null +++ b/vault/resource_ssh_secret_backend_role.go @@ -0,0 +1,269 @@ +package vault + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +var ( + sshSecretBackendRoleBackendFromPathRegex = regexp.MustCompile("^(.+)/roles/.+$") + sshSecretBackendRoleNameFromPathRegex = regexp.MustCompile("^.+/roles/(.+$)") +) + +func sshSecretBackendRoleResource() *schema.Resource { + return &schema.Resource{ + Create: sshSecretBackendRoleWrite, + Read: sshSecretBackendRoleRead, + Update: sshSecretBackendRoleWrite, + Delete: sshSecretBackendRoleDelete, + Exists: sshSecretBackendRoleExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Unique name for the role.", + }, + "backend": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "allow_bare_domains": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "allow_host_certificates": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "allow_subdomains": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "allow_user_certificates": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "allow_user_key_ids": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "allowed_critical_options": { + Type: schema.TypeString, + Optional: true, + }, + "allowed_domains": { + Type: schema.TypeString, + Optional: true, + }, + "allowed_extensions": { + Type: schema.TypeString, + Optional: true, + }, + "allowed_users": { + Type: schema.TypeString, + Optional: true, + }, + "default_user": { + Type: schema.TypeString, + Optional: true, + }, + "key_id_format": { + Type: schema.TypeString, + Optional: true, + }, + "key_type": { + Type: schema.TypeString, + Required: true, + }, + "max_ttl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "ttl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func sshSecretBackendRoleWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Get("backend").(string) + name := d.Get("name").(string) + + path := sshRoleResourcePath(backend, name) + + data := map[string]interface{}{ + "key_type": d.Get("key_type").(string), + "allow_bare_domains": d.Get("allow_bare_domains").(bool), + "allow_host_certificates": d.Get("allow_host_certificates").(bool), + "allow_subdomains": d.Get("allow_subdomains").(bool), + "allow_user_certificates": d.Get("allow_user_certificates").(bool), + "allow_user_key_ids": d.Get("allow_user_key_ids").(bool), + } + + if v, ok := d.GetOk("allowed_critical_options"); ok { + data["allowed_critical_options"] = v.(string) + } + + if v, ok := d.GetOk("allowed_domains"); ok { + data["allowed_domains"] = v.(string) + } + + if v, ok := d.GetOk("allowed_extensions"); ok { + data["allowed_extensions"] = v.(string) + } + + if v, ok := d.GetOk("allowed_users"); ok { + data["allowed_users"] = v.(string) + } + + if v, ok := d.GetOk("default_user"); ok { + data["default_user"] = v.(string) + } + + if v, ok := d.GetOk("key_id_format"); ok { + data["key_id_format"] = v.(string) + } + + if v, ok := d.GetOk("max_ttl"); ok { + data["max_ttl"] = v.(string) + } + + if v, ok := d.GetOk("ttl"); ok { + data["ttl"] = v.(string) + } + + log.Printf("[DEBUG] Writing role %q on SSH backend %q", name, backend) + _, err := client.Logical().Write(path, data) + if err != nil { + return fmt.Errorf("error writing role %q for backend %q: %s", name, backend, err) + } + log.Printf("[DEBUG] Wrote role %q on SSH backend %q", name, backend) + + d.SetId(path) + return sshSecretBackendRoleRead(d, meta) +} + +func sshSecretBackendRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + + name, err := sshSecretBackendRoleNameFromPath(path) + if err != nil { + log.Printf("[WARN] Removing ssh role %q because its ID is invalid", path) + d.SetId("") + return fmt.Errorf("invalid role ID %q: %s", path, err) + } + + backend, err := sshSecretBackendRoleBackendFromPath(path) + if err != nil { + log.Printf("[WARN] Removing ssh role %q because its ID is invalid", path) + d.SetId("") + return fmt.Errorf("invalid role ID %q: %s", path, err) + } + + log.Printf("[DEBUG] Reading role from %q", path) + role, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading role %q: %s", path, err) + } + log.Printf("[DEBUG] Read role from %q", path) + if role == nil { + log.Printf("[WARN] Role %q not found, removing from state", path) + d.SetId("") + return nil + } + d.Set("name", name) + d.Set("backend", backend) + d.Set("key_type", role.Data["key_type"]) + d.Set("allow_bare_domains", role.Data["allow_bare_domains"]) + d.Set("allow_host_certificates", role.Data["allow_host_certificates"]) + d.Set("allow_subdomains", role.Data["allow_subdomains"]) + d.Set("allow_user_certificates", role.Data["allow_user_certificates"]) + d.Set("allow_user_key_ids", role.Data["allow_user_key_ids"]) + d.Set("allowed_critical_options", role.Data["allowed_critical_options"]) + d.Set("allowed_domains", role.Data["allowed_domains"]) + d.Set("allowed_extensions", role.Data["allowed_extensions"]) + d.Set("allowed_users", role.Data["allowed_users"]) + d.Set("default_user", role.Data["default_user"]) + d.Set("key_id_format", role.Data["key_id_format"]) + d.Set("max_ttl", role.Data["max_ttl"]) + d.Set("ttl", role.Data["ttl"]) + + return nil +} + +func sshSecretBackendRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Deleting role %q", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting role %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted role %q", path) + + return nil +} + +func sshSecretBackendRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Checking if %q exists", path) + role, err := client.Logical().Read(path) + if err != nil { + return true, fmt.Errorf("error checking if %q exists: %s", path, err) + } + log.Printf("[DEBUG] Checked if %q exists", path) + return role != nil, nil +} + +func sshRoleResourcePath(backend, name string) string { + return strings.Trim(backend, "/") + "/roles/" + strings.Trim(name, "/") +} + +func sshSecretBackendRoleNameFromPath(path string) (string, error) { + if !sshSecretBackendRoleNameFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no name found") + } + res := sshSecretBackendRoleNameFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for name", len(res)) + } + return res[1], nil +} + +func sshSecretBackendRoleBackendFromPath(path string) (string, error) { + if !sshSecretBackendRoleBackendFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := sshSecretBackendRoleBackendFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_ssh_secret_backend_role_test.go b/vault/resource_ssh_secret_backend_role_test.go new file mode 100644 index 000000000..eaf8956e8 --- /dev/null +++ b/vault/resource_ssh_secret_backend_role_test.go @@ -0,0 +1,166 @@ +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 TestAccSSHSecretBackendRole_basic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test/ssh") + name := acctest.RandomWithPrefix("tf-test-role") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccSSHSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSSHSecretBackendRoleConfig_basic(name, backend), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "name", name), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "backend", backend), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_bare_domains", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_host_certificates", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_subdomains", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_certificates", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_key_ids", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_critical_options", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_domains", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_extensions", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_users", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "default_user", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_id_format", ""), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_type", "ca"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "max_ttl", "0"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "ttl", "0"), + ), + }, + { + Config: testAccSSHSecretBackendRoleConfig_updated(name, backend), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "name", name), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "backend", backend), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_bare_domains", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_host_certificates", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_subdomains", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_certificates", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_key_ids", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_critical_options", "foo,bar"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_domains", "example.com,foo.com"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_extensions", "ext1,ext2"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_users", "usr1,usr2"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "default_user", "usr"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_id_format", "{{role_name}}-test"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_type", "ca"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "max_ttl", "86400"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "ttl", "43200"), + ), + }, + }, + }) +} + +func TestAccSSHSecretBackendRole_import(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test/ssh") + name := acctest.RandomWithPrefix("tf-test-role") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAccSSHSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSSHSecretBackendRoleConfig_updated(name, backend), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "name", name), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "backend", backend), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_bare_domains", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_host_certificates", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_subdomains", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_certificates", "false"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allow_user_key_ids", "true"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_critical_options", "foo,bar"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_domains", "example.com,foo.com"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_extensions", "ext1,ext2"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "allowed_users", "usr1,usr2"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "default_user", "usr"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_id_format", "{{role_name}}-test"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "key_type", "ca"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "max_ttl", "86400"), + resource.TestCheckResourceAttr("vault_ssh_secret_backend_role.test_role", "ttl", "43200"), + ), + }, + { + ResourceName: "vault_ssh_secret_backend_role.test_role", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccSSHSecretBackendRoleCheckDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_ssh_secret_backend_role" { + continue + } + role, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return err + } + if role != nil { + return fmt.Errorf("role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testAccSSHSecretBackendRoleConfig_basic(name, path string) string { + return fmt.Sprintf(` +resource "vault_mount" "example" { + path = "%s" + type = "ssh" +} + +resource "vault_ssh_secret_backend_role" "test_role" { + name = "%s" + backend = "${vault_mount.example.path}" + key_type = "ca" + allow_user_certificates = true +} + +`, path, name) +} + +func testAccSSHSecretBackendRoleConfig_updated(name, path string) string { + return fmt.Sprintf(` +resource "vault_mount" "example" { + path = "%s" + type = "ssh" +} + +resource "vault_ssh_secret_backend_role" "test_role" { + name = "%s" + backend = "${vault_mount.example.path}" + allow_bare_domains = true + allow_host_certificates = true + allow_subdomains = true + allow_user_certificates = false + allow_user_key_ids = true + allowed_critical_options = "foo,bar" + allowed_domains = "example.com,foo.com" + allowed_extensions = "ext1,ext2" + allowed_users = "usr1,usr2" + default_user = "usr" + key_id_format = "{{role_name}}-test" + key_type = "ca" + max_ttl = "86400" + ttl = "43200" +} +`, path, name) +} diff --git a/website/docs/r/ssh_secret_backend_role.html.md b/website/docs/r/ssh_secret_backend_role.html.md new file mode 100644 index 000000000..c834fda10 --- /dev/null +++ b/website/docs/r/ssh_secret_backend_role.html.md @@ -0,0 +1,76 @@ +--- +layout: "vault" +page_title: "Vault: vault_ssh_secret_backend_role resource" +sidebar_current: "docs-vault-resource-ssh-secret-backend-role" +description: |- + Managing roles in an SSH secret backend in Vault +--- + +# vault\_ssh\_secret\_backend\_role + +Provides a resource to manage roles in an SSH secret backend +[SSH secret backend within Vault](https://www.vaultproject.io/docs/secrets/ssh/index.html). + +## Example Usage + +```hcl +resource "vault_mount" "example" { + type = "ssh" +} + +resource "vault_ssh_secret_backend_role" "foo" { + name = "my-role" + backend = "${vault_mount.example.path}" + key_type = "ca" + allow_user_certificates = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the role to create. + +* `backend` - (Required) The path where the SSH secret backend is mounted. + +* `key_type` - (Required) Specifies the type of credentials generated by this role. This can be either `otp`, `dynamic` or `ca`. + +* `allow_bare_domains` - (Optional) Specifies if host certificates that are requested are allowed to use the base domains listed in `allowed_domains`. + +* `allow_host_certificates` - (Optional) Specifies if certificates are allowed to be signed for use as a 'host'. + +* `allow_subdomains` - (Optional) Specifies if host certificates that are requested are allowed to be subdomains of those listed in `allowed_domains`. + +* `allow_user_certificates` - (Optional) Specifies if certificates are allowed to be signed for use as a 'user'. + +* `allow_user_key_ids` - (Optional) Specifies if users can override the key ID for a signed certificate with the `key_id` field. + +* `allowed_critical_options` - (Optional) Specifies a comma-separated list of critical options that certificates can have when signed. + +* `allowed_domains` - (Optional) The list of domains for which a client can request a host certificate. + +* `allowed_extensions` - (Optional) Specifies a comma-separated list of extensions that certificates can have when signed. + +* `allowed_users` - (Optional) Specifies a comma-separated list of usernames that are to be allowed, only if certain usernames are to be allowed. + +* `default_user` - (Optional) Specifies the default username for which a credential will be generated. + +* `key_id_format` - (Optional) Specifies a custom format for the key id of a signed certificate. + +* `max_ttl` - (Optional) Specifies the Time To Live value. + +* `ttl` - (Optional) Specifies the maximum Time To Live value. + + +## Attributes Reference + +No additional attributes are exposed by this resource. + +## Import + +SSH secret backend roles can be imported using the `path`, e.g. + +``` +$ terraform import vault_ssh_secret_backend_role.foo ssh/roles/my-role +``` diff --git a/website/vault.erb b/website/vault.erb index 8d61bbaaf..bc4623ce9 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -238,10 +238,15 @@ > vault_token_auth_backend_role + > vault_ssh_secret_backend_ca + > + ssh_secret_backend_role + + > vault_rabbitmq_secret_backend