diff --git a/vault/provider.go b/vault/provider.go index 96aa68aa0..33c895ba1 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -88,11 +88,12 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "vault_auth_backend": authBackendResource(), - "vault_aws_auth_backend_role": awsAuthBackendRoleResource(), - "vault_generic_secret": genericSecretResource(), - "vault_policy": policyResource(), - "vault_mount": mountResource(), + "vault_auth_backend": authBackendResource(), + "vault_aws_auth_backend_role": awsAuthBackendRoleResource(), + "vault_aws_auth_backend_sts_role": awsAuthBackendSTSRoleResource(), + "vault_generic_secret": genericSecretResource(), + "vault_policy": policyResource(), + "vault_mount": mountResource(), }, } } diff --git a/vault/resource_aws_auth_backend_sts_role.go b/vault/resource_aws_auth_backend_sts_role.go new file mode 100644 index 000000000..81ff7bed0 --- /dev/null +++ b/vault/resource_aws_auth_backend_sts_role.go @@ -0,0 +1,184 @@ +package vault + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +var ( + awsAuthBackendSTSRoleBackendFromPathRegex = regexp.MustCompile("^auth/(.+)/config/sts/.+$") + awsAuthBackendSTSRoleAccountIDFromPathRegex = regexp.MustCompile("^auth/.+/config/sts/(.+)$") +) + +func awsAuthBackendSTSRoleResource() *schema.Resource { + return &schema.Resource{ + Create: awsAuthBackendSTSRoleCreate, + Read: awsAuthBackendSTSRoleRead, + Update: awsAuthBackendSTSRoleUpdate, + Delete: awsAuthBackendSTSRoleDelete, + Exists: awsAuthBackendSTSRoleExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + Description: "AWS account ID to be associated with STS role.", + }, + "sts_role": { + Type: schema.TypeString, + Required: true, + Description: "AWS ARN for STS role to be assumed when interacting with the account specified.", + }, + "backend": { + Type: schema.TypeString, + Optional: true, + Description: "Unique name of the auth backend to configure.", + ForceNew: true, + Default: "aws", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + }, + } +} + +func awsAuthBackendSTSRoleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Get("backend").(string) + accountID := d.Get("account_id").(string) + stsRole := d.Get("sts_role").(string) + + path := awsAuthBackendSTSRolePath(backend, accountID) + + log.Printf("[DEBUG] Writing STS role %q to AWS auth backend", path) + _, err := client.Logical().Write(path, map[string]interface{}{ + "sts_role": stsRole, + }) + + d.SetId(path) + + if err != nil { + d.SetId("") + return fmt.Errorf("Error writing STS role %q to AWS auth backend: %s", path, err) + } + log.Printf("[DEBUG] Wrote STS role %q to AWS auth backend", path) + + return awsAuthBackendSTSRoleRead(d, meta) +} + +func awsAuthBackendSTSRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + + backend, err := awsAuthBackendSTSRoleBackendFromPath(path) + if err != nil { + return fmt.Errorf("Invalid path %q for AWS auth backend STS role: %s", path, err) + } + + accountID, err := awsAuthBackendSTSRoleAccountIDFromPath(path) + if err != nil { + return fmt.Errorf("Invalid path %q for AWS auth backend STS role: %s", path, err) + } + + log.Printf("[DEBUG] Reading STS role %q from AWS auth backend", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("Error reading STS role %q from AWS auth backend %s", path, err) + } + log.Printf("[DEBUG] Read STS role %q from AWS auth backend", path) + if resp == nil { + log.Printf("[WARN} AWS auth backend STS role %q not found, removing from state", path) + d.SetId("") + return nil + } + + d.Set("backend", backend) + d.Set("account_id", accountID) + d.Set("sts_role", resp.Data["sts_role"]) + return nil +} + +func awsAuthBackendSTSRoleUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + stsRole := d.Get("sts_role").(string) + path := d.Id() + + log.Printf("[DEBUG] Updating STS role %q in AWS auth backend", path) + _, err := client.Logical().Write(path, map[string]interface{}{ + "sts_role": stsRole, + }) + if err != nil { + return fmt.Errorf("Error updating STS role %q in AWS auth backend", path) + } + log.Printf("[DEBUG] Updated STS role %q in AWS auth backend", path) + + return awsAuthBackendSTSRoleRead(d, meta) +} + +func awsAuthBackendSTSRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Deleting STS role %q from AWS auth backend", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("Error deleting STS role %q from AWS auth backend", path) + } + log.Printf("[DEBUG] Deleted STS role %q from AWS auth backend", path) + + return nil +} + +func awsAuthBackendSTSRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Checking if STS role %q exists in AWS auth backend", path) + + resp, err := client.Logical().Read(path) + if err != nil { + return true, fmt.Errorf("Error checking if STS role %q exists in AWS auth backend: %s", path, err) + } + log.Printf("[DEBUG] Checked if STS role %q exists in AWS auth backend", path) + + return resp != nil, nil +} + +func awsAuthBackendSTSRolePath(backend, account string) string { + return "auth/" + strings.Trim(backend, "/") + "/config/sts/" + strings.Trim(account, "/") +} + +func awsAuthBackendSTSRoleBackendFromPath(path string) (string, error) { + if !awsAuthBackendSTSRoleBackendFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := awsAuthBackendSTSRoleBackendFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} + +func awsAuthBackendSTSRoleAccountIDFromPath(path string) (string, error) { + if !awsAuthBackendSTSRoleAccountIDFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no account ID found") + } + res := awsAuthBackendSTSRoleAccountIDFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for account ID", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_aws_auth_backend_sts_role_test.go b/vault/resource_aws_auth_backend_sts_role_test.go new file mode 100644 index 000000000..0ea618d7d --- /dev/null +++ b/vault/resource_aws_auth_backend_sts_role_test.go @@ -0,0 +1,132 @@ +package vault + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestAccAWSAuthBackendSTSRole_import(t *testing.T) { + backend := acctest.RandomWithPrefix("aws") + accountID := strconv.Itoa(acctest.RandInt()) + arn := acctest.RandomWithPrefix("arn:aws:iam::" + accountID + ":role/test-role") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAccCheckAWSAuthBackendSTSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAuthBackendSTSRoleConfig_basic(backend, accountID, arn), + Check: testAccAWSAuthBackendSTSRoleCheck_attrs(backend, accountID, arn), + }, + { + ResourceName: "vault_aws_auth_backend_sts_role.role", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAuthBackendSTSRole_basic(t *testing.T) { + backend := acctest.RandomWithPrefix("aws") + accountID := strconv.Itoa(acctest.RandInt()) + arn := acctest.RandomWithPrefix("arn:aws:iam::" + accountID + ":role/test-role") + updatedArn := acctest.RandomWithPrefix("arn:aws:iam::" + accountID + ":role/test-role") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAccCheckAWSAuthBackendSTSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAuthBackendSTSRoleConfig_basic(backend, accountID, arn), + Check: testAccAWSAuthBackendSTSRoleCheck_attrs(backend, accountID, arn), + }, + { + Config: testAccAWSAuthBackendSTSRoleConfig_basic(backend, accountID, updatedArn), + Check: testAccAWSAuthBackendSTSRoleCheck_attrs(backend, accountID, updatedArn), + }, + }, + }) +} + +func testAccCheckAWSAuthBackendSTSRoleDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_aws_auth_backend_sts_role" { + continue + } + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return fmt.Errorf("Error checking for AWS auth backend STS role %q: %s", rs.Primary.ID, err) + } + if secret != nil { + return fmt.Errorf("AWS auth backend STS role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testAccAWSAuthBackendSTSRoleCheck_attrs(backend, accountID, stsRole string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_aws_auth_backend_sts_role.role"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource has no primary instance state") + } + + endpoint := instanceState.ID + + if endpoint != "auth/"+backend+"/config/sts/"+accountID { + return fmt.Errorf("expected ID to be %q, got %q instead", "auth/"+backend+"/config/sts/"+accountID, endpoint) + } + + client := testProvider.Meta().(*api.Client) + resp, err := client.Logical().Read(endpoint) + if err != nil { + return fmt.Errorf("error reading back sts role from %q: %s", endpoint, err) + } + + if resp == nil { + return fmt.Errorf("%q doesn't exist", endpoint) + } + + attrs := map[string]string{ + "sts_role": "sts_role", + } + for stateAttr, apiAttr := range attrs { + if resp.Data[apiAttr] == nil && instanceState.Attributes[stateAttr] == "" { + continue + } + if resp.Data[apiAttr] != instanceState.Attributes[stateAttr] { + return fmt.Errorf("Expected %s (%s) of %q to be %q, got %q", apiAttr, stateAttr, endpoint, instanceState.Attributes[stateAttr], resp.Data[apiAttr]) + } + } + return nil + } +} + +func testAccAWSAuthBackendSTSRoleConfig_basic(backend, accountID, stsRole string) string { + return fmt.Sprintf(` +resource "vault_auth_backend" "aws" { + type = "aws" + path = "%s" +} + +resource "vault_aws_auth_backend_sts_role" "role" { + backend = "${vault_auth_backend.aws.path}" + account_id = "%s" + sts_role = "%s" +} +`, backend, accountID, stsRole) +} diff --git a/website/docs/r/aws_auth_backend_role.md b/website/docs/r/aws_auth_backend_role.md index 41263062c..eeb08f2a7 100644 --- a/website/docs/r/aws_auth_backend_role.md +++ b/website/docs/r/aws_auth_backend_role.md @@ -22,20 +22,20 @@ resource "vault_auth_backend" "aws" { } resource "vault_aws_auth_backend_role" "example" { - backend = "${vault_auth_backend.aws.path}" - role = "test-role" - auth_type = "iam" - bound_ami_id = "ami-8c1be5f6" - bound_account_id = "123456789012" - bound_vpc_id = "vpc-b61106d4" - bound_subnet_id = "vpc-133128f1" - bound_iam_role_arn = "arn:aws:iam::123456789012:role/MyRole" + backend = "${vault_auth_backend.aws.path}" + role = "test-role" + auth_type = "iam" + bound_ami_id = "ami-8c1be5f6" + bound_account_id = "123456789012" + bound_vpc_id = "vpc-b61106d4" + bound_subnet_id = "vpc-133128f1" + bound_iam_role_arn = "arn:aws:iam::123456789012:role/MyRole" bound_iam_instance_profile_arn = "arn:aws:iam::123456789012:instance-profile/MyProfile" - inferred_entity_type = "ec2_instance" - inferred_aws_region = "us-east-1" - ttl = 60 - max_ttl = 120 - policies = ["default", "dev", "prod"] + inferred_entity_type = "ec2_instance" + inferred_aws_region = "us-east-1" + ttl = 60 + max_ttl = 120 + policies = ["default", "dev", "prod"] } ``` diff --git a/website/docs/r/aws_auth_backend_sts_role.html b/website/docs/r/aws_auth_backend_sts_role.html new file mode 100644 index 000000000..92a2c95a7 --- /dev/null +++ b/website/docs/r/aws_auth_backend_sts_role.html @@ -0,0 +1,51 @@ +--- +layout: "vault" +page_title: "Vault: vault_aws_auth_backend_sts_role resource" +sidebar_current: "docs-vault-aws-auth-backend-sts-role" +description: |- + Configures an STS role in the Vault AWS Auth backend. +--- + +# vault\_aws\_auth\_backend\_sts\_role + +Manages an STS role in a Vault server. STS roles are mappings +between account IDs and STS ARNs. When a login attempt is made +from an EC2 instance in the account ID specified, the associated +STS role will be used to verify the request. For more information, +see the [Vault documentation](https://www.vaultproject.io/docs/auth/aws.html#cross-account-access). + +~> **Important** All data provided in the resource configuration will be + written in cleartext to state and plan files generated by Terraform, and will + appear in the console output when Terraform runs. Protect these artifacts + accordingly. See [the main provider documentation](../../index.html) for more + details. + +## Example Usage + +```hcl +resource "vault_auth_backend" "aws" { + type = "aws" +} + +resource "vault_aws_auth_backend_sts_role" "role" { + backend = "${vault_auth_backend.aws.path}" + account_id = "1234567890" + sts_role = "arn:aws:iam::1234567890:role/my-role" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Optional) The AWS account ID to configure the STS role for. + +* `sts_role` - (Optional) The STS role to assume when verifying requests made + by EC2 instances in the account specified by `account_id`. + +* `backend` - (Optional) The path the AWS auth backend being configured was + mounted at. Defaults to `aws`. + +## Attributes Reference + +No additional attributes are exported by this resource. diff --git a/website/vault.erb b/website/vault.erb index 35eaf2f43..9629d6232 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -28,6 +28,10 @@ vault_auth_backend + > + vault_aws_auth_backend_sts_role + + > vault_generic_secret