Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Bastion S3 Logging #63

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ No modules.

| Name | Type |
|------|------|
| [aws_cloudtrail.ssh-access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudtrail) | resource |
| [aws_cloudwatch_event_rule.ssh_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource |
| [aws_cloudwatch_event_target.ssm_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource |
| [aws_cloudwatch_log_group.ec2_cloudwatch_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
Expand Down Expand Up @@ -61,6 +62,7 @@ No modules.
| [aws_s3_bucket_logging.access_logging_on_session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_logging) | resource |
| [aws_s3_bucket_notification.session_logs_bucket_notification](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_notification) | resource |
| [aws_s3_bucket_ownership_controls.session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource |
| [aws_s3_bucket_policy.cloudwatch-s3-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | 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_s3_bucket_server_side_encryption_configuration.session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.session_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
Expand All @@ -72,6 +74,7 @@ No modules.
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_iam_policy.AmazonElasticFileSystemFullAccess](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | 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.cloudwatch-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.ssm_ec2_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_kms_key.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_key) | data source |
Expand Down Expand Up @@ -114,7 +117,7 @@ No modules.
| <a name="input_region"></a> [region](#input\_region) | AWS Region | `string` | n/a | yes |
| <a name="input_root_volume_config"></a> [root\_volume\_config](#input\_root\_volume\_config) | n/a | <pre>object({<br> volume_type = any<br> volume_size = any<br> })</pre> | <pre>{<br> "volume_size": "20",<br> "volume_type": "gp3"<br>}</pre> | no |
| <a name="input_security_group_ids"></a> [security\_group\_ids](#input\_security\_group\_ids) | List of security groups to associate with instance | `list(any)` | `[]` | no |
| <a name="input_session_log_bucket_name_prefix"></a> [session\_log\_bucket\_name\_prefix](#input\_session\_log\_bucket\_name\_prefix) | Name prefix of S3 bucket to store session logs | `string` | n/a | yes |
| <a name="input_session_logs_bucket_name_prefix"></a> [session\_logs\_bucket\_name\_prefix](#input\_session\_logs\_bucket\_name\_prefix) | Name prefix of S3 bucket to store session logs | `string` | n/a | yes |
| <a name="input_ssh_password"></a> [ssh\_password](#input\_ssh\_password) | Password for SSH access if SSM authentication is enabled | `string` | n/a | yes |
| <a name="input_ssh_user"></a> [ssh\_user](#input\_ssh\_user) | Username to use when accessing the instance using SSH | `string` | `"ubuntu"` | no |
| <a name="input_ssm_enabled"></a> [ssm\_enabled](#input\_ssm\_enabled) | Enable SSM agent | `bool` | `true` | no |
Expand Down
12 changes: 6 additions & 6 deletions examples/complete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ Example that uses the module with many of its configurations. Used in CI E2E tes
|------|------|
| [aws_kms_alias.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
| [aws_kms_key.default](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_lifecycle_configuration.access_log_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource |
| [aws_s3_bucket_notification.access_log_bucket_notification](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_notification) | 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_server_side_encryption_configuration.access_log_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.access_log_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
| [aws_s3_bucket.access_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket_lifecycle_configuration.access_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource |
| [aws_s3_bucket_notification.access_logs_bucket_notification](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_notification) | resource |
| [aws_s3_bucket_public_access_block.access_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_server_side_encryption_configuration.access_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.access_logs_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
| [aws_sqs_queue.access_log_queue](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource |
| [random_id.default](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
| [aws_ami.amazonlinux2](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source |
Expand Down
66 changes: 33 additions & 33 deletions examples/complete/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ resource "random_id" "default" {

locals {
# Add randomness to names to avoid collisions when multiple users are using this example
vpc_name = "${var.name_prefix}-${lower(random_id.default.hex)}"
bastion_name = "${var.name_prefix}-bastion-${lower(random_id.default.hex)}"
access_log_bucket_name_prefix = "${var.name_prefix}-accesslog-${lower(random_id.default.hex)}"
session_log_bucket_name_prefix = "${var.name_prefix}-bastionsessionlog-${lower(random_id.default.hex)}"
kms_key_alias_name_prefix = "alias/${var.name_prefix}-${lower(random_id.default.hex)}"
access_log_sqs_queue_name = "${var.name_prefix}-accesslog-access-${lower(random_id.default.hex)}"
vpc_name = "${var.name_prefix}-${lower(random_id.default.hex)}"
bastion_name = "${var.name_prefix}-bastion-${lower(random_id.default.hex)}"
access_logs_bucket_name_prefix = "${var.name_prefix}-accesslog-${lower(random_id.default.hex)}"
session_logs_bucket_name_prefix = "${var.name_prefix}-bastionsessionlog-${lower(random_id.default.hex)}"
kms_key_alias_name_prefix = "alias/${var.name_prefix}-${lower(random_id.default.hex)}"
access_log_sqs_queue_name = "${var.name_prefix}-accesslog-access-${lower(random_id.default.hex)}"
}

module "vpc" {
Expand Down Expand Up @@ -123,31 +123,31 @@ data "aws_iam_policy_document" "kms_access" {


# Create S3 bucket for access logs with versioning, encryption, blocked public access enabled
resource "aws_s3_bucket" "access_log_bucket" {
resource "aws_s3_bucket" "access_logs_bucket" {
# checkov:skip=CKV_AWS_144: Cross region replication is overkill
# checkov:skip=CKV_AWS_18: "Ensure the S3 bucket has access logging enabled" -- This is the access logging bucket. Logging to the logging bucket would cause an infinite loop.
bucket_prefix = local.access_log_bucket_name_prefix
bucket_prefix = local.access_logs_bucket_name_prefix
force_destroy = true
tags = var.tags

lifecycle {
precondition {
condition = length(local.access_log_bucket_name_prefix) <= 37
condition = length(local.access_logs_bucket_name_prefix) <= 37
error_message = "Bucket name prefixes may not be longer than 37 characters."
}
}
}

resource "aws_s3_bucket_versioning" "access_log_bucket" {
bucket = aws_s3_bucket.access_log_bucket.id
resource "aws_s3_bucket_versioning" "access_logs_bucket" {
bucket = aws_s3_bucket.access_logs_bucket.id

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "access_log_bucket" {
bucket = aws_s3_bucket.access_log_bucket.id
resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs_bucket" {
bucket = aws_s3_bucket.access_logs_bucket.id

rule {
apply_server_side_encryption_by_default {
Expand All @@ -157,16 +157,16 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "access_log_bucket
}
}

resource "aws_s3_bucket_public_access_block" "access_log_bucket" {
bucket = aws_s3_bucket.access_log_bucket.id
resource "aws_s3_bucket_public_access_block" "access_logs_bucket" {
bucket = aws_s3_bucket.access_logs_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_s3_bucket_lifecycle_configuration" "access_log_bucket" {
bucket = aws_s3_bucket.access_log_bucket.id
resource "aws_s3_bucket_lifecycle_configuration" "access_logs_bucket" {
bucket = aws_s3_bucket.access_logs_bucket.id

rule {
id = "delete_after_X_days"
Expand Down Expand Up @@ -204,17 +204,17 @@ resource "aws_sqs_queue" "access_log_queue" {
"Action": "sqs:SendMessage",
"Resource": "arn:${data.aws_partition.current.partition}:sqs:*:*:${local.access_log_sqs_queue_name}",
"Condition": {
"ArnEquals": { "aws:SourceArn": "${aws_s3_bucket.access_log_bucket.arn}" }
"ArnEquals": { "aws:SourceArn": "${aws_s3_bucket.access_logs_bucket.arn}" }
}
}
]
}
POLICY
}

resource "aws_s3_bucket_notification" "access_log_bucket_notification" {
resource "aws_s3_bucket_notification" "access_logs_bucket_notification" {
count = var.enable_sqs_events_on_access_log_access ? 1 : 0
bucket = aws_s3_bucket.access_log_bucket.id
bucket = aws_s3_bucket.access_logs_bucket.id

queue {
queue_arn = aws_sqs_queue.access_log_queue[0].arn
Expand Down Expand Up @@ -245,19 +245,19 @@ module "bastion" {
volume_size = "20"
encrypted = true
}
name = local.bastion_name
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.private_subnets[0]
region = var.region
access_logs_bucket_name = aws_s3_bucket.access_log_bucket.id
session_log_bucket_name_prefix = local.session_log_bucket_name_prefix
kms_key_arn = aws_kms_key.default.arn
ssh_user = var.bastion_ssh_user
ssh_password = var.bastion_ssh_password
assign_public_ip = false
enable_log_to_s3 = true
enable_log_to_cloudwatch = true
private_ip = var.private_ip != "" ? var.private_ip : null
name = local.bastion_name
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.private_subnets[0]
region = var.region
access_logs_bucket_name = aws_s3_bucket.access_logs_bucket.id
session_logs_bucket_name_prefix = local.session_logs_bucket_name_prefix
kms_key_arn = aws_kms_key.default.arn
ssh_user = var.bastion_ssh_user
ssh_password = var.bastion_ssh_password
assign_public_ip = false
enable_log_to_s3 = true
enable_log_to_cloudwatch = true
private_ip = var.private_ip != "" ? var.private_ip : null

tenancy = var.bastion_tenancy
zarf_version = var.zarf_version
Expand Down
66 changes: 66 additions & 0 deletions iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,72 @@ resource "aws_iam_role_policy_attachment" "bastion-ssm-aws-efs-policy-attach" {
policy_arn = data.aws_iam_policy.AmazonElasticFileSystemFullAccess.arn
}

data "aws_iam_policy_document" "cloudwatch-policy" {

statement {
sid = "AWSCloudTrailAclCheck"
effect = "Allow"

principals {
type = "Service"
identifiers = ["cloudtrail.amazonaws.com"]
}

actions = [
"s3:GetBucketAcl",
]

resources = [
data.aws_s3_bucket.access_logs_bucket.arn
]

condition {
test = "StringEquals"
variable = "AWS:SourceArn"

values = [
"arn:${data.aws_partition.current.partition}:cloudtrail:${var.region}:${data.aws_caller_identity.current.account_id}:trail/${var.name}-ssh-access",
]
}
}

statement {
sid = "AWSCloudTrailWrite"
effect = "Allow"

principals {
type = "Service"
identifiers = ["cloudtrail.amazonaws.com"]
}

actions = [
"s3:PutObject",
]

resources = [
"arn:${data.aws_partition.current.partition}:s3:::${data.aws_s3_bucket.access_logs_bucket.id}/*",
]

condition {
test = "StringEquals"
variable = "s3:x-amz-acl"

values = [
"bucket-owner-full-control",
]
}

condition {
test = "StringEquals"
variable = "AWS:SourceArn"

values = [
"arn:${data.aws_partition.current.partition}:cloudtrail:${var.region}:${data.aws_caller_identity.current.account_id}:trail/${var.name}-ssh-access",
]
}
}
}

# Create S3/CloudWatch Logs access document, policy and attach to role
data "aws_iam_policy_document" "ssm_s3_cwl_access" {
# checkov:skip=CKV_AWS_111: ADD REASON
Expand Down
20 changes: 20 additions & 0 deletions logging.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ resource "aws_cloudwatch_log_group" "ssh_access_log_group" {
kms_key_id = data.aws_kms_key.default.arn
}

# Create a cloudtrail and event rule to monitor bastion access over ssh
resource "aws_cloudtrail" "ssh-access" {
# checkov:skip=CKV_AWS_252: SNS not currently needed
# checkov:skip=CKV2_AWS_10: Cloudwatch logs already being used with cloudtrail
name = "${var.name}-ssh-access"
s3_bucket_name = data.aws_s3_bucket.access_logs_bucket.id
kms_key_id = data.aws_kms_key.default.arn
is_multi_region_trail = true
enable_log_file_validation = true
event_selector {
read_write_type = "All"
include_management_events = true
}
depends_on = [
aws_s3_bucket_policy.cloudwatch-s3-policy,
data.aws_kms_key.default,
aws_cloudwatch_log_group.ssh_access_log_group
]
}

resource "aws_cloudwatch_event_rule" "ssh_access" {
name = "${var.name}-ssh-access"
description = "filters ssm access logs and sends usable data to a cloudwatch log group"
Expand Down
7 changes: 6 additions & 1 deletion s3-buckets.tf
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#####################################################
##################### S3 Bucket #####################

resource "aws_s3_bucket_policy" "cloudwatch-s3-policy" {
bucket = data.aws_s3_bucket.access_logs_bucket.bucket
policy = data.aws_iam_policy_document.cloudwatch-policy.json
}

# Create S3 bucket for session logs with versioning, encryption, blocked public access enabled
resource "aws_s3_bucket" "session_logs_bucket" {
# checkov:skip=CKV_AWS_144: Cross region replication overkill
bucket_prefix = "${var.session_log_bucket_name_prefix}-"
bucket_prefix = "${var.session_logs_bucket_name_prefix}-"
force_destroy = true
tags = var.tags

Expand Down
4 changes: 2 additions & 2 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ variable "permissions_boundary" {

#### S3 Bucket

variable "session_log_bucket_name_prefix" {
variable "session_logs_bucket_name_prefix" {
description = "Name prefix of S3 bucket to store session logs"
type = string
validation {
condition = length(var.session_log_bucket_name_prefix) <= 37
condition = length(var.session_logs_bucket_name_prefix) <= 37
error_message = "Bucket name prefixes may not be longer than 37 characters."
}
}
Expand Down