diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 80f7462..51d6276 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -2,8 +2,8 @@ name: semgrep
on:
# Scan changed files in PRs, block on new issues only (existing issues ignored)
- pull_request:
- branches: [ main ]
+ # pull_request:
+ # branches: [ main ]
# Scan all files on branches, block on any issues
push:
@@ -15,26 +15,30 @@ on:
# schedule:
# - cron: '30 0 1,15 * *' # scheduled for 00:30 UTC on both the 1st and 15th of the month
+permissions:
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+
defaults:
run:
working-directory: src
jobs:
semgrep:
- permissions:
- contents: read # for actions/checkout to fetch code
- security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
name: scan
runs-on: ubuntu-latest
- container:
- image: returntocorp/semgrep
- # Skip any PR created by dependabot to avoid permission issues
- if: (github.actor != 'dependabot[bot]')
steps:
# Fetch project source
- uses: actions/checkout@v3
- - run: semgrep ci --config "p/ci" --config "p/python" --config "p/owasp-top-ten" --sarif --output=semgrep.sarif
+ - uses: returntocorp/semgrep-action@v1
+ with:
+ generateSarif: "1"
+ config: >-
+ p/ci
+ p/python
+ p/owasp-top-ten
+ p/cwe-top-25
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
diff --git a/.semgrepignore b/.semgrepignore
new file mode 100644
index 0000000..bbf1835
--- /dev/null
+++ b/.semgrepignore
@@ -0,0 +1 @@
+terraform/
diff --git a/README.md b/README.md
index a00b79e..c205fbf 100644
--- a/README.md
+++ b/README.md
@@ -24,10 +24,10 @@ This tool generates a new IAM access key pair every X number of days and informs
![aws-iam-key-rotator](iam-key-rotator.jpeg "AWS IAM Key Rotator")
- CloudWatch triggers lambda function which checks the age of access key for all the IAM users who have **IKR:EMAIL**(case-insensitive) tag attached.
-- If existing access key age is greater than `ACCESS_KEY_AGE` environment variable or `IKR:ROTATE_AFTER_DAYS` tag associated to the IAM user and if the user ONLY has a single key pair associated a new key pair is generated and the same is mailed to the user via your selected mail service.
-- The existing access key is than stored in DynamoDB table with user details and an expiration timestamp.
-- DynamoDB stream triggers destructor lambda function which is responsible for deleting the old access key associated to IAM user if the stream event is `delete`.
-- In case it fails to delete the existing key pair the entry is added back to the DynamoDB table so that the same can be picked up later for retry.
+- If existing access key age is greater than `ACCESS_KEY_AGE` environment variable or `IKR:ROTATE_AFTER_DAYS` tag associated to the IAM user and if the user ONLY has a single key pair associated, a new key pair is generated and if `ENCRYPT_KEY_PAIR` environment variable is set to true the new key pair is encrypted using a symmetric key which is stored in SSM parameter (`/ikr/secret/iam/IAM_USERNAME`) before the same is mailed to the user via the selected mail service.
+- The existing access key is then stored in DynamoDB table with user details and an expiration timestamp.
+- DynamoDB stream triggers destructor lambda function which is responsible for deleting the old access key associated to IAM user and the SSM parameter that stores the symmetric encryption key if `ENCRYPT_KEY_PAIR` environment variable is set to true. The destruction operation is carried out only if the DynamoDB stream event is of type `delete`.
+- In case the destructor function fails to delete the existing key pair, the entry is added back to the DynamoDB table for retry.
### Setup:
- Use the [terraform module](terraform) included in this repo to create all the AWS resources required to automate IAM key rotation
@@ -41,3 +41,4 @@ This tool generates a new IAM access key pair every X number of days and informs
### Helper Script:
- `tag-iam-users.py`: Tags IAM users by reading **iam-user-tags.json** file
+- `decryption.py`: Decrypt cipher text using the encryption key stored in the SSM parmeter store
diff --git a/decryption.py b/decryption.py
new file mode 100644
index 0000000..de190e2
--- /dev/null
+++ b/decryption.py
@@ -0,0 +1,15 @@
+import os
+
+from cryptography.fernet import Fernet
+
+ENC_KEY = os.environ.get('ENC_KEY', None)
+CIPHER_TEXT = os.environ.get('CIPHER_TEXT', None)
+
+if ENC_KEY is None:
+ raise KeyError('ENC_KEY is required to decrypt the cipher text')
+
+if CIPHER_TEXT is None:
+ raise KeyError('CIPHER_TEXT is required')
+
+f = Fernet(ENC_KEY)
+print(f.decrypt(CIPHER_TEXT.encode('utf-8')).decode('utf-8'))
diff --git a/iam-key-rotator.drawio b/iam-key-rotator.drawio
index aaa77e6..f16af0c 100644
--- a/iam-key-rotator.drawio
+++ b/iam-key-rotator.drawio
@@ -1 +1,124 @@
-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
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iam-key-rotator.jpeg b/iam-key-rotator.jpeg
index 38a442c..018735a 100644
Binary files a/iam-key-rotator.jpeg and b/iam-key-rotator.jpeg differ
diff --git a/src/creator.py b/src/creator.py
index 93f9967..1d25f6d 100644
--- a/src/creator.py
+++ b/src/creator.py
@@ -8,6 +8,7 @@
from botocore.exceptions import ClientError
import shared_functions
+import encryption
# Table name which holds existing access key pair details to be deleted
IAM_KEY_ROTATOR_TABLE = os.environ.get('IAM_KEY_ROTATOR_TABLE', None)
@@ -18,6 +19,9 @@
# No. of days to wait for deleting existing key pair after a new key pair is generated
DELETE_AFTER_DAYS = os.environ.get('DELETE_AFTER_DAYS', 5)
+# Whether to share encrypted version of key pair
+ENCRYPT_KEY_PAIR = os.environ.get('ENCRYPT_KEY_PAIR', True)
+
# Mail client to use for sending new key creation or existing key deletion mail
MAIL_CLIENT = os.environ.get('MAIL_CLIENT', 'ses')
@@ -190,6 +194,9 @@ def mark_key_for_destroy(userName, ak, existingKeyDeleteAge, email):
},
'delete_on': {
'N': str(round(datetime(today.year, today.month, today.day, tzinfo=pytz.utc).timestamp()) + (existingKeyDeleteAge * 24 * 60 * 60))
+ },
+ 'delete_enc_key': {
+ 'S': 'Y' if ENCRYPT_KEY_PAIR else 'N'
}
}
)
@@ -217,7 +224,16 @@ def create_user_key(userName, user):
# Email keys to user
existingKeyDeleteAge = user['attributes']['delete_after_days'] if 'delete_after_days' in user['attributes'] else DELETE_AFTER_DAYS
- send_email(user['attributes']['email'], userName, resp['AccessKey']['AccessKeyId'], resp['AccessKey']['SecretAccessKey'], user['attributes']['instruction'], user['keys'][0]['ak'], int(existingKeyDeleteAge))
+
+ if ENCRYPT_KEY_PAIR:
+ userAccessKey, userSecretAccessKey = encryption.encrypt(userName, resp['AccessKey']['AccessKeyId'], resp['AccessKey']['SecretAccessKey'])
+ userInstruction = 'The above key pair is encrypted so you need to decrypt it using the encryption key stored in SSM parameter /ikr/secret/iam/{} before using the key pair. You can use the *decryption.py* file present in the *skildops/aws-iam-key-rotator* repo. {}'.format(userName, user['attributes']['instruction'])
+ else:
+ userAccessKey = resp['AccessKey']['AccessKeyId']
+ userSecretAccessKey = resp['AccessKey']['SecretAccessKey']
+ userInstruction = user['attributes']['instruction']
+
+ send_email(user['attributes']['email'], userName, userAccessKey, userSecretAccessKey, userInstruction, user['keys'][0]['ak'], int(existingKeyDeleteAge))
# Mark exisiting key to destory after X days
mark_key_for_destroy(userName, user['keys'][0]['ak'], int(existingKeyDeleteAge), user['attributes']['email'])
@@ -229,6 +245,9 @@ def create_user_keys(users):
[executor.submit(create_user_key, user, users[user]) for user in users]
def handler(event, context):
+ global ENCRYPT_KEY_PAIR
+ ENCRYPT_KEY_PAIR = False if ENCRYPT_KEY_PAIR == 'false' else True
+
if IAM_KEY_ROTATOR_TABLE is None:
logger.error('IAM_KEY_ROTATOR_TABLE is required. Current value: {}'.format(IAM_KEY_ROTATOR_TABLE))
elif MAIL_FROM is None:
diff --git a/src/destructor.py b/src/destructor.py
index de1fc1b..944ff37 100644
--- a/src/destructor.py
+++ b/src/destructor.py
@@ -11,7 +11,7 @@
IAM_KEY_ROTATOR_TABLE = os.environ.get('IAM_KEY_ROTATOR_TABLE', None)
# In case lambda fails to delete the old key, how long should it wait before the next try
-RETRY_AFTER_MINS = os.environ.get('RETRY_AFTER_MINS', 5)
+RETRY_AFTER_MINS = int(os.environ.get('RETRY_AFTER_MINS', 5))
# Mail client to use for sending new key creation or existing key deletion mail
MAIL_CLIENT = os.environ.get('MAIL_CLIENT', 'ses')
@@ -22,6 +22,7 @@
# AWS_REGION is by default available within lambda environment
iam = boto3.client('iam', region_name=os.environ.get('AWS_REGION'))
ses = boto3.client('ses', region_name=os.environ.get('AWS_REGION'))
+ssm = boto3.client('ssm', region_name=os.environ.get('AWS_REGION'))
dynamodb = boto3.client('dynamodb', region_name=os.environ.get('AWS_REGION'))
logger = logging.getLogger('destructor')
@@ -77,12 +78,26 @@ def send_email(email, userName, existingAccessKey):
except (Exception, ClientError) as ce:
logger.error('Failed to send mail to user {} ({}). Reason: {}'.format(userName, email, ce))
+def delete_encryption_key(userName):
+ try:
+ logger.info('Deleting encryption key for {} user'.format(userName))
+ ssm.delete_parameter(
+ Name='/ikr/secret/iam/{}'.format(userName)
+ )
+ logger.info('Encryption key deleted for {} user'.format(userName))
+ except ClientError as ce:
+ logger.error('Unable to delete encryption key for {} user. Reason: {}'.format(userName, ce))
+ return False
+
+ return True
+
def destroy_user_key(rec):
if rec['eventName'] == 'REMOVE':
key = rec['dynamodb']['OldImage']
userName = key['user']['S']
userEmail = key['email']['S']
accessKey = key['ak']['S']
+ delEncKey = key['delete_enc_key']['S']
try:
logger.info('Deleting access key {} assocaited with user {}'.format(accessKey, userName))
iam.delete_access_key(
@@ -91,14 +106,19 @@ def destroy_user_key(rec):
)
logger.info('Access Key {} has been deleted'.format(accessKey))
+ # Delete user encryption key stored in ssm
+ if delEncKey == 'Y':
+ encKeyDeleted = delete_encryption_key(userName)
+
# Send mail to user about key deletion
send_email(userEmail, userName, accessKey)
except (Exception, ClientError) as ce:
logger.error('Failed to delete access key {}. Reason: {}'.format(accessKey, ce))
logger.info('Adding access key {} back to the database'.format(accessKey))
+
dynamodb.put_item(
TableName=IAM_KEY_ROTATOR_TABLE,
- Key={
+ Item={
'user': {
'S': userName
},
@@ -110,6 +130,9 @@ def destroy_user_key(rec):
},
'delete_on': {
'N': str(int(key['delete_on']['N']) + (RETRY_AFTER_MINS * 60))
+ },
+ 'delete_enc_key': {
+ 'S': 'N' if delEncKey == 'Y' and encKeyDeleted else delEncKey
}
}
)
diff --git a/src/encryption.py b/src/encryption.py
new file mode 100644
index 0000000..968fba4
--- /dev/null
+++ b/src/encryption.py
@@ -0,0 +1,41 @@
+import boto3
+import logging
+import sys
+import os
+
+from cryptography.fernet import Fernet
+from botocore.exceptions import ClientError
+
+ssm = boto3.client('ssm', region_name=os.environ.get('AWS_REGION'))
+
+logger = logging.getLogger('encryption')
+logger.setLevel(logging.INFO)
+
+def store_in_ssm(userName, encryptionKey):
+ try:
+ logger.info('Creating SSM parameter to store encryption key for {} user'.format(userName))
+ ssm.put_parameter(
+ Name='/ikr/secret/iam/{}'.format(userName),
+ Description='Encryption key used to encrypt access key pair of {} user'.format(userName),
+ Value=encryptionKey,
+ Type='SecureString',
+ Overwrite=True
+ )
+ logger.info('SSM parameter created for {} user'.format(userName))
+ except ClientError as ce:
+ logger.error('Failed to create SSM parameter to store encryption key for {} user. Reason: {}'.format(userName, ce))
+ return False
+
+ return True
+
+def encrypt(userName, accessKey, secretAccessKey):
+ encryptionKey = Fernet.generate_key().decode('utf-8')
+
+ if not store_in_ssm(userName, encryptionKey):
+ sys.exit(1)
+
+ f = Fernet(encryptionKey)
+ encryptedAccessKey = f.encrypt(accessKey.encode('utf-8')).decode('utf-8')
+ encryptedSecretAccessKey = f.encrypt(secretAccessKey.encode('utf-8')).decode('utf-8')
+
+ return encryptedAccessKey, encryptedSecretAccessKey
diff --git a/src/shared_functions.py b/src/shared_functions.py
index 582408a..0ea789f 100644
--- a/src/shared_functions.py
+++ b/src/shared_functions.py
@@ -9,6 +9,6 @@
def fetch_account_info():
return {
- 'id': os.environ['AWS_ACCOUNT_ID'],
+ 'id': os.environ.get('ACCOUNT_ID', ''),
'name': iam.list_account_aliases()['AccountAliases'][0] if len(iam.list_account_aliases()['AccountAliases']) > 0 else ''
}
diff --git a/tag-iam-users.py b/tag-iam-users.py
index 28b2410..231d9e8 100644
--- a/tag-iam-users.py
+++ b/tag-iam-users.py
@@ -6,21 +6,21 @@
from botocore.exceptions import ClientError
# AWS Profile to use for API calls
-IKR_AWS_PROFILE = os.environ.get('IKR_AWS_PROFILE', None)
+AWS_PROFILE = os.environ.get('AWS_PROFILE', None)
# AWS Access Key to use for API calls
-IKR_AWS_ACCESS_KEY_ID = os.environ.get('IKR_AWS_ACCESS_KEY_ID', None)
+AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID', None)
# AWS Secret Access Key to use for API calls
-IKR_AWS_SECRET_ACCESS_KEY = os.environ.get('IKR_AWS_SECRET_ACCESS_KEY', None)
+AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY', None)
# AWS Session Token to use for API calls
-IKR_AWS_SESSION_TOKEN = os.environ.get('IKR_AWS_SESSION_TOKEN', None)
+AWS_SESSION_TOKEN = os.environ.get('AWS_SESSION_TOKEN', None)
# AWS region to use
-IKR_AWS_REGION = os.environ.get('IKR_AWS_REGION', 'us-east-1')
+AWS_REGION = 'us-east-1'
-session = boto3.Session(aws_access_key_id=IKR_AWS_ACCESS_KEY_ID, aws_secret_access_key=IKR_AWS_SECRET_ACCESS_KEY, aws_session_token=IKR_AWS_SESSION_TOKEN, region_name=IKR_AWS_REGION, profile_name=IKR_AWS_PROFILE)
+session = boto3.Session(aws_access_key_id=AWS_ACCESS_KEY_ID, aws_secret_access_key=AWS_SECRET_ACCESS_KEY, aws_session_token=AWS_SESSION_TOKEN, region_name=AWS_REGION, profile_name=AWS_PROFILE)
iam = session.client('iam')
iamUsers = json.load(open('iam-user-tags.json'))
diff --git a/terraform/README.md b/terraform/README.md
index 4c1865d..3c54226 100644
--- a/terraform/README.md
+++ b/terraform/README.md
@@ -1,6 +1,6 @@
# IAM Key Rotator
-![Test](https://img.shields.io/github/workflow/status/skildops/aws-iam-key-rotator/test/main?label=Test&style=for-the-badge) ![Checkov](https://img.shields.io/github/workflow/status/skildops/aws-iam-key-rotator/checkov/main?label=Checkov&style=for-the-badge)
+![Test](https://img.shields.io/github/actions/workflow/status/skildops/aws-iam-key-rotator/test.yml?branch=main&label=Test&style=for-the-badge) ![Checkov](https://img.shields.io/github/actions/workflow/status/skildops/aws-iam-key-rotator/checkov.yml?branch=main&label=Checkov&style=for-the-badge)
This terraform module will deploy the following services:
- DynamoDB Table
@@ -11,6 +11,11 @@ This terraform module will deploy the following services:
**Note:** You need to implement [remote backend](https://www.terraform.io/docs/language/settings/backends/index.html) by yourself and is recommended for state management.
+**Important:** `cryptography` library has issues with AWS lambda so use the below command to build the package whenever required
+```bash
+pip install --platform manylinux2014_x86_64 --implementation cp --python 3.9 --only-binary=:all: --target . cryptography
+```
+
## Requirements
| Name | Version |
@@ -44,6 +49,7 @@ This terraform module will deploy the following services:
| rotate_after_days | Days after which a new access key pair should be generated. **Note:** If `IKR:ROTATE_AFTER_DAYS` tag is set for the IAM user, this is ignored | `number` | `85` | no |
| delete_after_days | No. of days to wait for deleting existing key pair after a new key pair is generated. **Note:** If `IKR:DELETE_AFTER_DAYS` tag is set for the IAM user, this is ignored | `number` | `5` | no |
| retry_after_mins | In case lambda fails to delete the old key, how long should it wait before the next try | `number` | `5` | no |
+| encrypt_key_pair | Whether to share encrypted version of key pair with the user instead of sending them in plain text. The encryption key will be stored in SSM paramter store in `/ikr/secret/iam/USERNAME` format | `bool` | `true` | no |
| mail_client | Mail client to use. **Supported Clients:** smtp, ses and mailgun | `string` | `"ses"` | no |
| mail_from | Email address which should be used for sending mails. **Note:** Prior setup of mail client is required | `string` | n/a | yes |
| smtp_protocol | Security protocol to use for SMTP connection. **Supported values:** ssl and tls. **Note:** Required if mail client is set to smtp | `string` | `null` | no |
diff --git a/terraform/archive.tf b/terraform/archive.tf
index 0ec848b..20efe3b 100644
--- a/terraform/archive.tf
+++ b/terraform/archive.tf
@@ -1,48 +1,28 @@
-data "template_file" "creator" {
- template = file("../src/creator.py")
-}
-
-data "template_file" "destructor" {
- template = file("../src/destructor.py")
-}
-
-data "template_file" "shared_functions" {
- template = file("../src/shared_functions.py")
-}
-
-data "template_file" "ses_mailer" {
- template = file("../src/ses_mailer.py")
-}
-
-data "template_file" "mailgun_mailer" {
- template = file("../src/mailgun_mailer.py")
-}
-
-data "template_file" "smtp_mailer" {
- template = file("../src/smtp_mailer.py")
-}
-
data "archive_file" "creator" {
type = "zip"
output_path = "${path.module}/creator.zip"
source {
- content = data.template_file.creator.rendered
+ content = file("../src/creator.py")
filename = "creator.py"
}
source {
- content = data.template_file.shared_functions.rendered
+ content = file("../src/shared_functions.py")
filename = "shared_functions.py"
}
source {
- content = data.template_file.ses_mailer.rendered
+ content = file("../src/encryption.py")
+ filename = "encryption.py"
+ }
+ source {
+ content = file("../src/ses_mailer.py")
filename = "ses_mailer.py"
}
source {
- content = data.template_file.mailgun_mailer.rendered
+ content = file("../src/mailgun_mailer.py")
filename = "mailgun_mailer.py"
}
source {
- content = data.template_file.smtp_mailer.rendered
+ content = file("../src/smtp_mailer.py")
filename = "smtp_mailer.py"
}
}
@@ -51,23 +31,23 @@ data "archive_file" "destructor" {
type = "zip"
output_path = "${path.module}/destructor.zip"
source {
- content = data.template_file.destructor.rendered
+ content = file("../src/destructor.py")
filename = "destructor.py"
}
source {
- content = data.template_file.shared_functions.rendered
+ content = file("../src/shared_functions.py")
filename = "shared_functions.py"
}
source {
- content = data.template_file.ses_mailer.rendered
+ content = file("../src/ses_mailer.py")
filename = "ses_mailer.py"
}
source {
- content = data.template_file.mailgun_mailer.rendered
+ content = file("../src/mailgun_mailer.py")
filename = "mailgun_mailer.py"
}
source {
- content = data.template_file.smtp_mailer.rendered
+ content = file("../src/smtp_mailer.py")
filename = "smtp_mailer.py"
}
}
diff --git a/terraform/cryptography.zip b/terraform/cryptography.zip
new file mode 100644
index 0000000..0ecd4ca
Binary files /dev/null and b/terraform/cryptography.zip differ
diff --git a/terraform/main.tf b/terraform/main.tf
index 69d888d..e1d0500 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -59,20 +59,27 @@ resource "aws_dynamodb_table" "iam_key_rotator" {
# ====== Lambda Layers =====
resource "aws_lambda_layer_version" "pytz" {
- filename = "pytz.zip"
- source_code_hash = filebase64sha256("pytz.zip")
- description = "https://pypi.org/project/pytz/"
- layer_name = "pytz"
-
+ filename = "pytz.zip"
+ source_code_hash = filebase64sha256("pytz.zip")
+ description = "https://pypi.org/project/pytz/"
+ layer_name = "pytz"
compatible_runtimes = ["python3.6", "python3.7", "python3.8", "python3.9"]
+
}
resource "aws_lambda_layer_version" "requests" {
- filename = "requests.zip"
- source_code_hash = filebase64sha256("requests.zip")
- description = "https://pypi.org/project/requests/"
- layer_name = "requests"
+ filename = "requests.zip"
+ source_code_hash = filebase64sha256("requests.zip")
+ description = "https://pypi.org/project/requests/"
+ layer_name = "requests"
+ compatible_runtimes = ["python3.6", "python3.7", "python3.8", "python3.9"]
+}
+resource "aws_lambda_layer_version" "cryptography" {
+ filename = "cryptography.zip"
+ source_code_hash = filebase64sha256("cryptography.zip")
+ description = "https://cryptography.io/en/latest/"
+ layer_name = "cryptography"
compatible_runtimes = ["python3.6", "python3.7", "python3.8", "python3.9"]
}
@@ -84,48 +91,61 @@ resource "aws_iam_role" "iam_key_creator" {
tags = var.tags
}
-resource "aws_iam_role_policy" "iam_key_creator_policy" {
- name = "${var.key_creator_role_name}-policy"
- role = aws_iam_role.iam_key_creator.id
-
- policy = <<-EOF
- {
- "Version": "2012-10-17",
- "Statement": [
- {
- "Action": [
- "iam:ListUserTags",
- "iam:ListAccessKeys",
- "iam:ListUsers",
- "iam:CreateAccessKey"
- ],
- "Effect": "Allow",
- "Resource": "*"
- },
- {
- "Action": [
- "dynamodb:PutItem"
- ],
- "Effect": "Allow",
- "Resource": "${aws_dynamodb_table.iam_key_rotator.arn}"
- },
- {
- "Action": [
- "ssm:GetParameter"
- ],
- "Effect": "Allow",
- "Resource": "arn:aws:ssm:${var.region}:${local.account_id}:parameter/iakr/*"
- },
- {
- "Action": [
- "ses:SendEmail"
- ],
- "Effect": "Allow",
- "Resource": "*"
- }
+data "aws_iam_policy_document" "iam_key_creator_policy" {
+ # checkov:skip=CKV_AWS_109: Ensure IAM policies does not allow permissions management / resource exposure without constraints
+ # checkov:skip=CKV_AWS_110: Ensure IAM policies does not allow privilege escalation
+ # checkov:skip=CKV_AWS_107: Ensure IAM policies does not allow credentials exposure
+ statement {
+ effect = "Allow"
+ actions = [
+ "iam:ListUserTags",
+ "iam:ListAccessKeys",
+ "iam:ListUsers",
+ "iam:CreateAccessKey",
+ "iam:ListAccountAliases"
+ ]
+ resources = ["*"]
+ }
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "dynamodb:PutItem"
]
+ resources = [aws_dynamodb_table.iam_key_rotator.arn]
}
- EOF
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "ssm:GetParameter"
+ ]
+ resources = ["arn:aws:ssm:${var.region}:${local.account_id}:parameter/ikr/*"]
+ }
+
+ dynamic "statement" {
+ for_each = var.encrypt_key_pair == true ? [0] : []
+ content {
+ effect = "Allow"
+ actions = ["ssm:PutParameter"]
+ resources = ["arn:aws:ssm:${var.region}:${local.account_id}:parameter/ikr/*"]
+ }
+ }
+
+ dynamic "statement" {
+ for_each = var.mail_client == "ses" ? [0] : []
+ content {
+ effect = "Allow"
+ actions = ["ses:SendEmail"]
+ resources = ["*"]
+ }
+ }
+}
+
+resource "aws_iam_role_policy" "iam_key_creator_policy" {
+ name = "${var.key_creator_role_name}-policy"
+ role = aws_iam_role.iam_key_creator.id
+ policy = data.aws_iam_policy_document.iam_key_creator_policy.json
}
resource "aws_iam_role_policy_attachment" "iam_key_creator_logs" {
@@ -134,11 +154,11 @@ resource "aws_iam_role_policy_attachment" "iam_key_creator_logs" {
}
resource "aws_cloudwatch_event_rule" "iam_key_creator" {
- name = "IAMAccessKeyCreator"
- description = "Triggers a lambda function periodically which creates a set of new access key pair for a user if the existing key pair is X days old"
- is_enabled = true
-
+ name = "IAMAccessKeyCreator"
+ description = "Triggers a lambda function periodically which creates a set of new access key pair for a user if the existing key pair is X days old"
+ is_enabled = true
schedule_expression = "cron(${var.cron_expression})"
+ tags = var.tags
}
resource "aws_cloudwatch_event_target" "iam_key_creator" {
@@ -157,7 +177,7 @@ resource "aws_lambda_permission" "iam_key_creator" {
resource "aws_ssm_parameter" "mailgun" {
count = var.mail_client == "mailgun" ? 1 : 0
- name = "/iakr/secret/mailgun"
+ name = "/ikr/secret/mailgun"
value = var.mailgun_api_key
type = "SecureString"
tags = var.tags
@@ -165,7 +185,7 @@ resource "aws_ssm_parameter" "mailgun" {
resource "aws_ssm_parameter" "smtp_password" {
count = var.mail_client == "smtp" ? 1 : 0
- name = "/iakr/secret/smtp"
+ name = "/ikr/secret/smtp"
value = var.smtp_password
type = "SecureString"
tags = var.tags
@@ -184,6 +204,7 @@ resource "aws_lambda_function" "iam_key_creator" {
# checkov:skip=CKV_AWS_116: DLQ not required
# checkov:skip=CKV_AWS_117: VPC deployment not required
# checkov:skip=CKV_AWS_173: By default environment variables are encrypted at rest
+ # checkov:skip=CKV_AWS_272: Code-signing not required
function_name = var.key_creator_function_name
description = "Create new access key pair for IAM user"
role = aws_iam_role.iam_key_creator.arn
@@ -196,7 +217,7 @@ resource "aws_lambda_function" "iam_key_creator" {
timeout = var.function_timeout
reserved_concurrent_executions = var.reserved_concurrent_executions
- layers = [aws_lambda_layer_version.pytz.arn, aws_lambda_layer_version.requests.arn]
+ layers = [aws_lambda_layer_version.pytz.arn, aws_lambda_layer_version.requests.arn, aws_lambda_layer_version.cryptography.arn]
tracing_config {
mode = var.xray_tracing_mode
@@ -207,6 +228,7 @@ resource "aws_lambda_function" "iam_key_creator" {
IAM_KEY_ROTATOR_TABLE = aws_dynamodb_table.iam_key_rotator.name
ROTATE_AFTER_DAYS = var.rotate_after_days
DELETE_AFTER_DAYS = var.delete_after_days
+ ENCRYPT_KEY_PAIR = var.encrypt_key_pair
MAIL_CLIENT = var.mail_client
MAIL_FROM = var.mail_from
SMTP_PROTOCOL = var.smtp_protocol
@@ -230,60 +252,60 @@ resource "aws_iam_role" "iam_key_destructor" {
tags = var.tags
}
-resource "aws_iam_role_policy" "iam_key_destructor_policy" {
- name = "${var.key_destructor_role_name}-policy"
- role = aws_iam_role.iam_key_destructor.id
-
- policy = <<-EOF
- {
- "Version": "2012-10-17",
- "Statement": [
- {
- "Action": [
- "iam:DeleteAccessKey"
- ],
- "Effect": "Allow",
- "Resource": "*"
- },
- {
- "Action": [
- "dynamodb:PutItem"
- ],
- "Effect": "Allow",
- "Resource": [
- "${aws_dynamodb_table.iam_key_rotator.arn}"
- ]
- },
- {
- "Action": [
- "dynamodb:DescribeStream",
- "dynamodb:GetRecords",
- "dynamodb:GetShardIterator",
- "dynamodb:ListShards",
- "dynamodb:ListStreams"
- ],
- "Effect": "Allow",
- "Resource": [
- "${aws_dynamodb_table.iam_key_rotator.stream_arn}"
- ]
- },
- {
- "Action": [
- "ssm:GetParameter"
- ],
- "Effect": "Allow",
- "Resource": "arn:aws:ssm:${var.region}:${local.account_id}:parameter/iakr/*"
- },
- {
- "Action": [
- "ses:SendEmail"
- ],
- "Effect": "Allow",
- "Resource": "*"
- }
+data "aws_iam_policy_document" "iam_key_destructor_policy" {
+ # checkov:skip=CKV_AWS_109: Ensure IAM policies does not allow permissions management / resource exposure without constraints
+ statement {
+ effect = "Allow"
+ actions = [
+ "iam:DeleteAccessKey",
+ "iam:ListAccountAliases"
+ ]
+ resources = ["*"]
+ }
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "dynamodb:PutItem"
+ ]
+ resources = [aws_dynamodb_table.iam_key_rotator.arn]
+ }
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "dynamodb:DescribeStream",
+ "dynamodb:GetRecords",
+ "dynamodb:GetShardIterator",
+ "dynamodb:ListShards",
+ "dynamodb:ListStreams"
]
+ resources = [aws_dynamodb_table.iam_key_rotator.stream_arn]
+ }
+
+ dynamic "statement" {
+ for_each = var.encrypt_key_pair == true ? [0] : []
+ content {
+ effect = "Allow"
+ actions = ["ssm:DeleteParameter"]
+ resources = ["arn:aws:ssm:${var.region}:${local.account_id}:parameter/ikr/secret/iam/*"]
+ }
}
- EOF
+
+ dynamic "statement" {
+ for_each = var.mail_client == "ses" ? [0] : []
+ content {
+ effect = "Allow"
+ actions = ["ses:SendEmail"]
+ resources = ["*"]
+ }
+ }
+}
+
+resource "aws_iam_role_policy" "iam_key_destructor_policy" {
+ name = "${var.key_destructor_role_name}-policy"
+ role = aws_iam_role.iam_key_destructor.id
+ policy = data.aws_iam_policy_document.iam_key_destructor_policy.json
}
resource "aws_iam_role_policy_attachment" "iam_key_destructor_logs" {
@@ -292,9 +314,10 @@ resource "aws_iam_role_policy_attachment" "iam_key_destructor_logs" {
}
resource "aws_lambda_event_source_mapping" "iam_key_destructor" {
- event_source_arn = aws_dynamodb_table.iam_key_rotator.stream_arn
- function_name = aws_lambda_function.iam_key_destructor.arn
- starting_position = "LATEST"
+ event_source_arn = aws_dynamodb_table.iam_key_rotator.stream_arn
+ function_name = aws_lambda_function.iam_key_destructor.arn
+ starting_position = "LATEST"
+ maximum_retry_attempts = 0
}
resource "aws_cloudwatch_log_group" "iam_key_destructor" {
@@ -310,6 +333,7 @@ resource "aws_lambda_function" "iam_key_destructor" {
# checkov:skip=CKV_AWS_116: DLQ not required
# checkov:skip=CKV_AWS_117: VPC deployment not required
# checkov:skip=CKV_AWS_173: By default environment variables are encrypted at rest
+ # checkov:skip=CKV_AWS_272: Code-signing not required
function_name = var.key_destructor_function_name
description = "Delete existing access key pair for IAM user"
role = aws_iam_role.iam_key_destructor.arn
diff --git a/terraform/vars.tf b/terraform/vars.tf
index 327490d..2cdcf68 100644
--- a/terraform/vars.tf
+++ b/terraform/vars.tf
@@ -136,6 +136,12 @@ variable "retry_after_mins" {
description = "In case lambda fails to delete the old key, how long should it wait before the next try"
}
+variable "encrypt_key_pair" {
+ type = bool
+ default = true
+ description = "Whether to share encrypted version of key pair with the user instead of sending them in plain text. The encryption key will be stored in SSM paramter store in `/ikr/secret/iam/USERNAME` format"
+}
+
variable "mail_client" {
type = string
default = "ses"