diff --git a/.gitignore b/.gitignore index 43d7d26..ef7c6df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .terraform *.tfstate* *.tfvars* -.terraform.lock.hcl \ No newline at end of file +.terraform.lock.hcl +.DS_Store diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100755 index 0000000..c4bbe48 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,12 @@ +{ + "MD002": false, + "MD013": false, + "MD033": { + "allowed_elements": [ + "br", + "a" + ] + }, + "MD034": false, + "MD041": false +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..58df6da --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +--- +# yamllint disable rule:line-length +default_language_version: + python: python3.8 +repos: + - repo: git://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-json + - id: check-merge-conflict + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: pretty-format-json + args: + - --autofix + - id: detect-aws-credentials + args: + - --allow-missing-credentials + - id: detect-private-key + - repo: git://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.10 + hooks: + - id: forbid-tabs + exclude_types: [python, javascript, dtd, markdown, makefile, xml] + exclude: binary|\.bin$ + - repo: git://github.com/jameswoolfenden/pre-commit-shell + rev: 0.0.2 + hooks: + - id: shell-lint + exclude: template|\.template$ + - repo: git://github.com/igorshubovych/markdownlint-cli + rev: v0.27.1 + hooks: + - id: markdownlint + - repo: git://github.com/adrienverge/yamllint + rev: v1.26.1 + hooks: + - id: yamllint + name: yamllint + description: This hook runs yamllint. + entry: yamllint + language: python + types: [file, yaml] + - repo: git://github.com/jameswoolfenden/pre-commit + rev: v0.1.46 + hooks: + - id: terraform-fmt + language_version: python3.8 + - id: tf2docs + language_version: python3.8 + - repo: git://github.com/bridgecrewio/checkov + rev: 2.0.181 + hooks: + - id: checkov + verbose: true + entry: checkov -d example/examplea diff --git a/LICENSE b/LICENSE index 57bc88a..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index 98ba146..0f5d07b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Terraform AWS Session Manager -A Terraform module to setup [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html). +A Terraform module to setup [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html). -This module creates the a SSM document to support encrypted session manager communication and logs. It also creates a KMS key, S3 bucket, and CloudWatch Log group to store logs. In addition, for EC2 instances without a public IP address it can create VPC endpoints to enable private session manager communication. However, the VPC endpoint creation can also be facilitated by other modules such as [this](https://github.com/terraform-aws-modules/terraform-aws-vpc). Be aware of the [AWS PrivateLink pricing](https://aws.amazon.com/privatelink/pricing/) before deployment. +This module creates the a SSM document to support encrypted session manager communication and logs. It also creates a KMS key, S3 bucket, and CloudWatch Log group to store logs. In addition, for EC2 instances without a public IP address it can create VPC endpoints to enable private session manager communication. However, the VPC endpoint creation can also be facilitated by other modules such as [this](https://github.com/terraform-aws-modules/terraform-aws-vpc). Be aware of the [AWS PrivateLink pricing](https://aws.amazon.com/privatelink/pricing/) before deployment. ## Usage @@ -41,46 +41,96 @@ module "ssm" { This module does not create any IAM policies for access to session manager. To do that, look at example policies in the [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/getting-started-restrict-access-quickstart.html) - + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.12 | +| [aws](#requirement\_aws) | >= 1.36.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 1.36.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.session_manager_log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_iam_instance_profile.ssm_profile](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.ssm_s3_cwl_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.ssm_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.SSM-role-policy-attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.SSM-s3-cwl-policy-attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_kms_alias.ssmkey](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | +| [aws_kms_key.ssmkey](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_s3_bucket.access_log_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket.session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_public_access_block.access_log_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_public_access_block.session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_security_group.ssm_sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_ssm_document.session_manager_prefs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_document) | resource | +| [aws_vpc_endpoint.ec2messages](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.ssmmessages](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint_route_table_association.private_s3_route](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_route_table_association) | resource | +| [aws_vpc_endpoint_route_table_association.private_s3_subnet_route](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_route_table_association) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy.AmazonSSMManagedInstanceCore](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | +| [aws_iam_policy_document.kms_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_s3_cwl_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_route_table.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route_table) | data source | +| [aws_subnet_ids.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet_ids) | data source | +| [aws_vpc.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source | ## Inputs -Below is a list of this modules input values: -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:-----:| -| bucket\_name | Name of S3 bucket to store session logs | `string` | | yes | -| log\_archive\_days | Number of days to wait before archiving to Glacier | `number` | `30` | no | -| log\_expire\_days | Number of days to wait before deleting session logs | `number` | `365` | no | -| access\_log\_bucket\_name | Name of the S3 bucket to store bucket access logs | `string` | | yes | -| access\_log\_expire\_days | Number of days to wait before deleting access logs | `number` | `30` | no | -| kms\_key\_deletion\_window | Waiting period for scheduled KMS Key deletion. Can be 7-30 days | `number` | `7` | no | -| kms\_key\_alias | Alias of the KMS key. Must start with alias/ followed by a name | `string` | `alias/ssm-key` | no | -| cloudwatch\_logs\_retention | Number of days to retain Session Logs in CloudWatch | `number` | `30` | no | -| cloudwatch\_log\_group\_name | Name of the CloudWatch Log Group for storing SSM Session Logs | `string` | `/ssm/session-logs` | no | -| tags | A map of tags to add to all resources | `map(string)` | `{}` | no | -| vpc\_id | VPC ID to deploy endpoints to | `string` | `null` | no | -| enable\_log\_to\_s3 | Enable Session Manager to Log to S3 | `bool` | `true` | no | -| enable\_log\_to\_cloudwatch | Enable Session Manager to Log to CloudWatch Logs | `bool` | `true` | no | -| vpc\_endpoints\_enabled | Create VPC Endpoints | `bool` | `false` | no | - +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_log\_bucket\_name](#input\_access\_log\_bucket\_name) | Name of S3 bucket to store access logs from session logs bucket | `string` | n/a | yes | +| [access\_log\_expire\_days](#input\_access\_log\_expire\_days) | Number of days to wait before deleting access logs | `number` | `30` | no | +| [bucket\_name](#input\_bucket\_name) | Name of S3 bucket to store session logs | `string` | n/a | yes | +| [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Name of the CloudWatch Log Group for storing SSM Session Logs | `string` | `"/ssm/session-logs"` | no | +| [cloudwatch\_logs\_retention](#input\_cloudwatch\_logs\_retention) | Number of days to retain Session Logs in CloudWatch | `number` | `30` | no | +| [enable\_log\_to\_cloudwatch](#input\_enable\_log\_to\_cloudwatch) | Enable Session Manager to Log to CloudWatch Logs | `bool` | `true` | no | +| [enable\_log\_to\_s3](#input\_enable\_log\_to\_s3) | Enable Session Manager to Log to S3 | `bool` | `true` | no | +| [kms\_key\_alias](#input\_kms\_key\_alias) | Alias of the KMS key. Must start with alias/ followed by a name | `string` | `"alias/ssm-key"` | no | +| [kms\_key\_deletion\_window](#input\_kms\_key\_deletion\_window) | Waiting period for scheduled KMS Key deletion. Can be 7-30 days. | `number` | `7` | no | +| [log\_archive\_days](#input\_log\_archive\_days) | Number of days to wait before archiving to Glacier | `number` | `30` | no | +| [log\_expire\_days](#input\_log\_expire\_days) | Number of days to wait before deleting | `number` | `365` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [vpc\_endpoints\_enabled](#input\_vpc\_endpoints\_enabled) | Create VPC Endpoints | `bool` | `false` | no | +| [vpc\_id](#input\_vpc\_id) | VPC ID to deploy endpoints into | `string` | `null` | no | ## Outputs -| Name | Example Value | Description | -|------|----------------|-------------| -| session_logs_bucket_name | my-session-logs | S3 bucket for session logs | -| access_log_bucket_name | my-session-access-logs | S3 bucket for S3 access logs | -| cloudwatch_log_group_arn | arn:aws:logs:us-west-2:123456789012:log-group:/ssm/session-logs:* | CloudWatch Log group for session logs | -| kms_key_arn | arn:aws:kms:us-west-2:123456789012:key/2320fbba-d4e5-420d-82d3-1a4d6b8605e8 | KMS Key Arn for Encrypting logs and session | -| iam_role_arn | arn:aws:iam::123456789012:role/ssm_role | IAM Role for EC2 instances | -| iam_profile_name | ssm_profile | EC2 instance profile for SSM | -| ssm_security_group | ["sg-05e4f4cf12db5a191"] | Security Group used to access VPC Endpoints | -| vpc_endpoint_ssm | ["vpce-0cefc23e81d365733"] | VPC Endpoint for SSM | -| vpc_endpoint_ec2messages | ["vpce-0f507468fb9b06b8b"] | VPC Endpoint for EC2 Messages | -| vpc_endpoint_ssmmessages | ["vpce-0fe2cb670d40ec053"] | VPC Endpoint for SSM Messages | -| vpc_endpoint_s3 | ["vpce-0a8ebde94fa301a4a"] | VPC Endpoint for S3 | -| vpc_endpoint_logs | ["vpce-08c90d8df9ef37f90"] | VPC Endpoint for CloudWatch Logs | -| vpc_endpoint_kms | ["vpce-07ddc11beac1d4a3f"] | VPC Endpoint for KMS | +| Name | Description | +|------|-------------| +| [access\_log\_bucket\_name](#output\_access\_log\_bucket\_name) | n/a | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | n/a | +| [iam\_profile\_name](#output\_iam\_profile\_name) | n/a | +| [iam\_role\_arn](#output\_iam\_role\_arn) | n/a | +| [kms\_key\_arn](#output\_kms\_key\_arn) | n/a | +| [session\_logs\_bucket\_name](#output\_session\_logs\_bucket\_name) | n/a | +| [ssm\_security\_group](#output\_ssm\_security\_group) | n/a | +| [vpc\_endpoint\_ec2messages](#output\_vpc\_endpoint\_ec2messages) | n/a | +| [vpc\_endpoint\_kms](#output\_vpc\_endpoint\_kms) | n/a | +| [vpc\_endpoint\_logs](#output\_vpc\_endpoint\_logs) | n/a | +| [vpc\_endpoint\_s3](#output\_vpc\_endpoint\_s3) | n/a | +| [vpc\_endpoint\_ssm](#output\_vpc\_endpoint\_ssm) | n/a | +| [vpc\_endpoint\_ssmmessages](#output\_vpc\_endpoint\_ssmmessages) | n/a | + ## SSM Usage Example diff --git a/aws_s3_bucket.access_log_bucket.tf b/aws_s3_bucket.access_log_bucket.tf new file mode 100644 index 0000000..5c9ebfe --- /dev/null +++ b/aws_s3_bucket.access_log_bucket.tf @@ -0,0 +1,42 @@ + +resource "aws_s3_bucket" "access_log_bucket" { + # checkov:skip=CKV_AWS_144: Cross region replication is overkill + # checkov:skip=CKV_AWS_18: + # checkov:skip=CKV_AWS_52: + bucket = var.access_log_bucket_name + acl = "log-delivery-write" + force_destroy = true + + tags = var.tags + + versioning { + enabled = true + } + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.ssmkey.arn + sse_algorithm = "aws:kms" + } + } + } + + lifecycle_rule { + id = "delete_after_X_days" + enabled = true + + expiration { + days = var.access_log_expire_days + } + } +} + + +resource "aws_s3_bucket_public_access_block" "access_log_bucket" { + bucket = aws_s3_bucket.access_log_bucket.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/aws_s3_bucket.session_logs_bucket.tf b/aws_s3_bucket.session_logs_bucket.tf new file mode 100644 index 0000000..28adf6a --- /dev/null +++ b/aws_s3_bucket.session_logs_bucket.tf @@ -0,0 +1,49 @@ +resource "aws_s3_bucket" "session_logs_bucket" { + # checkov:skip=CKV_AWS_144: Cross region replication overkill + # checkov:skip=CKV_AWS_52: + bucket = var.bucket_name + acl = "private" + force_destroy = true + tags = var.tags + + versioning { + enabled = true + } + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.ssmkey.arn + sse_algorithm = "aws:kms" + } + } + } + + lifecycle_rule { + id = "archive_after_X_days" + enabled = true + + transition { + days = var.log_archive_days + storage_class = "GLACIER" + } + + expiration { + days = var.log_expire_days + } + } + + logging { + target_bucket = aws_s3_bucket.access_log_bucket.id + target_prefix = "log/" + } + +} + +resource "aws_s3_bucket_public_access_block" "session_logs_bucket" { + bucket = aws_s3_bucket.session_logs_bucket.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/aws_security_group.ssm_sg.tf b/aws_security_group.ssm_sg.tf new file mode 100644 index 0000000..b4919ab --- /dev/null +++ b/aws_security_group.ssm_sg.tf @@ -0,0 +1,25 @@ + +# Create VPC Endpoints For Session Manager +resource "aws_security_group" "ssm_sg" { + count = var.vpc_endpoints_enabled ? 1 : 0 + name = "ssm-sg" + description = "Allow TLS inbound To AWS Systems Manager Session Manager" + vpc_id = var.vpc_id + + ingress { + description = "HTTPS from VPC" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [data.aws_vpc.selected[0].cidr_block] + } + + egress { + description = "Allow All Egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + tags = var.tags +} diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..3d32652 --- /dev/null +++ b/data.tf @@ -0,0 +1,4 @@ +data "aws_vpc" "selected" { + count = var.vpc_endpoints_enabled ? 1 : 0 + id = var.vpc_id +} diff --git a/example/examplea/module.ssm.tf b/example/examplea/module.ssm.tf new file mode 100644 index 0000000..f0cce4b --- /dev/null +++ b/example/examplea/module.ssm.tf @@ -0,0 +1,7 @@ +module "ssm" { + source = "../../" + bucket_name = "my-session-logs" + access_log_bucket_name = "my-session-access-logs" + enable_log_to_s3 = true + enable_log_to_cloudwatch = true +} diff --git a/example/examplea/outputs.tf b/example/examplea/outputs.tf new file mode 100644 index 0000000..42904be --- /dev/null +++ b/example/examplea/outputs.tf @@ -0,0 +1,51 @@ +output "session_logs_bucket_name" { + value = module.ssm.session_logs_bucket_name +} + +output "access_log_bucket_name" { + value = module.ssm.access_log_bucket_name +} + +output "cloudwatch_log_group_arn" { + value = module.ssm.cloudwatch_log_group_arn +} + +output "kms_key_arn" { + value = module.ssm.kms_key_arn +} + +output "iam_role_arn" { + value = module.ssm.iam_role_arn +} + +output "iam_profile_name" { + value = module.ssm.iam_profile_name +} + +output "ssm_security_group" { + value = module.ssm.ssm_security_group +} + +output "vpc_endpoint_ssm" { + value = module.ssm.vpc_endpoint_ssm +} + +output "vpc_endpoint_ec2messages" { + value = module.ssm.vpc_endpoint_ec2messages +} + +output "vpc_endpoint_ssmmessages" { + value = module.ssm.vpc_endpoint_ssmmessages +} + +output "vpc_endpoint_s3" { + value = module.ssm.vpc_endpoint_s3 +} + +output "vpc_endpoint_logs" { + value = module.ssm.vpc_endpoint_logs +} + +output "vpc_endpoint_kms" { + value = module.ssm.vpc_endpoint_kms +} diff --git a/example/examplea/provider.aws.tf b/example/examplea/provider.aws.tf new file mode 100644 index 0000000..b64be2a --- /dev/null +++ b/example/examplea/provider.aws.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = "eu-west-2" +} diff --git a/iam.tf b/iam.tf new file mode 100644 index 0000000..864343e --- /dev/null +++ b/iam.tf @@ -0,0 +1,150 @@ + +data "aws_iam_policy_document" "kms_access" { + # checkov:skip=CKV_AWS_111: todo reduce perms on key + # checkov:skip=CKV_AWS_109: ADD REASON + statement { + sid = "KMS Key Default" + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + actions = [ + "kms:*", + ] + + resources = ["*"] + + } + + statement { + sid = "CloudWatchLogsEncryption" + principals { + type = "Service" + identifiers = ["logs.${data.aws_region.current.name}.amazonaws.com"] + } + actions = [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*", + ] + + resources = ["*"] + } + +} + + +#"kmsKeyId": "${aws_kms_key.ssmkey.key_id}", +#"kmsKeyId": "${aws_kms_key.ssmkey.arn}", + +# Create EC2 Instance Role +resource "aws_iam_role" "ssm_role" { + name = "ssm_role" + path = "/" + tags = var.tags + + assume_role_policy = <