initial commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<!-- This readme file is generated with terraform-docs -->
|
||||
# SecretRotationReminder
|
||||
Deploy lambda function which takes secret rotation event from secretsmanager
|
||||
and send reminders to users using SNS.
|
||||
This function can be used by any number of secrets
|
||||
Secret ARN is obtained from the secretsmanager event
|
||||
|
||||
This function overrides the blueprint function from AWS. Instead of rotating the secret value,
|
||||
it sends a reminder to user who will manually rotate the secret.
|
||||
|
||||
## Requirements
|
||||
|
||||
No requirements.
|
||||
|
||||
## Providers
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| archive | n/a |
|
||||
| aws | n/a |
|
||||
|
||||
## Modules
|
||||
|
||||
No modules.
|
||||
|
||||
## Resources
|
||||
|
||||
| Name | Type |
|
||||
|------|------|
|
||||
| [aws_cloudwatch_log_group.rotation-reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
|
||||
| [aws_iam_policy.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
|
||||
| [aws_iam_role.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
|
||||
| [aws_iam_role_policy_attachment.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
|
||||
| [aws_lambda_function.rotation-reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
|
||||
| [aws_lambda_permission.rotation-reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
|
||||
| [aws_security_group.rotation-reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
|
||||
| [aws_sns_topic.reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
|
||||
| [aws_sns_topic_subscription.reminder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource |
|
||||
| [archive_file.payload](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
|
||||
| [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
|
||||
| [aws_subnet.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Description | Type | Default | Required |
|
||||
|------|-------------|------|---------|:--------:|
|
||||
| lambda-subnet-ids | List of subnets to place lambda function | `list(string)` | n/a | yes |
|
||||
| logs-cmk-arn | ARN of cloudwatch logs encryption CMK | `string` | n/a | yes |
|
||||
| prefix | Resource prefix. e.g. whk1-bea-icc-mbk | `string` | n/a | yes |
|
||||
| rotation-reminder-recipients | SNS recipients for secret rotation reminders | `list(string)` | n/a | yes |
|
||||
| sns-cmk-arn | ARN of SNS encryption CMK | `string` | n/a | yes |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| function-arn | n/a |
|
||||
|
||||
---
|
||||
## Authorship
|
||||
This module was developed by Rackspace.
|
||||
@@ -0,0 +1,17 @@
|
||||
module "secret-rotation-reminder" {
|
||||
source = "../"
|
||||
sns-cmk-arn = "arn:aws:kms:ap-east-1:111122223333:key/e13912c7-54d3-4d77-9a52-c482bcaf3209"
|
||||
logs-cmk-arn = "arn:aws:kms:ap-east-1:111122223333:key/143d0178-8ad2-458b-90b3-0fa6b3e62fc4"
|
||||
rotation-reminder-recipients = ["foo@bar.local"]
|
||||
prefix = "prod-project1"
|
||||
lambda-subnet-ids = ["subnet-001", "subnet-002"]
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_rotation" "secret-rotation" {
|
||||
secret_id = "your-secret-id"
|
||||
rotation_lambda_arn = module.secret-rotation-reminder.function-arn
|
||||
rotate_immediately = false
|
||||
rotation_rules {
|
||||
automatically_after_days = 365
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* # SecretRotationReminder
|
||||
* Deploy lambda function which takes secret rotation event from secretsmanager
|
||||
* and send reminders to users using SNS.
|
||||
* This function can be used by any number of secrets
|
||||
* Secret ARN is obtained from the secretsmanager event
|
||||
*
|
||||
* This function overrides the blueprint function from AWS. Instead of rotating the secret value,
|
||||
* it sends a reminder to user who will manually rotate the secret.
|
||||
*/
|
||||
|
||||
resource "aws_sns_topic" "reminder" {
|
||||
name = "${var.prefix}-SecretRotationReminder"
|
||||
kms_master_key_id = var.sns-cmk-arn
|
||||
}
|
||||
|
||||
resource "aws_sns_topic_subscription" "reminder" {
|
||||
for_each = toset(var.rotation-reminder-recipients)
|
||||
topic_arn = aws_sns_topic.reminder.arn
|
||||
protocol = "email"
|
||||
endpoint = each.value
|
||||
}
|
||||
|
||||
data "archive_file" "payload" {
|
||||
type = "zip"
|
||||
source_file = "${path.module}/rotation_reminder.py"
|
||||
output_path = "payload.zip"
|
||||
}
|
||||
|
||||
data "aws_subnet" "this" {
|
||||
id = var.lambda-subnet-ids[0]
|
||||
}
|
||||
|
||||
resource "aws_security_group" "rotation-reminder" {
|
||||
name = "${var.prefix}-SecretRotationReminder"
|
||||
description = "Allow access to VPC endpoint"
|
||||
vpc_id = data.aws_subnet.this.vpc_id
|
||||
|
||||
egress {
|
||||
description = "Access to VPC endpoints"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "rotation-reminder" {
|
||||
function_name = "${var.prefix}-SecretRotationReminder"
|
||||
description = "Sends secret rotation reminder"
|
||||
role = aws_iam_role.lambda.arn
|
||||
handler = "rotation_reminder.lambda_handler"
|
||||
filename = data.archive_file.payload.output_path
|
||||
source_code_hash = data.archive_file.payload.output_base64sha256
|
||||
runtime = "python3.13"
|
||||
timeout = 180
|
||||
vpc_config {
|
||||
subnet_ids = var.lambda-subnet-ids
|
||||
security_group_ids = [aws_security_group.rotation-reminder.id]
|
||||
}
|
||||
|
||||
environment {
|
||||
variables = {
|
||||
SNS_TOPIC_ARN = aws_sns_topic.reminder.arn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "rotation-reminder" {
|
||||
statement_id = "SecretRotationReminderPermission"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.rotation-reminder.function_name
|
||||
principal = "secretsmanager.amazonaws.com"
|
||||
# this function should be allowed to send reminders for all secrets # source_arn = module.mock-secret.secret_arn
|
||||
}
|
||||
|
||||
|
||||
resource "aws_cloudwatch_log_group" "rotation-reminder" {
|
||||
name = "/aws/lambda/whk1-bea-icc-obk-SecretRotationReminder"
|
||||
retention_in_days = 400 # intentionally set to longer than 1 year as rotation may happen yearly
|
||||
kms_key_id = var.logs-cmk-arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "lambda" {
|
||||
name = "${var.prefix}-SecretRotationReminderFunctionRole"
|
||||
assume_role_policy = data.aws_iam_policy_document.assume_role.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "lambda" {
|
||||
for_each = { for k, v in [
|
||||
"arn:aws:iam::aws:policy/AWSLambdaExecute",
|
||||
aws_iam_policy.lambda.arn
|
||||
] : k => v }
|
||||
role = aws_iam_role.lambda.name
|
||||
policy_arn = each.value
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "lambda" {
|
||||
name_prefix = "SecretRotationPolicy"
|
||||
policy = jsonencode(
|
||||
{
|
||||
"Version" : "2012-10-17",
|
||||
"Statement" : [
|
||||
{
|
||||
"Sid" : "AllowAccessToSecretSnsVpc",
|
||||
"Effect" : "Allow",
|
||||
"Action" : [
|
||||
"SNS:Publish",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:ListSecretVersionIds",
|
||||
"secretsmanager:UpdateSecretVersionStage",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:PutSecretValue",
|
||||
"ec2:CreateNetworkInterface",
|
||||
"ec2:DescribeNetworkInterfaces",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DeleteNetworkInterface",
|
||||
"ec2:AssignPrivateIpAddresses",
|
||||
"ec2:UnassignPrivateIpAddresses",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeVpcs",
|
||||
"ec2:GetSecurityGroupsForVpc"
|
||||
],
|
||||
"Resource" : "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["lambda.amazonaws.com"]
|
||||
}
|
||||
|
||||
actions = ["sts:AssumeRole"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
output function-arn {
|
||||
value = aws_lambda_function.rotation-reminder.arn
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
This function is designed to send a reminder through SNS
|
||||
when secretsmanager automatic rotation event arrives. It
|
||||
does not rotate any secret.
|
||||
|
||||
Secretsmanager initiate rotation in 4 steps [1], and some of
|
||||
these steps must be implemented even if the purpose of this
|
||||
function is not to rotate any secret.
|
||||
|
||||
[1] https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda-functions.html
|
||||
"""
|
||||
import boto3
|
||||
import os
|
||||
|
||||
|
||||
# Read sns topic from lambda environment variable
|
||||
SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN']
|
||||
|
||||
# lambda handler
|
||||
def lambda_handler(event, context):
|
||||
# debug use
|
||||
# print(f"DEBUG: Event received by Lambda: {event}")
|
||||
|
||||
secret_id = event['SecretId']
|
||||
token = event['ClientRequestToken']
|
||||
step = event['Step']
|
||||
|
||||
# Secretsmanager sends 4 rotation events, but we will only use the createSecret event
|
||||
# and the finishSecret event
|
||||
if step == "createSecret":
|
||||
# send notification and create a new secret from existing secret
|
||||
send_notification(secret_id, token)
|
||||
elif step == "finishSecret":
|
||||
# set new secret with version AWSCURRENT
|
||||
swap_current_version(secret_id, token)
|
||||
else:
|
||||
print(f"Steps other than createSecret and finishSecret will be ignored: {step}")
|
||||
|
||||
def send_notification(secret_id, token):
|
||||
print(f"Clone secret and send notification for {secret_id}")
|
||||
sm_client = boto3.client('secretsmanager')
|
||||
"""
|
||||
A new secret version is required by rotation workflow
|
||||
We will simply copy existing version to a new version
|
||||
label it AWSPENDING
|
||||
"""
|
||||
orig_secret = sm_client.get_secret_value(
|
||||
SecretId=secret_id,
|
||||
VersionStage='AWSCURRENT'
|
||||
)['SecretString']
|
||||
|
||||
response = sm_client.put_secret_value(
|
||||
SecretId=secret_id,
|
||||
ClientRequestToken=token,
|
||||
SecretString=orig_secret,
|
||||
VersionStages=['AWSPENDING']
|
||||
)
|
||||
print(f"Retrieved existing secret and saved it as AWSPENDING. Version id is {response['VersionId']}")
|
||||
|
||||
# Send out reminder about secret rotation
|
||||
sns_client = boto3.client('sns')
|
||||
sns_client.publish(
|
||||
TopicArn=SNS_TOPIC_ARN,
|
||||
Message=f"""Hello Cloud Operation team,
|
||||
|
||||
The following secret is due for update. Please perform the following steps to rotate the secret:
|
||||
|
||||
{secret_id}
|
||||
|
||||
1. Open the secret on secretsmanager.
|
||||
2. Click the Retrieve secret value botton to reveal the Edit button.
|
||||
3. Click on Edit and change the secret string to a new one. Click save to commit your change.
|
||||
4. Update the password for the underlying resource (e.g. redis or rds)
|
||||
5. Optionally update your application configuration with the new credential if it does not fetch from secretsmanager automatically
|
||||
|
||||
""",
|
||||
Subject='Secret rotation reminder for ' + secret_id.split(":")[6]
|
||||
)
|
||||
print(f"Notification sent to {SNS_TOPIC_ARN}")
|
||||
|
||||
def swap_current_version(secret_id, token):
|
||||
sm_client = boto3.client('secretsmanager')
|
||||
metadata = sm_client.describe_secret(SecretId=secret_id)
|
||||
pending_version_id = None
|
||||
current_version_id = None
|
||||
for version in metadata["VersionIdsToStages"]:
|
||||
if "AWSCURRENT" in metadata["VersionIdsToStages"][version]:
|
||||
current_version_id = version
|
||||
elif "AWSPENDING" in metadata["VersionIdsToStages"][version]:
|
||||
pending_version_id = version
|
||||
|
||||
print(f"Remove {current_version_id} from AWSCURRENT and point AWSCURRENT to {pending_version_id}")
|
||||
sm_client.update_secret_version_stage(
|
||||
SecretId=secret_id,
|
||||
VersionStage="AWSCURRENT",
|
||||
MoveToVersionId=pending_version_id,
|
||||
RemoveFromVersionId=current_version_id
|
||||
)
|
||||
|
||||
print(f"Remove AWSPENDING from {pending_version_id}")
|
||||
sm_client.update_secret_version_stage(
|
||||
SecretId=secret_id,
|
||||
VersionStage='AWSPENDING',
|
||||
RemoveFromVersionId=pending_version_id
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
variable "rotation-reminder-recipients" {
|
||||
type = list(string)
|
||||
description = "SNS recipients for secret rotation reminders"
|
||||
}
|
||||
|
||||
variable "sns-cmk-arn" {
|
||||
type = string
|
||||
description = "ARN of SNS encryption CMK"
|
||||
}
|
||||
|
||||
variable "prefix" {
|
||||
type = string
|
||||
description = "Resource prefix. e.g. whk1-bea-icc-mbk"
|
||||
}
|
||||
|
||||
variable "lambda-subnet-ids" {
|
||||
type = list(string)
|
||||
description = "List of subnets to place lambda function"
|
||||
}
|
||||
|
||||
variable "logs-cmk-arn" {
|
||||
type = string
|
||||
description = "ARN of cloudwatch logs encryption CMK"
|
||||
}
|
||||
Reference in New Issue
Block a user