Skip to content

Commit

Permalink
repo setup
Browse files Browse the repository at this point in the history
  • Loading branch information
johnricords committed Mar 26, 2024
1 parent 0f73aa0 commit e8a4816
Show file tree
Hide file tree
Showing 19 changed files with 1,465 additions and 2 deletions.
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
# terraform-aws-tardigrade-iam-key-enforcer
Module to manage IAM key enforcer
# Tardigrade IAM Key Enforcer

This repo contains the Python-based Lambda function that will audit IAM Access keys for an account and will enforce key rotation as well as notify users.

## Basic Function

The Lambda function is triggered for each account by an Event notification that is configured to run on a schedule.
The function audits each user in an account for access keys and determines how long before they expire, it will then notify users that their key expires in X days and that automatic key enforcement is forthcoming.

<!-- BEGIN TFDOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.1 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 3.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 3.0 |

## Resources

| Name | Type |
|------|------|
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_iam_policy_document.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_admin_email"></a> [admin\_email](#input\_admin\_email) | Admin Email that will receive all emails and reports about actions taken if email is enabled | `string` | n/a | yes |
| <a name="input_assume_role_name"></a> [assume\_role\_name](#input\_assume\_role\_name) | Name of the IAM role that the lambda will assume in the target account | `string` | n/a | yes |
| <a name="input_email_source"></a> [email\_source](#input\_email\_source) | Email that will be used to send messages | `string` | n/a | yes |
| <a name="input_key_age_delete"></a> [key\_age\_delete](#input\_key\_age\_delete) | Age at which a key should be deleted (e.g. 120) | `number` | n/a | yes |
| <a name="input_key_age_inactive"></a> [key\_age\_inactive](#input\_key\_age\_inactive) | Age at which a key should be inactive (e.g. 90) | `number` | n/a | yes |
| <a name="input_key_age_warning"></a> [key\_age\_warning](#input\_key\_age\_warning) | Age at which to warn (e.g. 75) | `number` | n/a | yes |
| <a name="input_key_use_threshold"></a> [key\_use\_threshold](#input\_key\_use\_threshold) | Age at which unused keys should be deleted (e.g.30) | `number` | n/a | yes |
| <a name="input_accounts"></a> [accounts](#input\_accounts) | List of account objects to create events for | <pre>list(object({<br> account_name = string<br> account_number = string<br> role_name = optional(string) # deprecated<br> armed = bool<br> debug = optional(bool, false)<br> email_user_enabled = bool<br> email_targets = list(string)<br> exempt_groups = list(string)<br> schedule_expression = optional(string, "cron(0 1 ? * SUN *)")<br><br> }))</pre> | `[]` | no |
| <a name="input_email_admin_report_enabled"></a> [email\_admin\_report\_enabled](#input\_email\_admin\_report\_enabled) | Used to enable or disable the SES emailed report | `bool` | `false` | no |
| <a name="input_email_admin_report_subject"></a> [email\_admin\_report\_subject](#input\_email\_admin\_report\_subject) | Subject of the report email that is sent | `string` | `null` | no |
| <a name="input_email_banner_message"></a> [email\_banner\_message](#input\_email\_banner\_message) | Messages that will be at the top of all emails sent to notify recipients of important information | `string` | `""` | no |
| <a name="input_email_banner_message_color"></a> [email\_banner\_message\_color](#input\_email\_banner\_message\_color) | Color of email banner message, must be valid html color | `string` | `"red"` | no |
| <a name="input_email_tag"></a> [email\_tag](#input\_email\_tag) | Tag to be placed on the IAM user that we can use to notify when their key is going to be disabled/deleted | `string` | `"keyenforcer:email"` | no |
| <a name="input_email_templates"></a> [email\_templates](#input\_email\_templates) | Email templates to use for Admin and User emails | <pre>object({<br> admin = optional(object({<br> subject = optional(string, null),<br> html = optional(string, null),<br> text = optional(string, null),<br> }), {}),<br> user = optional(object({<br> subject = optional(string, null),<br> html = optional(string, null),<br> text = optional(string, null),<br> }), {})<br> })</pre> | `{}` | no |
| <a name="input_lambda"></a> [lambda](#input\_lambda) | Map of any additional arguments for the upstream lambda module. See <https://github.com/terraform-aws-modules/terraform-aws-lambda> | <pre>object({<br> artifacts_dir = optional(string, "builds")<br> build_in_docker = optional(bool, false)<br> create_package = optional(bool, true)<br> ephemeral_storage_size = optional(number)<br> ignore_source_code_hash = optional(bool, true)<br> local_existing_package = optional(string)<br> recreate_missing_package = optional(bool, false)<br> runtime = optional(string, "python3.11")<br> s3_bucket = optional(string)<br> s3_existing_package = optional(map(string))<br> s3_prefix = optional(string)<br> store_on_s3 = optional(bool, false)<br> timeout = optional(number, 300)<br> source_path = optional(object({<br> patterns = optional(list(string), ["!\\.terragrunt-source-manifest"])<br> }), {})<br> })</pre> | `{}` | no |
| <a name="input_log_level"></a> [log\_level](#input\_log\_level) | Log level for lambda | `string` | `"INFO"` | no |
| <a name="input_project_name"></a> [project\_name](#input\_project\_name) | Project name to prefix resources with | `string` | `"iam-key-enforcer"` | no |
| <a name="input_s3_bucket"></a> [s3\_bucket](#input\_s3\_bucket) | Bucket name to write the audit report to if s3\_enabled is set to 'true' | `string` | `null` | no |
| <a name="input_s3_enabled"></a> [s3\_enabled](#input\_s3\_enabled) | Set to 'true' and provide s3\_bucket if the audit report should be written to S3 | `bool` | `false` | no |
| <a name="input_schedule_expression"></a> [schedule\_expression](#input\_schedule\_expression) | (DEPRECATED) Schedule Expressions for Rules | `string` | `null` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | Tags for resource | `map(string)` | `{}` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_lambda"></a> [lambda](#output\_lambda) | The lambda module object |
| <a name="output_queue"></a> [queue](#output\_queue) | The SQS Queue resource object |

<!-- END TFDOCS -->
53 changes: 53 additions & 0 deletions email_templates/admin_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{#if email_banner_msg}}
<h1 style="color:{{email_banner_msg_color}}">{{email_banner_msg}}</h1>
{{/if}}

<h2>Expiring Access Key Report for {{account_number}} - {{account_name}}</h2>

{{#if unarmed}}
<h3 style="color:red">
The IAM Key Enforcer is not active and NO action has been taken on your key
</h3>
<p>
The information below is for informational purposes and represents the results if the IAM Key Enforcer were active.
</p>
{{/if}}

<p>
Access Keys over {{key_age_inactive}} days old have been DEACTIVATED, keys older than {{key_age_delete}} days have
been DELETED.
Access keys over {{key_age_warning}} days old are DEACTIVATED at {{key_age_inactive}} days old and DELETED after
{{key_age_delete}} days old.
Rotate any keys as necessary to prevent disruption to your applications.
</p>

{{#if exempt_groups}}
<p>
Grayed out rows are exempt via membership in an exempt IAM Group(s):{{exempt_groups}}.
<br />
Exempted group members also have a key status value of &lt;STATUS&gt; (Exempt).
</p>
{{/if}}

<table>
<thead>
<tr>
<th>IAM User Name</th>
<th>Access Key ID</th>
<th>Key Age</th>
<th>Key Status</th>
<th>Last Used</th>
</tr>
</thead>
<tbody>
{{#each key_report_contents}}
<tr bgcolor="{{bg_color}}">
<td>{{user_name}}</td>
<td>{{access_key_id}}</td>
<td>{{key_age}}</td>
<td>{{key_status}}</td>
<td>{{last_used_date}}</td>
</tr>
{{/each}}
</tbody>
</table>
26 changes: 26 additions & 0 deletions email_templates/admin_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{{#if email_banner_msg}}
{{email_banner_msg}}
{{/if}}

<h2>Expiring Access Key Report for {{account_number}} - {{account_name}}</h2>

{{#if unarmed}}
The IAM Key Enforcer is not active and NO action has been taken on your key

The information below is for informational purposes and represents the results if the IAM Key Enforcer were active.
{{/if}}

Access Keys over {{key_age_inactive}} days old have been DEACTIVATED, keys older than {{key_age_delete}} days have been DELETED.
Access keys over {{key_age_warning}} days old are DEACTIVATED at {{key_age_inactive}} days old and DELETED after {{key_age_delete}} days old.
Rotate any keys as necessary to prevent disruption to your applications.


{{#if exempt_groups}}
Exempted IAM Group(s):{{exempt_groups}}.
Exempted group members have a key status value of <STATUS> (Exempt).
{{/if}}

IAM User Name, Access Key ID, Key Age, Key Status, Last Used
{{#each key_report_contents}}
{{user_name}}, {{access_key_id}}, {{key_age}}, {{key_status}}, {{last_used_date}}
{{/each}}
16 changes: 16 additions & 0 deletions email_templates/user_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#if email_banner_msg}}
<h1 style="color:{{email_banner_msg_color}}">{{email_banner_msg}}</h1>
{{/if}}

<h2>Expiring Access Key Report for {{user_name}}</h2>

{{#if unarmed}}
<h3 style="color:red">
The IAM Key Enforcer is not active and NO action has been taken on your key
</h3>
<p>
The information below is for informational purposes and represents the results if the IAM Key Enforcer were active.
</p>
{{/if}}

<p>The access key {{access_key_id}} is over {{key_age}} days old and has been {{action}}.</p>
13 changes: 13 additions & 0 deletions email_templates/user_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{#if email_banner_msg}}
{{email_banner_msg}}
{{/if}}

Expiring Access Key Report for {{user_name}}

{{#if unarmed}}
The IAM Key Enforcer is not active and NO action has been taken on your key

The information below is for informational purposes and represents the results if the IAM Key Enforcer were active.
{{/if}}

The access key {{access_key_id}} is over {{key_age}} days old and has been {{action}}.
191 changes: 191 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}
data "aws_region" "current" {}

data "aws_iam_policy_document" "lambda" {
statement {
sid = "AllowS3Object"
actions = [
"s3:PutObject",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
]
resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.s3_bucket}/*"]
}

statement {
actions = [
"ses:SendEmail",
"ses:SendTemplatedEmail",
"ses:TestRenderTemplate"
]
resources = [
"*"
]
}

statement {
sid = "AllowAssumeRole"
actions = [
"sts:AssumeRole"
]
resources = [
"arn:${data.aws_partition.current.partition}:iam::*:role/${var.assume_role_name}"
]
}
}

module "lambda" {
source = "git::https://github.com/terraform-aws-modules/terraform-aws-lambda.git?ref=v7.2.1"

description = "Lambda function for Key Enforcement"
function_name = var.project_name
handler = "iam_key_enforcer.lambda_handler"
tags = var.tags

attach_policy_json = true
policy_json = data.aws_iam_policy_document.lambda.json

artifacts_dir = var.lambda.artifacts_dir
build_in_docker = var.lambda.build_in_docker
create_package = var.lambda.create_package
ignore_source_code_hash = var.lambda.ignore_source_code_hash
local_existing_package = var.lambda.local_existing_package
recreate_missing_package = var.lambda.recreate_missing_package
ephemeral_storage_size = var.lambda.ephemeral_storage_size
runtime = var.lambda.runtime
s3_bucket = var.lambda.s3_bucket
s3_existing_package = var.lambda.s3_existing_package
s3_prefix = var.lambda.s3_prefix
store_on_s3 = var.lambda.store_on_s3
timeout = var.lambda.timeout

environment_variables = {
LOG_LEVEL = var.log_level
EMAIL_ADMIN_REPORT_ENABLED = var.email_admin_report_enabled
EMAIL_ADMIN_REPORT_SUBJECT = var.email_admin_report_subject
EMAIL_SOURCE = var.email_source
ADMIN_EMAIL = var.admin_email
KEY_AGE_WARNING = var.key_age_warning
KEY_AGE_INACTIVE = var.key_age_inactive
KEY_AGE_DELETE = var.key_age_delete
KEY_USE_THRESHOLD = var.key_use_threshold
S3_ENABLED = var.s3_enabled
S3_BUCKET = var.s3_bucket
EMAIL_TAG = var.email_tag
EMAIL_BANNER_MSG = var.email_banner_message
EMAIL_BANNER_MSG_COLOR = var.email_banner_message_color
EMAIL_USER_TEMPLATE = aws_ses_template.user_template.id
EMAIL_ADMIN_TEMPLATE = aws_ses_template.admin_template.id
}

source_path = [
{
path = "${path.module}/src/python",
prefix_in_zip = ""
pip_requirements = true
patterns = var.lambda.source_path.patterns
},
]
}

resource "aws_lambda_permission" "this" {
action = "lambda:InvokeFunction"
function_name = module.lambda.lambda_function_name
principal = "events.amazonaws.com"
source_arn = "arn:${data.aws_partition.current.partition}:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/${var.project_name}-*"
}

##############################
# SQS Queue Policy
##############################
resource "aws_sqs_queue_policy" "this" {
queue_url = aws_sqs_queue.this.id
policy = jsonencode(
{
Version = "2012-10-17",
Id = "sqspolicy",
Statement : [
{
Sid = "AllowSend",
Effect = "Allow",
Principal = "*",
Action = "sqs:SendMessage",
Resource = aws_sqs_queue.this.arn,
Condition = {
"ArnLike" : {
"aws:SourceArn" : "arn:${data.aws_partition.current.partition}:events:*:*:rule/${var.project_name}-*"
}
}
},
{
Sid = "AllowRead",
Effect = "Allow",
"Principal" : {
"AWS" : "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root"
},
Action = "sqs:ReceiveMessage",
Resource = aws_sqs_queue.this.arn,
}
]
}
)
}

##############################
# SQS Queue
##############################
resource "aws_sqs_queue" "this" {
name = "${var.project_name}-dlq"
message_retention_seconds = 1209600
receive_wait_time_seconds = 20
visibility_timeout_seconds = 30
tags = var.tags
}

##############################
# Schedule Event
##############################
module "scheduled_events" {
source = "./modules/scheduled_event"
for_each = { for account in var.accounts : account.account_name => account }

event_name = each.value.account_name
event_rule_description = "Scheduled Event that runs IAM Key Enforcer Lambda for account ${each.value.account_number} - ${each.value.account_name}"
lambda_arn = module.lambda.lambda_function_arn
project_name = var.project_name
tags = var.tags
schedule_expression = var.schedule_expression != null ? var.schedule_expression : each.value.schedule_expression


dead_letter_config = {
arn = aws_sqs_queue.this.arn
}

input_transformer = {
input_template = jsonencode({
"account_number" : each.value.account_number,
"account_name" : each.value.account_name,
"role_arn" : "arn:${data.aws_partition.current.partition}:iam::${each.value.account_number}:role/${var.assume_role_name}",
"armed" : each.value.armed,
"debug" : each.value.debug,
"email_targets" : each.value.email_targets,
"exempt_groups" : each.value.exempt_groups,
"email_user_enabled" : each.value.email_user_enabled,
})
}
}

resource "aws_ses_template" "user_template" {
name = "${var.project_name}-user"
html = var.email_templates.user.html != null ? var.email_templates.user.html : file("${path.module}/email_templates/user_email.html")
subject = var.email_templates.user.subject != null ? var.email_templates.user.subject : "IAM User Key {{armed_state_msg}} for {{user_name}}"
text = var.email_templates.user.text != null ? var.email_templates.user.text : file("${path.module}/email_templates/user_email.txt")
}

resource "aws_ses_template" "admin_template" {
name = "${var.project_name}-admin"
html = var.email_templates.admin.html != null ? var.email_templates.admin.html : file("${path.module}/email_templates/admin_email.html")
subject = var.email_templates.admin.subject != null ? var.email_templates.admin.subject : "IAM Key Enforcement Report for {{account_number}}"
text = var.email_templates.admin.text != null ? var.email_templates.admin.text : file("${path.module}/email_templates/admin_email.txt")
}
Loading

0 comments on commit e8a4816

Please sign in to comment.