AWS Lambda Function: IAM User Password Expiry Notice | SES, Boto3 & Terraform

Overview

In this implementation, you'll be guided through the necessary steps to set up an AWS Lambda function to email notifications to IAM Users when their AWS Web Console passwords are expiring. The function is written in Python (boto3) and integrated with AWS SES using a verified domain. Terraform code samples are provided for all of the infrastructure configuration steps.

The purpose of this initiative is to provide proactive security measures while streamlining administrative tasks by sending an advanced notice to AWS IAM users that their passwords are expiring. By promptly alerting users before their passwords reach the expiration threshold defined in the security policy, you can facilitate regular password resets, ensuring stronger security measures are maintained. This approach reduces the administrative burden on administrators, as users can self-reset their passwords in advance of expiration. Additionally, it empowers users to take ownership of their account security, promoting a culture of proactive password management.

With this implementation, you will enhance the overall security, efficiency, and user experience within your AWS IAM environment.

Pre-requisites

  • Domain ownership and access to manage DNS records

    • Domain jennasrunbooks.com will be used as an example in the below sample code output. Update this with your appropriate domain.
  • Terraform installed

  • Install Python, boto3 and other script modules as needed to review the code locally without errors

  • Clone repo containing Lambda Python script

  • AWS IAM permissions to deploy Terraform resources

  • Email tags assigned to each AWS IAM User, sample Terraform code from a terraform.tfvars file:

      user_names = {
        "user1" = {
          "name" = "user1",
          "tag"  = { email = "user1@jennasrunbooks.com", role = "engineering" }
        }
      }
    

Procedure

Set up and verify the email domain

  • Register and verify your email domain in the Amazon SES console to establish your email sender identity.

  • If you'd like to use a custom MAIL FROM domain, check out this AWS doc and note the following:

    Important
    To successfully set up a custom MAIL FROM domain with Amazon SES, you must publish exactly one MX record to the DNS server of your MAIL FROM domain. If the MAIL FROM domain has multiple MX records, the custom MAIL FROM setup with Amazon SES will fail.
  • Sample Terraform code to deploy an AWS SES domain identity using Easy DKIM settings and a custom MAIL FROM domain:

      # Update the domain variable to your domain
      # using the SES service which is integrated into other services ie Lambda
    
      variable "domain" {
        type    = string
        default = "jennasrunbooks.com"
      }
    
      resource "aws_ses_domain_identity" "ses_domain" {
        domain = var.domain
      }
    
      resource "aws_ses_domain_mail_from" "main" {
        domain           = aws_ses_domain_identity.ses_domain.domain
        mail_from_domain = "mail.${var.domain}"
      }
    
      resource "aws_ses_domain_identity_verification" "email_identity_verification" {
        domain = aws_ses_domain_identity.ses_domain.domain
      }
    
    • The initial terraform apply will likely timeout on the aws_ses_domain_identity_verification resource depending on how quickly your DNS provider can propagate the changes to verify the domain identity with AWS. You can simply rerun an apply to update the state once the domain has been verified.

    • Update your DNS provider records for your domain with the DKIM CNAME and MAIL FROM MX and SPF records provided by AWS as shown in this example after running terraform apply:

    • You have the option to download a csv of each record set which can be useful if another team or 3rd party manages the records for your domain.

    • DNS propagation can take anywhere from a few minutes to several hours. If entered correctly, the SES page for the domain verification will display the identity status as Verified. Once verified, you'll also receive an email from AWS for each configuration type you've configured.

Write the Lambda function Python script

  • Link to the script: https://github.com/jksprattler/aws-security/blob/main/lambda/password_notification/password_notification.py

  • Only IAM users with valid email tags using the verified domain in their address will be notified.

  • Update the SES email body contents with an appropriate message for your audience.

  • Include a direct link to the account's AWS console sign-in page. If you have multiple accounts used in your infrastructure, it can also be helpful to specify to your clients which account the password reset is for by providing both the friendly name and account# in the message body.

  • Update the Source email account in the ses_client.send_email method as appropriate to your cloud admins team Distribution List.

