Automating Rotation of IAM User Access and Secret Keys

Benhur P
8 min readAug 22, 2020

--

In this post, I will explain how AWS Identity and Access Management (IAM) user access keys and secrets may be stored in AWS Secrets Manager and rotated automatically using AWS Lambda at deterministic intervals.

Components and Workflow

Let’s look at the components required to incorporate automatic IAM access key rotation into your IAM operations workflow:

  • AWS Lambda generates API calls to IAM services to rotate and update keys.
  • Amazon CloudWatch initiates events on a scheduled basis to rotate keys.
  • AWS IAM provides user access and secret keys for accessing AWS resources and services from non-AWS systems.
  • AWS Secrets Manager stores secrets that are consumed by application or content developers.
  • Amazon Simple Notification Service (SNS) sends notifications whenever keys are changed.

Once the components — IAM user with API key or secret, AWS Lambda functions, Amazon CloudWatch Events, AWS Secrets Manager secrets — are created, the solution follows these steps:

  1. Every 90 days, a Lambda function creates a new key and uploads it as a current key in AWS Secrets Manager.
  2. SNS updates the application owner that a key has been rotated. Applications can use the AWS Secrets Manager API to retrieve the current key instead of hard coding in the application.
  3. After 100 days, the same Lambda function disables the old keys and a notification is sent to the user.
  4. After 110 days, the old keys are deleted and notifications are sent to the user again.

You can opt to rotate the keys every day and schedule the Lambda function every day, or you can schedule it at your convenience.

Setting Up the Key Rotation

In this post, we will build automated key rotation based on the following policies:

  • All IAM users have to use new access key and secret key every 90 days.
  • Deactivate previous access key and secret key every 100 days.
  • Delete the previous access key and secret key every 110 days.

The keys lifecycle length can be customized based on your organization’s need by modifying the CloudWatch Event rules. In the next steps, we’ll create our Lambda function and integrate it with CloudWatch Event Rules.

Creating AWS Lambda Functions

Create Python 3.6 Lambda functions with a lambda_basic_execution role (or any other Lambda service role) that you decide should have permission to modify secrets and IAM user access and keys. Let’s name the Lambda function AutomatedKeyRotation for this example.

  1. create a lambda function using code provided below
  2. Create users using json file
import json
import boto3
import base64
import datetime
import os
from datetime import date
from botocore.exceptions import ClientError
iam = boto3.client('iam')
secretmanager = boto3.client('secretsmanager')
#IAM_UserName=os.environ['IAM_UserName']
#SecretName=os.environ['SecretName']

def create_key(uname):
try:
IAM_UserName=uname
response = iam.create_access_key(UserName=IAM_UserName)
AccessKey = response['AccessKey']['AccessKeyId']
SecretKey = response['AccessKey']['SecretAccessKey']
json_data=json.dumps({'AccessKey':AccessKey,'SecretKey':SecretKey})
secmanagerv=secretmanager.put_secret_value(SecretId=IAM_UserName,SecretString=json_data)
emailmsg="New "+AccessKey+" has been create. Please get the secret key value from secret manager"
ops_sns_topic ='arn:aws:sns:us-east-1:<accountnumber>:SecManagerKeyRotation'
sns_send_report = boto3.client('sns',region_name='us-east-1')
sns_send_report.publish(TopicArn=ops_sns_topic, Message=emailmsg, Subject="New Key created for user"+ IAM_UserName)
except ClientError as e:
print (e)

def deactive_key(uname):
try:
#GET PREVIOUS AND CURRENT VERSION OF KEY FROM SECRET MANAGER
IAM_UserName=uname
getpresecvalue=secretmanager.get_secret_value(SecretId=IAM_UserName,VersionStage='AWSPREVIOUS')
#getcursecvalue=secretmanager.get_secret_value(SecretId='secmanager3',VersionStage='AWSCURRENT')
#print (getpresecvalue)
#print (getcursecvalue)
preSecString = json.loads(getpresecvalue['SecretString'])
preAccKey=preSecString['AccessKey']
#GET CREATION DATE OF CURRENT VERSION OF ACCESS KEY
#curdate=getcursecvalue['CreatedDate']
#GET TIMEZONE FROM CREATION DATE
#tz=curdate.tzinfo
#CALCULATE TIME DIFFERENCE BETWEEN CREATION DATE AND TODAY
#diff=datetime.datetime.now(tz)-curdate
#diffdays=diff.days
#print (curdate)
#print (tz)
#print (diffdays)
#print (preAccKey)
#IF TIME DIFFERENCE IS MORE THAN x NUMBER OF DAYS THEN DEACTIVATE PREVIOUS KEY AND SEND A MESSAGE
#if diffdays >= 1:
iam.update_access_key(AccessKeyId=preAccKey,Status='Inactive',UserName=IAM_UserName)
emailmsg="PreviousKey "+preAccKey+" has been disabled for IAM User"+IAM_UserName
ops_sns_topic ='arn:aws:sns:us-east-1:<accountnumber>:SecManagerKeyRotation'
sns_send_report = boto3.client('sns',region_name='us-east-1')
sns_send_report.publish(TopicArn=ops_sns_topic, Message=emailmsg, Subject='Previous Key Deactivated')
return
except ClientError as e:
print (e)
#else:
# print ("Current Key is not older than 10 days")
#print (datediff)

