diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6ec52d..84ff9c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,12 @@ ### Modules +* `iam-user-policy`: Module for IAM user and corresponding policy + ### Examples +* `vault-iam`: New example which creates a admin user for vault with policy suitable for it. +* `vault-s3-private`: New example showing vault and IAM integration with restricted access. # v0.8.2 diff --git a/examples/vault-iam/Makefile b/examples/vault-iam/Makefile new file mode 100644 index 00000000..6a60f4c8 --- /dev/null +++ b/examples/vault-iam/Makefile @@ -0,0 +1,43 @@ +.PHONY: init plan apply destroy clean + +.DEFAULT_GOAL = help + +# Hardcoding value of 3 minutes when we check if the plan file is stale +STALE_PLAN_FILE := `find "tf.out" -mmin -3 | grep -q tf.out` + +## Check if tf.out is stale (Older than 2 minutes) +check-plan-file: + @if ! ${STALE_PLAN_FILE} ; then \ + echo "ERROR: Stale tf.out plan file (older than 3 minutes)!"; \ + exit 1; \ + fi + +## Runs terraform get and terraform init for env +init: + @terraform get + @terraform init + +## use 'terraform plan' to map out updates to apply +plan: + @terraform plan -out=tf.out + +## use 'terraform apply' to apply updates in a 'tf.out' plan file +apply: check-plan-file + @terraform apply tf.out + +## use 'terraform destroy' to remove all resources from AWS +destroy: + @terraform destroy + +## Show help screen. +help: + @echo "Please use \`make <target>' where <target> is one of\n\n" + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")-1); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf "%-30s %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) diff --git a/examples/vault-iam/README.md b/examples/vault-iam/README.md new file mode 100644 index 00000000..bcfdac27 --- /dev/null +++ b/examples/vault-iam/README.md @@ -0,0 +1,10 @@ +# Vault IAM user + +When setting up vault with AWS as it's secret engine, you need to have +AWS secret and access keys for an IAM user with relevant +permission. This example creates IAM user named "vault_user" and +appropriate policy for it. + +In order for this example to create keys, you need to modify +`variables.tf` appropriately. + diff --git a/examples/vault-iam/main.tf b/examples/vault-iam/main.tf new file mode 100644 index 00000000..1ce19d76 --- /dev/null +++ b/examples/vault-iam/main.tf @@ -0,0 +1,43 @@ +resource "aws_iam_access_key" "vaultkey" { + user = module.vault_iam_user_policy.user_name + count = var.key_count +} + +data "aws_caller_identity" "current" {} + +module "vault_iam_user_policy" { + source = "../../modules/iam-user-policy/" + user_name = "vault_user" + environment = "dev" + iam_policy_name = "vault_user_policy" + iam_user_policy = <<EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:AttachUserPolicy", + "iam:CreateAccessKey", + "iam:CreateUser", + "iam:DeleteAccessKey", + "iam:DeleteUser", + "iam:DeleteUserPolicy", + "iam:DetachUserPolicy", + "iam:ListAccessKeys", + "iam:ListAttachedUserPolicies", + "iam:ListGroupsForUser", + "iam:ListUserPolicies", + "iam:PutUserPolicy", + "iam:RemoveUserFromGroup" + ], + "Resource": [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/vtest-*" + ] + } + ] +} +EOF + +} + diff --git a/examples/vault-iam/outputs.tf b/examples/vault-iam/outputs.tf new file mode 100644 index 00000000..66b66efd --- /dev/null +++ b/examples/vault-iam/outputs.tf @@ -0,0 +1,8 @@ +output "secret_key" { + value = [aws_iam_access_key.vaultkey.*.secret] +} + +output "access_id" { + value = [aws_iam_access_key.vaultkey.*.id] +} + diff --git a/examples/vault-iam/variables.tf b/examples/vault-iam/variables.tf new file mode 100644 index 00000000..b3eb7f44 --- /dev/null +++ b/examples/vault-iam/variables.tf @@ -0,0 +1,5 @@ +variable "key_count" { + description = "Set to 1 to optionally create access keys for the created IAM user" + default = "0" +} + diff --git a/examples/vault-s3-private/Makefile b/examples/vault-s3-private/Makefile new file mode 100644 index 00000000..3694c5c4 --- /dev/null +++ b/examples/vault-s3-private/Makefile @@ -0,0 +1,49 @@ +.PHONY: init plan apply destroy clean + +.DEFAULT_GOAL = help + +# Hardcoding value of 3 minutes when we check if the plan file is stale +STALE_PLAN_FILE := `find "tf.out" -mmin -3 | grep -q tf.out` + +## Check if tf.out is stale (Older than 2 minutes) +check-plan-file: + @if ! ${STALE_PLAN_FILE} ; then \ + echo "ERROR: Stale tf.out plan file (older than 3 minutes)!"; \ + exit 1; \ + fi + +## Runs terraform get and terraform init for env +init: + @terraform get + @terraform init + +## use 'terraform plan' to map out updates to apply +plan: + @terraform plan -out=tf.out + +## use 'terraform apply' to apply updates in a 'tf.out' plan file +apply: check-plan-file + @terraform apply tf.out + +## use 'terraform destroy' to remove all resources from AWS +destroy: + @terraform destroy + +## rm -rf all files and state +clean: + @rm -f tf.out + @rm -f terraform.*.backup + @rm -f terraform.tfstate + +## Show help screen. +help: + @echo "Please use \`make <target>' where <target> is one of\n\n" + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")-1); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf "%-30s %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) diff --git a/examples/vault-s3-private/README.md b/examples/vault-s3-private/README.md new file mode 100644 index 00000000..52a40053 --- /dev/null +++ b/examples/vault-s3-private/README.md @@ -0,0 +1,128 @@ +# Example showing Vault and IAM Integration + +This example creates a private s3 bucket resources. It then uses vault +to create keys which only has access to those s3 buckets. The example +code will create an IAM role with access to that bucket and will also +configure vault so that we can dynamically generate credentials for +accessing that bucket. + +## Requirements + +These are the required things for this example: + +* A running vault server. If you just want to experiment with this, + run a development server using: + +``` shellsession +vault server -dev +``` + +* The AWS access and secret keys for an IAM user which the AWS Secret + Backend for Vault will use for issuing new credentials. If you don't + have any, you can create one using [vault-iam + example](../vault-iam). You need to put the access keys in + [variables.tf](./variables.tf) + + +## Environment creation and deployment + +``` shellsession +$ make init +$ make plan +$ make apply +module.vault_aws_backend.vault_aws_secret_backend.aws: Creating... +module.vault_aws_backend.vault_aws_secret_backend.aws: Creation complete after 0s [id=fpco/aws/dev/vault] +aws_iam_role.vault_bucket_role: Creating... +aws_s3_bucket.vault-test-bucket: Creating... +aws_iam_role.vault_bucket_role: Still creating... [10s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [10s elapsed] +aws_iam_role.vault_bucket_role: Still creating... [20s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [20s elapsed] +aws_iam_role.vault_bucket_role: Creation complete after 22s [id=bucket_access_role] +module.vault_aws_backend.vault_aws_secret_backend_role.aws_role: Creating... +module.vault_aws_backend.vault_aws_secret_backend_role.aws_role: Creation complete after 0s [id=fpco/aws/dev/vault/roles/s3_app_user] +aws_s3_bucket.vault-test-bucket: Still creating... [30s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [40s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [50s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m0s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m10s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m20s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m30s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m40s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [1m50s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [2m0s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [2m10s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [2m20s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [2m30s elapsed] +aws_s3_bucket.vault-test-bucket: Still creating... [2m40s elapsed] +aws_s3_bucket.vault-test-bucket: Creation complete after 2m48s [id=vault-fpco-test-bucket] +aws_iam_role_policy.vault_bucket_policy: Creating... +aws_iam_role_policy.vault_bucket_policy: Still creating... [10s elapsed] +aws_iam_role_policy.vault_bucket_policy: Still creating... [20s elapsed] +aws_iam_role_policy.vault_bucket_policy: Creation complete after 24s [id=bucket_access_role:bucket-policy] + +Apply complete! Resources: 5 added, 0 changed, 0 destroyed. + +The state of your infrastructure has been saved to the path +below. This state is required to modify and destroy your +infrastructure, so keep it safe. To inspect the complete state +use the `terraform show` command. + +State path: terraform.tfstate +``` + +## Testing + +Make sure you are already authorized with the vault server. If not, +use `vault login` to do it. And then, you can dynamically create AWS +credentials for accessing the s3 bucket you created: + +``` shellsession +$ vault read fpco/aws/dev/vault/creds/s3_app_user +Key Value +--- ----- +lease_id fpco/aws/prod/vault/creds/s3_app_user/eJcLUNbpTNRFpLoTL9mEW76p +lease_duration 14m59s +lease_renewable false +access_key xxx +secret_key xxx +security_token xxx +``` + +Now let's try to see all the files in our bucket: + +``` shellsession +$ env AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx AWS_SESSION_TOKEN=xxx aws s3 ls s3://s3-vault-demo-dev-bucket +``` + +It gives you no output since there are no files. But the command +works, which confirms us that the generated credentials are working as +expected. + +Now let's try to do something for which you don't have access with the +same credentials: + +``` shellsession +$ env AWS_ACCESS_KEY_ID=xxxx AWS_SECRET_ACCESS_KEY=xxxx AWS_SESSION_TOKEN=xxx aws ec2 describe-instances --region="us-east-2" +An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation. +``` + +That doesn't work, which is expected. Let's try to see if we can +access files of some other buckets which is present: + +``` shellsession +$ env AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx AWS_SESSION_TOKEN=xxx aws s3 ls s3://some-other-existing-bucket +An error occurred (AccessDenied) when calling the ListObjects operation: Access Denied +``` + +## Destruction + +``` shellsession +$ make destroy +$ make clean +``` + +## Notes + +- This example was last tested with `Terraform v0.12.3` +- This example assumes AWS credentials setup with access to the **us-east-2** region. diff --git a/examples/vault-s3-private/main.tf b/examples/vault-s3-private/main.tf new file mode 100644 index 00000000..6330f01b --- /dev/null +++ b/examples/vault-s3-private/main.tf @@ -0,0 +1,77 @@ +data "aws_region" "current" {} + +locals { + name_prefix = "${var.application}-${var.environment}" + region = data.aws_region.current.name +} + +resource "aws_s3_bucket" "vault-test-bucket" { + bucket = "${local.name_prefix}-bucket" + acl = "private" + region = local.region + + tags = { + Environment = var.environment + } +} + +# Here we allow everyone to assume this role. In production systems +# it's best to restrict it's scope so that only some IAM users are +# able to assume this role. +resource "aws_iam_role" "vault_bucket_role" { + name = "${local.name_prefix}-bucket-role" + + assume_role_policy = <<EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "AWS": "*" + }, + "Effect": "Allow", + "Sid": "" + } + ] +} +EOF + + + tags = { + Environment = var.environment + } +} + +resource "aws_iam_role_policy" "vault_bucket_policy" { + name = "${local.name_prefix}-bucket-policy" + role = aws_iam_role.vault_bucket_role.id + policy = <<EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:*", "iam:CreateAccessKey"], + "Resource": ["${aws_s3_bucket.vault-test-bucket.arn}"] + } + ] +} +EOF + +} + +module "vault_aws_backend" { +source = "../../modules/vault-aws-backend/" +vault_address = var.vault_address +vault_token = var.vault_token +secret_backend_path = var.secret_backend_path +default_lease_ttl_seconds = var.default_lease_ttl_seconds +max_lease_ttl_seconds = var.max_lease_ttl_seconds +credential_type = var.credential_type +role_name = var.role_name +role_arn = aws_iam_role.vault_bucket_role.arn +access_key = var.access_key +secret_key = var.secret_key +} + diff --git a/examples/vault-s3-private/variables.tf b/examples/vault-s3-private/variables.tf new file mode 100644 index 00000000..ef11f1e5 --- /dev/null +++ b/examples/vault-s3-private/variables.tf @@ -0,0 +1,62 @@ +variable "application" { + description = "Application name" + default = "s3-vault-demo" + type = string +} + +variable "environment" { + description = "Environment name: dev/qa/prod etc" + default = "dev" + type = string +} + +variable "vault_address" { + description = "URL for the Vault server" + type = string +} + +variable "vault_token" { + description = "Vault token needed for authorization to the server" + type = string +} + +variable "secret_backend_path" { + description = "Unique AWS secret path for mouting" + type = string + default = "fpco/aws/dev/vault" +} + +variable "default_lease_ttl_seconds" { + description = "The default TTL for credentials issued by this backend." + type = string + default = "900" +} + +variable "max_lease_ttl_seconds" { + description = "The maximum TTL that can be requested for credentials issued by this backend." + type = string + default = "900" +} + +variable "credential_type" { + description = "Specifies the type of credential to be used when retrieving credentials from the role." + type = string + default = "assumed_role" +} + +variable "access_key" { + description = "The AWS Access Key ID this backend should use to issue new credentials." + type = string +} + +variable "secret_key" { + description = "The AWS Secret Key this backend should use to issue new credentials." + type = string +} + +variable "role_name" { + description = "The name to identify this role within the backend. Must be unique within the backend." + type = string + default = "s3_app_user" +} + diff --git a/modules/iam-user-policy/README.md b/modules/iam-user-policy/README.md new file mode 100644 index 00000000..21460867 --- /dev/null +++ b/modules/iam-user-policy/README.md @@ -0,0 +1,9 @@ +# IAM user and policy + +This module creates IAM user and policy and associates it with the +created user. + +Further resources: + +* [AWS Policy elements reference](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html) +* [AWS IAM Policy Documents with terraform](https://www.terraform.io/docs/providers/aws/guides/iam-policy-documents.html) diff --git a/modules/iam-user-policy/main.tf b/modules/iam-user-policy/main.tf new file mode 100644 index 00000000..d9e6e05e --- /dev/null +++ b/modules/iam-user-policy/main.tf @@ -0,0 +1,18 @@ +resource "aws_iam_user" "iam_user" { + name = var.user_name + tags = { + "user" = var.user_name + "Environment" = var.environment + } +} + +resource "aws_iam_policy" "iam_policy" { + name = var.iam_policy_name + policy = var.iam_user_policy +} + +resource "aws_iam_user_policy_attachment" "iam_user_policy" { + user = aws_iam_user.iam_user.name + policy_arn = aws_iam_policy.iam_policy.arn +} + diff --git a/modules/iam-user-policy/outputs.tf b/modules/iam-user-policy/outputs.tf new file mode 100644 index 00000000..c16228d3 --- /dev/null +++ b/modules/iam-user-policy/outputs.tf @@ -0,0 +1,4 @@ +output "user_name" { + value = aws_iam_user.iam_user.name +} + diff --git a/modules/iam-user-policy/variables.tf b/modules/iam-user-policy/variables.tf new file mode 100644 index 00000000..0ac4b1d6 --- /dev/null +++ b/modules/iam-user-policy/variables.tf @@ -0,0 +1,21 @@ +variable "user_name" { + description = "Name of the IAM user that needs to be created" + type = string +} + +variable "environment" { + description = "Environment name: dev/qa/prod etc" + default = "dev" + type = string +} + +variable "iam_policy_name" { + description = "Policy name for the IAM user" + type = string +} + +variable "iam_user_policy" { + description = "The policy document. This is a JSON formatted string." + type = string +} + diff --git a/modules/vault-aws-backend/README.md b/modules/vault-aws-backend/README.md new file mode 100644 index 00000000..23b7e165 --- /dev/null +++ b/modules/vault-aws-backend/README.md @@ -0,0 +1,6 @@ +# AWS Secret Backend for Vault + +This module creates an AWS secret backend and a role for the +backend. Roles are used to map credentials to the policies that +generated them. For an example of it's use, [see +vault-s3-private](../../examples/vault-s3-private). diff --git a/modules/vault-aws-backend/main.tf b/modules/vault-aws-backend/main.tf new file mode 100644 index 00000000..02ba3d09 --- /dev/null +++ b/modules/vault-aws-backend/main.tf @@ -0,0 +1,25 @@ +provider "aws" { + region = "${var.region}" +} + +provider "vault" { + address = "${var.vault_address}" + token = "${var.vault_token}" +} + +resource "vault_aws_secret_backend" "aws" { + region = "${var.region}" + access_key = "${var.access_key}" + secret_key = "${var.secret_key}" + path = "${var.secret_backend_path}" + default_lease_ttl_seconds = "${var.default_lease_ttl_seconds}" + max_lease_ttl_seconds = "${var.max_lease_ttl_seconds}" +} + +resource "vault_aws_secret_backend_role" "aws_role" { + backend = "${vault_aws_secret_backend.aws.path}" + name = "${var.role_name}" + credential_type = "${var.credential_type}" + role_arns = ["${var.role_arn}"] +} + diff --git a/modules/vault-aws-backend/variables.tf b/modules/vault-aws-backend/variables.tf new file mode 100644 index 00000000..0012a7da --- /dev/null +++ b/modules/vault-aws-backend/variables.tf @@ -0,0 +1,55 @@ +variable "vault_address" { + description = "URL for the Vault server" + type = "string" +} + +variable "vault_token" { + description = "Vault token needed for authorization to the server" + type = "string" +} + +variable "region" { + description = "AWS Region" + type = "string" + default = "us-east-2" +} + +variable "secret_backend_path" { + description = "Unique AWS secret path for mouting" + type = "string" +} + +variable "default_lease_ttl_seconds" { + description = "The default TTL for credentials issued by this backend." + type = "string" +} + +variable "max_lease_ttl_seconds" { + description = "The maximum TTL that can be requested for credentials issued by this backend." + type = "string" +} + +variable "credential_type" { + description = "Specifies the type of credential to be used when retrieving credentials from the role." + type = "string" +} + +variable "access_key" { + description = "The AWS Access Key ID this backend should use to issue new credentials." + type = "string" +} + +variable "secret_key" { + description = "The AWS Secret Key this backend should use to issue new credentials." + type = "string" +} + +variable "role_name" { + description = "The name to identify this role within the backend. Must be unique within the backend." + type = "string" +} + +variable "role_arn" { + description = "Specifies the ARN of the AWS role this Vault role is allowed to assume" + type = "string" +} diff --git a/tests/main.tf b/tests/main.tf index e16f33e4..75834cd1 100644 --- a/tests/main.tf +++ b/tests/main.tf @@ -788,3 +788,11 @@ module "web-alb" { subnet_ids = [] vpc_id = "" } + +module "iam-user-policy" { + source = "../modules/iam-user-policy/" + user_name = "" + environment = "" + iam_policy_name = "" + iam_user_policy = "" +}