Configure the Lambda IAM policy and role

  • Create an IAM policy that includes the necessary permissions for the Lambda function to generate and get the credential report, send emails using SES, and access user tags (we specifically need the email tags for each user).

  • Give the policy permissions to create the log group, and log streams and push log events to the streams. This will be used by CloudWatch when it's triggered on the scheduled event.

  • Create an IAM role and attach the IAM policy to it. This role will be assumed by the Lambda function to access the required AWS services.

  • Sample Terraform code to create the IAM policy, role and policy attachment needed for the lambda function:

      # Update Resource ARN references to region, accountID and aws lambda log-group name as needed for your environment
    
      resource "aws_iam_policy" "lambda_password_notification_policy" {
        name   = "lambda_password_notification_policy"
        policy = <<EOF
          {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                  "iam:GenerateCredentialReport",
                  "ses:SendEmail",
                  "ses:SendRawEmail",
                  "iam:GetCredentialReport",
                  "iam:ListUserTags"
                ],
                "Resource": "*"
              },
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup"
                ],
                "Resource": "arn:aws:logs:region:accountID:*"
              }
            ]
          },
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": "arn:aws:logs:region:accountID:log-group:/aws/lambda/password_notification:*"
              }
            ]
          }
          EOF
        tags   = var.common_tags
      }
    
      resource "aws_iam_role" "lambda_password_notification_role" {
        name               = "lambda_password_notification_role"
        assume_role_policy = <<EOF
          {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Sid": "",
                "Effect": "Allow",
                "Principal": {
                  "Service": "lambda.amazonaws.com"
                },
                "Action": "sts:AssumeRole"
              }
            ]
          }
          EOF
      }
    
      resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
        role       = aws_iam_role.lambda_password_notification_role.name
        policy_arn = aws_iam_policy.lambda_password_notification_policy.arn
      }
    

Setup the Lambda function

  • Create the new Lambda function with Python (boto3) to generate SES email notifications of expired passwords by generating and parsing the IAM credentials report using Terraform to deploy the resources.

  • Python 3.8 will be used for the runtime

  • The handler will use the name of the function, ie: password_notification.lambda_handler

  • The previously created IAM role will be assigned to the Lambda function

  • The Python script will be zipped using the Terraform archive_file data source

  • The source_code_hash argument will be applied to the aws_lambda_function resource block to trigger updates in the terraform state when changes are made to the Python script.

  • Sample Terraform code:

      # Update below file paths according to your directory architecture
    
      data "aws_lambda_function" "password_notification" {
        function_name = aws_lambda_function.password_notification.function_name
      }
    
      data "archive_file" "password_notification_zip" {
        type        = "zip"
        source_dir  = "src_dir/scripts/aws/lambda/password_notification/"
        output_path = "dest_dir/scripts/aws/lambda/password_notification/password_notification.zip"
      }
    
      resource "aws_lambda_function" "password_notification" {
        filename         = "dest_dir/scripts/aws/lambda/password_notification/password_notification.zip"
        source_code_hash = data.archive_file.password_notification_zip.output_base64sha256
        function_name    = "password_notification"
        description      = "lambda function to send email notifications (SES) to users when passwords are expiring"
        role             = aws_iam_role.lambda_password_notification_role.arn
        handler          = "password_notification.lambda_handler"
        runtime          = "python3.8"
        timeout          = 180
        memory_size      = 128
        depends_on = [
          aws_iam_role_policy_attachment.lambda_policy_attachment,
          aws_cloudwatch_log_group.password_notification_log_group,
        ]
      }
    

Test the Lambda function

  • Test the Lambda function manually by triggering it with sample input data to ensure it sends the correct email notifications.

  • Use the existing lambda function deployed previously or create a new one using the same settings, IAM role, etc for this test.

  • Use the Lambda console to manually test the function.

  • Example updated Lambda Python to force a test email notification using custom JSON event values:

      """
      Jenna Sprattler | SRE Kentik | 2023-05-21
      Test lambda function to send email notifications (SES) to specified test user that passwords are expiring
      """
      import time
      from datetime import datetime
      from dateutil import parser
      import boto3
    
      iam_client = boto3.client('iam')
      ses_client = boto3.client('ses')
    
      def lambda_handler(event, context):
          username = event['username']
          password_last_changed = event['password_last_changed']
          email = event['email']
    
          if password_last_changed not in ('N/A', 'not_supported'):
              # Parse the date and time components from the timestamp
              password_last_changed_date = parser.parse(password_last_changed)
              days_since_password_change = (
                  datetime.now() - password_last_changed_date.replace(tzinfo=None)).days
              if days_since_password_change > 78:
                  message = f'''
                      <html>
                      <body>
                      <p>Hello {username},</p>
                      <p>Your password to access the <a href="https://signin.aws.amazon.com/console">AWS web console</a> has expired or will be expiring within the next 12 days.</p>
                      <p>If your password is still valid, please log into the web console and follow the banner instructions to reset your password now.</p>
                      <p>If you have API access keys configured, you can use the Password Reset Self Service <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_password_reset.py">script</a>.</p>
                      <p>If you don't use API keys, and your password has passed the expiration date then reply to this email and we'll assist with your password reset.</p>
                      <p>If you don't use your AWS account, reply to this email and we'll remove your account.</p>
                      <br>
                          <p>Thank you,</p>
                          <p>Cloud Admins</p>
                      </body>
                      </html>
                      '''
                  ses_client.send_email(
                      Source='cloud-admins@jennasrunbooks.com',
                      Destination={'ToAddresses': [email]},
                      Message={
                          'Subject': {'Data': 'AWS Password Expiry Notification'},
                          'Body': {'Html': {'Data': message}}
                      }
                  )
          return 'Password expiry notifications sent to: ' + username
    
    • Add appropriate JSON test event values to your variables and generate a test SES email notification, for example:

        {
          "username": "user1",
          "password_last_changed": "2023-02-10T00:00:00Z",
          "email": "user1@jennasrunbooks.com"
        }
      
    • Validate that the test email was received by the test user's email account. Review the message subject and body contents for accuracy. Sample test email to my AWS IAM user1:

    • Feel free to use the Password Reset Self-Service script referenced in the above email body.

    • For users with passwords passed the expiration date, check out my Administrative user password reset script.