def delete_key(uname):
try:
IAM_UserName=uname
print (IAM_UserName)
getpresecvalue=secretmanager.get_secret_value(SecretId=IAM_UserName,VersionStage='AWSPREVIOUS')
#getcursecvalue=secretmanager.get_secret_value(SecretId='secmanager3',VersionStage='AWSCURRENT')
preSecString = json.loads(getpresecvalue['SecretString'])
preAccKey=preSecString['AccessKey']
#print (preAccKey)
#GET CREATION DATE OF CURRENT VERSION OF ACCESS KEY
#curdate=getcursecvalue['CreatedDate']
#GET TIMEZONE FROM CREATION DATE
#tz=curdate.tzinfo
#CALCULATE TIME DIFFERENCE BETWEEN CREATION DATE AND TODAY
#diff=datetime.datetime.now(tz)-curdate
#diffdays=diff.days
#IF TIME DIFFERENCE IS MORE THAN x NUMBER OF DAYS THEN DEACTIVATE PREVIOUS KEY AND SEND A MESSAGE
#if diffdays >= 1:
keylist=iam.list_access_keys (UserName=IAM_UserName)
#print (keylist)
for x in range(2):
prevkeystatus=keylist['AccessKeyMetadata'][x]['Status']
preacckeyvalue=keylist['AccessKeyMetadata'][x]['AccessKeyId']
print (prevkeystatus)
if prevkeystatus == "Inactive":
if preAccKey==preacckeyvalue:
print (preacckeyvalue)
iam.delete_access_key (UserName=IAM_UserName,AccessKeyId=preacckeyvalue)
emailmsg="PreviousKey "+preacckeyvalue+" has been deleted for user"+IAM_UserName
ops_sns_topic ='arn:aws:sns:us-east-1:<accountnumber>:SecManagerKeyRotation'
sns_send_report = boto3.client('sns',region_name='us-east-1')
sns_send_report.publish(TopicArn=ops_sns_topic, Message=emailmsg, Subject='Previous Key has been deleted')
return
else:
print ("secret manager previous value doesn't match with inactive IAM key value")
else:
print ("previous key is still active")
return
except ClientError as e:
print (e)
#else:
#print ("Current Key is not older than 10 days")

def lambda_handler(event, context):
# TODO implement
faction=event ["action"]
fuser_name=event ["username"]
if faction == "create":
status = create_key(fuser_name)
print (status)
elif faction == "deactivate":
status = deactive_key(fuser_name)
print (status)
elif faction == "delete":
status = delete_key(fuser_name)
print (status)

Userid_json

