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

repo setup #2

Merged
merged 4 commits into from
Mar 28, 2024
Merged
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
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ updates:
docker:
patterns:
- "*"
# Maintain dependencies for terraform
- package-ecosystem: terraform
directory: /
schedule:
interval: weekly
groups:
terraform:
patterns:
- "*"
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
FROM plus3it/tardigrade-ci:0.24.15

COPY ./src/python/requirements.txt /app/requirements/lambda.txt

RUN python -m pip install --no-cache-dir \
-r /app/requirements/lambda.txt
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