Setup the CloudWatch event rule

  • Create the CloudWatch Event Rule to trigger the desired schedule for automating the password expiration notification Lambda function which contains the SES send_email operation to your users with expiring passwords.

  • Set the target of the CloudWatch Event Rule to the Lambda function you created.

  • (Optional) Create the log group for the CloudWatch event log streams. This is created automatically whenever a new Lambda function is deployed however, if/when you destroy your Lambda function in the future, Terraform will not automatically delete the log group that was created. If the log group resource is managed by the Terraform state then you will know to delete that resource and have cleaner infrastructure should the Lambda function be removed in the future.

  • There is a hidden Terraform resource called aws_lambda_permission that needs to be created. This is created automatically when a Lambda function is created using the AWS console. This allows the CloudWatch events permissions to access the Lambda function. This is needed for the CloudWatch event rule (cron scheduler), log streams, etc to work properly with the function.

  • For testing purposes, you could update the schedule_expression of the event rule to run every 10 min while confirming that the Lambda function and Cloudwatch settings are working properly then update to a daily interval: rate(10 minutes)

  • Sample Terraform code:

      resource "aws_lambda_permission" "allow_cloudwatch_for_password_notification" {
        statement_id  = "AllowExecutionFromCloudWatch"
        action        = "lambda:InvokeFunction"
        function_name = aws_lambda_function.password_notification.function_name
        principal     = "events.amazonaws.com"
        source_arn    = aws_cloudwatch_event_rule.password_notification_schedule.arn
      }
    
      resource "aws_cloudwatch_log_group" "password_notification_log_group" {
        name              = "/aws/lambda/password_notification"
        retention_in_days = 7
      }
    
      resource "aws_cloudwatch_event_rule" "password_notification_schedule" {
        name                = "password_notification_schedule"
        description         = "Scheduled rule for password notification"
        schedule_expression = "cron(0 13 * * ? *)" # Schedule for 8am CST / 1pm UTC daily (adjust as needed)
      }
    
      resource "aws_cloudwatch_event_target" "password_notification_target" {
        rule = aws_cloudwatch_event_rule.password_notification_schedule.name
        arn  = data.aws_lambda_function.password_notification.arn
      }
    

Validate functionality

  • Monitor the CloudWatch logs in the designated Lambda function's log group and check for any errors or unexpected behavior. Example of a successful run:

  • Verify that the Lambda function is generating the credential report and sending the email notifications to the intended recipients.

  • You should see an IAM credentials report created shortly after the Lambda function's scheduled event, for example:

  • You can download this report and sort by the password_last_changed column to capture a list of users that should have received the SES notification. Reach out to these users to confirm whether or not they received the email notification.

Conclusion

This implementation provides a comprehensive solution for notifying AWS IAM users of their upcoming password expirations for accessing the AWS web console. With this proactive approach to security, password hygiene is prioritized while also helping reduce the risk of compromised credentials.

Empowering users to self-reset their passwords before expiration not only enhances account security but also streamlines administrative tasks. This implementation showcases the value of leveraging automation and user-centric design to create a more secure and efficient AWS IAM environment. With this solution, organizations can foster a culture of proactive password management.

Resources