{
"AWSTemplateFormatVersion" : "2010-09-09",

"Description" : "Template to create Cloudwatch events to call lambda function for every user created",

"Parameters" : {
"Password": {
"NoEcho": "true",
"Type": "String",
"Description" : "New account password",
"MinLength": "1",
"MaxLength": "41",
"ConstraintDescription" : "the password must be between 1 and 41 characters"
},
"UName": {
"Type": "String",
"Description" : "New account password",
"MinLength": "1",
"MaxLength": "41",
"ConstraintDescription" : "the password must be between 1 and 41 characters"
},

"LambdaFunctionARN": {
"Description": "Exisiting Lambda function ARN",
"Type": "String"
}

},

"Resources" : {
"CFNUser" : {
"Type" : "AWS::IAM::User",
"Properties" : {
"UserName" : {"Ref":"UName"},
"LoginProfile": {
"Password": { "Ref" : "Password" }
}
}
},

"CFNKeys" : {
"Type" : "AWS::IAM::AccessKey",
"Properties" : {
"UserName" : { "Ref": "CFNUser" }

}
},
"MySecret": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"Name": {"Ref":"UName"},
"SecretString": {
"Fn::Join": [
"", [
"{\"AccessKey\":",
"\"",
{"Ref" : "CFNKeys"},
"\"",
",\"SecretKey\":",
"\"",
{"Fn::GetAtt" : ["CFNKeys", "SecretAccessKey"]},
"\"",
"}"
]
]
}
}
},
"ScheduledRuleCreate": {
"Type": "AWS::Events::Rule",
"Properties": {
"Description": "ScheduledRule",
"ScheduleExpression": "rate(90 days)",
"State": "ENABLED",
"Targets": [{
"Arn": {"Ref":"LambdaFunctionARN"},
"Id":"test",
"Input": {
"Fn::Join": [
"", [
"{\"action\":\"create\",\"username\":\"",
{
"Ref": "UName"
},
"\"}"
]
]
}
}]
}
},
"PermissionForEventsToInvokeCreateLambda": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": {"Ref":"LambdaFunctionARN"},
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["ScheduledRuleCreate", "Arn"] }
}
},
"ScheduledRuleDeactivate": {
"Type": "AWS::Events::Rule",
"Properties": {
"Description": "ScheduledRule",
"ScheduleExpression": "rate(100 days)",
"State": "ENABLED",
"Targets": [{
"Arn": {"Ref":"LambdaFunctionARN"},
"Id":"test",
"Input": {
"Fn::Join": [
"", [
"{\"action\":\"deactivate\",\"username\":\"",
{
"Ref": "UName"
},
"\"}"
]
]
}
}]
}
},
"PermissionForEventsToInvokeDeactivateLambda": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": {"Ref":"LambdaFunctionARN"},
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["ScheduledRuleDeactivate", "Arn"] }
}
},
"ScheduledRuleDelete": {
"Type": "AWS::Events::Rule",
"Properties": {
"Description": "ScheduledRule",
"ScheduleExpression": "rate(110 days)",
"State": "ENABLED",
"Targets": [{
"Arn": {"Ref":"LambdaFunctionARN"},
"Id":"test",
"Input": {
"Fn::Join": [
"", [
"{\"action\":\"delete\",\"username\":\"",
{
"Ref": "UName"
},
"\"}"
]
]
}
}]
}
},
"PermissionForEventsToInvokeDeleteLambda": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": {"Ref":"LambdaFunctionARN"},
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["ScheduledRuleDelete", "Arn"] }
}
}
},

"Outputs" : {
"AccessKey" : {
"Value" : { "Ref" : "CFNKeys" },
"Description" : "AWSAccessKeyId of new user"
},
"SecretKey" : {
"Value" : { "Fn::GetAtt" : ["CFNKeys", "SecretAccessKey"]},
"Description" : "AWSSecretKey of new user"
}
}
}

Setting Up Amazon CloudWatch Event Rules

Create three CloudWatch Event rules with a schedule to call the Lambda function every 90, 100, and 110 days as outlined by the rotation policies. These CloudWatch rules should have the Lambda function as the target. The Lambda function name and version should be same across all the CloudWatch rules.

To create the CloudWatch Event rules, go to the AWS Management Console’s CloudWatch Events tab. On the Rules section, click Create rule as follows:

Figure 1 — Amazon CloudWatch Event rule configuration.

Once the Lambda function AutomatedKeyRotation has been set as target, we can click Configure details to name the rule and save it. Repeat the same process to create the three CloudWatch Event rules.

In Figure 2 above, you can see there are two parameters being passed to Lambda via CloudWatch Event rule input configuration:

  • Parameter 1:
  • key: action
  • value: create
  • Parameter 2:
  • key: username
  • value: <name of the user>

The Lambda function handler will use the Parameter 1 value as input to choose which function to be executed. In this case, the action value is create and Lambda will execute create_key function.

Figure 2— AWS Lambda function handler.

The Lambda function will use the username value passed from the CloudWatch Event rule as uname input for create_key function.

Figure 3— The create_key function is called based on event value ‘create.’

Next, create an SNS topic for Lambda to send notifications, and provide the SNS topic’s Amazon Resource Name (ARN) on the Lambda code below.

Figure 4— SNS topic ARN.

Follow the same steps to set up the deactivate and delete functions based on CloudWatch Event rule input configuration.

IAM User and AWS Secrets Manager

To test our workflow, follow the steps below to create the IAM user and secret inside AWS Secrets Manager as a target of our Lambda function:

  • Create the IAM user with access key and secret key for programmatic access.
  • Create the AWS Secrets Manager secret for the IAM user you created, and set the secret name to the same value as the IAM user name.
  • The secret contains AccessKey and SecretKey for the IAM user you created; see example below:

Figure 5— IAM user AccessKey and SecretKey.

The AccessKey and SecretKey will be modified on the time intervals provided in the CloudWatch Event rule. Figure 7 below provides an example of a modified AccessKey and SecretKey based on our Lambda function and CloudWatch Event rule.

Figure 6— Modified AccessKey example.

The Lambda code also modifies the access key variable with the new rotated values. Figure 8 below shows how the new access keys look like inside AWS Secrets Manager.

Figure 7— Modified AccessKey example inside AWS Secrets Manager.

--

--