diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc36be4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: + push: + branches: [master] + pull_request: + +jobs: + typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@v1.30.2 + + terraform-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: ^1 + terraform_wrapper: false + + - run: terraform fmt -check + + terraform-validate: + runs-on: ubuntu-latest + + strategy: + matrix: + project: + - asset-account/terraform/stack-set/examples/self-managed + - asset-account/terraform/stack-set/examples/service-managed + + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: ^1 + terraform_wrapper: false + + - run: terraform init -input=false + working-directory: ${{ matrix.project }} + + - run: terraform validate + working-directory: ${{ matrix.project }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4c18be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.terraform +.terraform.tfstate.lock.info +terraform.tfstate +terraform.tfstate.backup +terraform.tfvars diff --git a/asset-account/README.md b/asset-account/README.md new file mode 100644 index 0000000..9af3f54 --- /dev/null +++ b/asset-account/README.md @@ -0,0 +1,11 @@ +# Elastio Asset Account Stack + +Elastio Asset Account stack creates IAM roles to link the AWS account with the Elastio Connector. This allows the Elastio Connector to scan the assets available in the account where the Elastio Asset Account stack is deployed. + +There are several ways to deploy the Elastio Asset Account stack, that we'll review below. + +## AWS CloudFormation StackSet + +You can generate the CloudFormation template link for your Elastio Asset Account using the Elastio Portal UI and then deploy it via a CloudFormation StackSet either manually, or using the Elastio official Terraform wrapper module for this. + +See the [`terraform/stack-set`](./terraform/stack-set) to get started. diff --git a/asset-account/terraform/stack-set/README.md b/asset-account/terraform/stack-set/README.md new file mode 100644 index 0000000..d1a38ae --- /dev/null +++ b/asset-account/terraform/stack-set/README.md @@ -0,0 +1,9 @@ +# Elastio Asset Account CloudFormation StackSet + +See [this README](../..) for more details on what this stack does. + +This is a Terraform module, that is a thin wrapper on top of an [`aws_cloudformation_stack_set`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set) and [`aws_cloudformation_stack_instances`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_instances) resources used to deploy the Elastio Asset Account stack. + +See the `examples` directory for some examples of how this module can be used: +- `self-managed` - deploy the stack set using the [self-managed permission model](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-getting-started-create-self-managed.html) +- `service-managed` - deploy the stack set using the [service-managed permission model](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-orgs-associate-stackset-with-org.html) diff --git a/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl b/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl new file mode 100644 index 0000000..7de3aa8 --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.92.0" + constraints = ">= 5.0.0" + hashes = [ + "h1:ZnpTxMfg5PThZc5WZCsZELinsR0gPhdTpNmXjVcf7aE=", + "zh:1d3a0b40831360e8e988aee74a9ff3d69d95cb541c2eae5cb843c64303a091ba", + "zh:3d29cbced6c708be2041a708d25c7c0fc22d09e4d0b174360ed113bfae786137", + "zh:4341a203cf5820a0ca18bb514ae10a6c113bc6a728fb432acbf817d232e8eff4", + "zh:4a49e2d91e4d92b6b93ccbcbdcfa2d67935ce62e33b939656766bb81b3fd9a2c", + "zh:54c7189358b37fd895dedbabf84e509c1980a8c404a1ee5b29b06e40497b8655", + "zh:5d8bb1ff089c37cb65c83b4647f1981fded993e87d8132915d92d79f29e2fcd8", + "zh:618f2eb87cd65b245aefba03991ad714a51ff3b841016ef68e2da2b85d0b2325", + "zh:7bce07bc542d0588ca42bac5098dd4f8af715417cd30166b4fb97cedd44ab109", + "zh:81419eab2d8810beb114b1ff5cbb592d21edc21b809dc12bb066e4b88fdd184a", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9dea39d4748eeeebe2e76ca59bca4ccd161c2687050878c47289a98407a23372", + "zh:d692fc33b67ac89e916c8f9233d39eacab8c438fe10172990ee9d94fba5ca372", + "zh:d9075c7da48947c029ba47d5985e1e8e3bf92367bfee8ca1ff0e747765e779a1", + "zh:e81c62db317f3b640b2e04eba0ada8aa606bcbae0152c09f6242e86b86ef5889", + "zh:f68562e073722c378d2f3529eb80ad463f12c44aa5523d558ae3b69f4de5ca1f", + ] +} diff --git a/asset-account/terraform/stack-set/examples/self-managed/README.md b/asset-account/terraform/stack-set/examples/self-managed/README.md new file mode 100644 index 0000000..0d7cb3a --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/README.md @@ -0,0 +1,7 @@ +# Self-Managed StackSet Example + +This is a basic example of using the `elastio-asset-account-stack-set` terraform module with the self-managed AWS Cloudformation StackSet. + +You can deploy it even within a single account. Just specify the `template_url` input variable at minimum. + +You can specify the `admin_account_aws_profile` and `asset_account_aws_profile` to use separate Admin and Asset accounts. If you don't specify them, then the default AWS account configured in your environment will be used as both the Admin and the Asset account. diff --git a/asset-account/terraform/stack-set/examples/self-managed/admin.tf b/asset-account/terraform/stack-set/examples/self-managed/admin.tf new file mode 100644 index 0000000..c9c821e --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/admin.tf @@ -0,0 +1,71 @@ +module "elastio_asset_accounts" { + # Use the link from the real terraform registry here. Relative path is used for testing purposes. + source = "../../" + providers = { + aws = aws.admin + } + + depends_on = [ + # Needs to wait for the execution role in the asset account to be fully created + aws_iam_role_policy.execution_deployment, + + # Needs to wait for the admin role in the admin account to be fully created + aws_iam_role_policy.admin_execution, + ] + + template_url = var.template_url + + # We are deploying just into a single asset account in this example + accounts = [local.asset_account_id] + + administration_role_arn = aws_iam_role.admin.arn +} + +# Admin role, that StackSets will use to access the asset accounts to deploy the stacks +resource "aws_iam_role" "admin" { + provider = aws.admin + + assume_role_policy = data.aws_iam_policy_document.admin_trust.json + name = "AWSCloudFormationStackSetAdministrationRole" +} + +data "aws_iam_policy_document" "admin_trust" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + identifiers = ["cloudformation.amazonaws.com"] + type = "Service" + } + + # Conditions to prevent the confused deputy attack + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.admin_account_id] + } + + condition { + test = "StringLike" + variable = "aws:SourceArn" + values = ["arn:aws:cloudformation:*:${local.admin_account_id}:stackset/*"] + } + } +} + +data "aws_iam_policy_document" "admin_execution" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + resources = ["arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole"] + } +} + +resource "aws_iam_role_policy" "admin_execution" { + provider = aws.admin + + name = "AssumeExecutionRole" + policy = data.aws_iam_policy_document.admin_execution.json + role = aws_iam_role.admin.name +} diff --git a/asset-account/terraform/stack-set/examples/self-managed/asset.tf b/asset-account/terraform/stack-set/examples/self-managed/asset.tf new file mode 100644 index 0000000..baff615 --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/asset.tf @@ -0,0 +1,35 @@ +resource "aws_iam_role" "execution" { + provider = aws.asset + + name = "AWSCloudFormationStackSetExecutionRole" + assume_role_policy = data.aws_iam_policy_document.execution_trust.json +} + +data "aws_iam_policy_document" "execution_trust" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + identifiers = [aws_iam_role.admin.arn] + type = "AWS" + } + } +} + +# Specifies the set of permissions required for the deployment of the Cloudfomation stack +data "aws_iam_policy_document" "execution_deployment" { + statement { + actions = ["*"] + effect = "Allow" + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "execution_deployment" { + provider = aws.asset + + name = "Deployment" + policy = data.aws_iam_policy_document.execution_deployment.json + role = aws_iam_role.execution.name +} diff --git a/asset-account/terraform/stack-set/examples/self-managed/providers.tf b/asset-account/terraform/stack-set/examples/self-managed/providers.tf new file mode 100644 index 0000000..10751be --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/providers.tf @@ -0,0 +1,22 @@ +provider "aws" { + alias = "admin" + profile = var.admin_account_aws_profile +} + +provider "aws" { + alias = "asset" + profile = var.asset_account_aws_profile +} + +data "aws_caller_identity" "admin" { + provider = aws.admin +} + +data "aws_caller_identity" "asset" { + provider = aws.asset +} + +locals { + admin_account_id = data.aws_caller_identity.admin.account_id + asset_account_id = data.aws_caller_identity.asset.account_id +} diff --git a/asset-account/terraform/stack-set/examples/self-managed/variables.tf b/asset-account/terraform/stack-set/examples/self-managed/variables.tf new file mode 100644 index 0000000..e355baf --- /dev/null +++ b/asset-account/terraform/stack-set/examples/self-managed/variables.tf @@ -0,0 +1,23 @@ +variable "template_url" { + description = <<-DESCR + The URL of the Elastio Asset Account CloudFormation template obtained from + the Elastio Portal. + + This parameter is sensitive, because anyone who knows this URL can deploy + Elastio Account stack and linking it to your Elastio tenant. + DESCR + + sensitive = true + type = string + nullable = false +} + +variable "admin_account_aws_profile" { + type = string + default = null +} + +variable "asset_account_aws_profile" { + type = string + default = null +} diff --git a/asset-account/terraform/stack-set/examples/service-managed/.terraform.lock.hcl b/asset-account/terraform/stack-set/examples/service-managed/.terraform.lock.hcl new file mode 100644 index 0000000..7de3aa8 --- /dev/null +++ b/asset-account/terraform/stack-set/examples/service-managed/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.92.0" + constraints = ">= 5.0.0" + hashes = [ + "h1:ZnpTxMfg5PThZc5WZCsZELinsR0gPhdTpNmXjVcf7aE=", + "zh:1d3a0b40831360e8e988aee74a9ff3d69d95cb541c2eae5cb843c64303a091ba", + "zh:3d29cbced6c708be2041a708d25c7c0fc22d09e4d0b174360ed113bfae786137", + "zh:4341a203cf5820a0ca18bb514ae10a6c113bc6a728fb432acbf817d232e8eff4", + "zh:4a49e2d91e4d92b6b93ccbcbdcfa2d67935ce62e33b939656766bb81b3fd9a2c", + "zh:54c7189358b37fd895dedbabf84e509c1980a8c404a1ee5b29b06e40497b8655", + "zh:5d8bb1ff089c37cb65c83b4647f1981fded993e87d8132915d92d79f29e2fcd8", + "zh:618f2eb87cd65b245aefba03991ad714a51ff3b841016ef68e2da2b85d0b2325", + "zh:7bce07bc542d0588ca42bac5098dd4f8af715417cd30166b4fb97cedd44ab109", + "zh:81419eab2d8810beb114b1ff5cbb592d21edc21b809dc12bb066e4b88fdd184a", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9dea39d4748eeeebe2e76ca59bca4ccd161c2687050878c47289a98407a23372", + "zh:d692fc33b67ac89e916c8f9233d39eacab8c438fe10172990ee9d94fba5ca372", + "zh:d9075c7da48947c029ba47d5985e1e8e3bf92367bfee8ca1ff0e747765e779a1", + "zh:e81c62db317f3b640b2e04eba0ada8aa606bcbae0152c09f6242e86b86ef5889", + "zh:f68562e073722c378d2f3529eb80ad463f12c44aa5523d558ae3b69f4de5ca1f", + ] +} diff --git a/asset-account/terraform/stack-set/examples/service-managed/README.md b/asset-account/terraform/stack-set/examples/service-managed/README.md new file mode 100644 index 0000000..4af9f91 --- /dev/null +++ b/asset-account/terraform/stack-set/examples/service-managed/README.md @@ -0,0 +1,9 @@ +# Service-Managed StackSet Example + +This is a basic example of using the `elastio-asset-account-stack-set` terraform module with the service-managed AWS Cloudformation StackSet. + +You'll need to deploy it from the AWS Management account. You'll also need to specify both the input variables: `accounts` and `organizational_unit_ids`. + +AWS API requires at least one org unit ID that contains the provided accounts. It doesn't mean you'll deploy the StackSet into the entire org unit, it's just a quirk of the AWS API. The Stack set instances will still be deployed into the accounts specified in `accounts`. + +If you want to deploy into the entire org unit, then modify the `deployment_targets` as needed for your use case. diff --git a/asset-account/terraform/stack-set/examples/service-managed/main.tf b/asset-account/terraform/stack-set/examples/service-managed/main.tf new file mode 100644 index 0000000..9d85a5e --- /dev/null +++ b/asset-account/terraform/stack-set/examples/service-managed/main.tf @@ -0,0 +1,16 @@ +module "elastio_asset_accounts" { + # Use the link from the real terraform registry here. Relative path is used for testing purposes. + source = "../../" + + template_url = var.template_url + + permission_model = "SERVICE_MANAGED" + deployment_targets = { + account_filter_type = "INTERSECTION" + accounts = var.accounts + organizational_unit_ids = var.organizational_unit_ids + } + auto_deployment = { + enabled = false + } +} diff --git a/asset-account/terraform/stack-set/examples/service-managed/variables.tf b/asset-account/terraform/stack-set/examples/service-managed/variables.tf new file mode 100644 index 0000000..7f01719 --- /dev/null +++ b/asset-account/terraform/stack-set/examples/service-managed/variables.tf @@ -0,0 +1,30 @@ +variable "template_url" { + description = <<-DESCR + The URL of the Elastio Asset Account CloudFormation template obtained from + the Elastio Portal. + + This parameter is sensitive, because anyone who knows this URL can deploy + Elastio Account stack and linking it to your Elastio tenant. + DESCR + + sensitive = true + type = string + nullable = false +} + +variable "accounts" { + type = list(string) + + description = <<-DESCR + List of AWS account IDs where the Elastio Asset Account stack instances will + be deployed. + DESCR +} + +variable "organizational_unit_ids" { + type = list(string) + + description = <<-DESCR + Organization root ID or organizational unit (OU) IDs to which stack sets deploy. + DESCR +} diff --git a/asset-account/terraform/stack-set/main.tf b/asset-account/terraform/stack-set/main.tf new file mode 100644 index 0000000..91fe644 --- /dev/null +++ b/asset-account/terraform/stack-set/main.tf @@ -0,0 +1,83 @@ +resource "aws_cloudformation_stack_set" "this" { + tags = merge(var.tags, { "elastio:resource" = true }) + + name = var.stack_set_name + description = var.stack_set_description + administration_role_arn = var.administration_role_arn + execution_role_name = var.execution_role_name + permission_model = var.permission_model + call_as = var.call_as + template_url = var.template_url + + capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] + + dynamic "auto_deployment" { + for_each = var.auto_deployment[*] + content { + enabled = auto_deployment.value.enabled + retain_stacks_on_account_removal = auto_deployment.value.retain_stacks_on_account_removal + } + } + + dynamic "managed_execution" { + for_each = var.managed_execution[*] + content { + active = managed_execution.value.active + } + } + + dynamic "operation_preferences" { + for_each = var.operation_preferences[*] + content { + failure_tolerance_count = operation_preferences.value.failure_tolerance_count + failure_tolerance_percentage = operation_preferences.value.failure_tolerance_percentage + max_concurrent_count = operation_preferences.value.max_concurrent_count + max_concurrent_percentage = operation_preferences.value.max_concurrent_percentage + } + } + + parameters = { + for key, value in { + iamResourceNamesPrefix = var.iam_resource_names_prefix + iamResourceNamesSuffix = var.iam_resource_names_suffix + encryptWithCmk = var.encrypt_with_cmk + lambdaTracing = var.lambda_tracing + } : + key => tostring(value) + } + + # Ignore some internal parameter values + lifecycle { + ignore_changes = [ + parameters["cloudConnectorAccountId"], + parameters["cloudConnectorRoleExternalId"], + parameters["deploymentNotificationToken"], + parameters["deploymentNotificationTopicArn"], + ] + } +} + +resource "aws_cloudformation_stack_instances" "this" { + stack_set_name = aws_cloudformation_stack_set.this.name + + accounts = var.accounts + + dynamic "deployment_targets" { + for_each = var.deployment_targets[*] + content { + account_filter_type = deployment_targets.value.account_filter_type + accounts = deployment_targets.value.accounts + accounts_url = deployment_targets.value.accounts_url + organizational_unit_ids = deployment_targets.value.organizational_unit_ids + } + } + + dynamic "operation_preferences" { + for_each = var.operation_preferences[*] + content { + concurrency_mode = operation_preferences.concurrency_mode + } + } + + retain_stacks = var.retain_stacks +} diff --git a/asset-account/terraform/stack-set/outputs.tf b/asset-account/terraform/stack-set/outputs.tf new file mode 100644 index 0000000..7617831 --- /dev/null +++ b/asset-account/terraform/stack-set/outputs.tf @@ -0,0 +1,14 @@ + +output "stack_set" { + value = aws_cloudformation_stack_set.this + description = <