For simple AWS SDK python and nodejs scripts you can easily pull in a lambda script into a cloudformation template with a jinja2 lookup statement. Below is part of our account setup mentioned previously.

In order to disable access keys that has not been used recently, we scan for it every morning and if we find one, disable it.

The lambda function

The lambda function is very simple, and pretty much just:

  • looks at all the users
  • sees if they are in a group that will exclude them from this disable-fest
  • then if they are still in contention checks their active access keys last used time
  • if the key is not being used, disable it
import datetime
import boto3

ZERO = datetime.timedelta(0)

class UTC(datetime.tzinfo):
    def utcoffset(self, dt):
        return ZERO
    def tzname(self, dt):
        return "UTC"
    def dst(self, dt):
        return ZERO

utc = UTC()
def handler(event, context):
    client = boto3.client('iam')
    userList = client.list_users()
    foo = []
    for user in userList['Users']:
        groupList = client.list_groups_for_user(UserName=user['UserName'])
        isSUG = False
        for group in groupList['Groups']:
            if group['GroupName'] == 'ServiceUserGroup':
                print ('sug user...',user['UserName'])
                isSUG = True
        if not isSUG:
            keyList = client.list_access_keys(UserName=user['UserName'])
            for key in keyList['AccessKeyMetadata']:
                print (user['UserName'])
                if key['Status'] == 'Active':
                   status = client.get_access_key_last_used( AccessKeyId=key['AccessKeyId'] )
                   print (status['UserName'],status['AccessKeyLastUsed'])
                   disableKey = True
                   if 'LastUsedDate' in status['AccessKeyLastUsed']:
                       now = datetime.datetime.now(utc)
                       diff = now - status['AccessKeyLastUsed']['LastUsedDate']
                       if diff.total_seconds() > 30*60:
                           print ('going to disable', status['UserName'],key['AccessKeyId'], diff.total_seconds())
                       else:
                           print ('key in use', status['UserName'],key['AccessKeyId'], diff.total_seconds())
                           disableKey = False
                   if disableKey:
                       client.update_access_key(UserName=user['UserName'], AccessKeyId=key['AccessKeyId'], Status='Inactive')
                       print('disabled....')

The cloudformation snippet

In our aws_account_config variable for each account we specify a boolean that, if true, spits out the following yaml

    ###disable access tokens
    {% if aws_account_config[acc].auto_disable_keys %}
    disableKeysRole:
      Type: 'AWS::IAM::Role'
      Properties:
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Principal:
              Service: 'lambda.amazonaws.com'
            Action:
            - 'sts:AssumeRole'
        Path: '/'
        Policies:
        - PolicyName: logs
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
              Resource: 'arn:aws:logs:*:*:*'
        - PolicyName: iam
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
                - 'iam:ListUsers'
                - 'iam:GetAccessKeyLastUsed'
                - 'iam:ListAccessKeys'
                - 'iam:ListGroupsForUser'
                - 'iam:UpdateAccessKey'
              Resource: '*'
    disableKeysLambdaFunction:
      Type: 'AWS::Lambda::Function'
      Properties:
        Code:
          ZipFile: |
            {{lookup('file','roles/setup_account/files/disableKeys.py')|indent(width=12,indentfirst=False)}}
        Handler: 'index.handler'
        MemorySize: 128
        Role: !GetAtt 'disableKeysRole.Arn'
        Runtime: 'python3.6'
        Timeout: 60
    disableKeysEventsRule:
      Type: 'AWS::Events::Rule'
      DependsOn: disableKeysLambdaFunction
      Properties:
        Name: disableKeysEvents
        ScheduleExpression: cron(0 2 * * ? *)
        State: ENABLED
        Targets:
          - { Arn: !GetAtt 'disableKeysLambdaFunction.Arn', Id: disableKeys }
    permissionForEventsToInvokeLambda: 
      Type: "AWS::Lambda::Permission"
      Properties: 
        FunctionName: !Ref "disableKeysLambdaFunction"
        Action: "lambda:InvokeFunction"
        Principal: "events.amazonaws.com"
        SourceArn: !GetAtt 'disableKeysEventsRule.Arn'
    {% endif %}

The template consists of a Lambda execution role, the Lambda itself, the Events Rule, and its Lambda Permission to trigger the Lambda.

In order to pull in the function into the template we use ansible's lookup function and format it so it fits in properly with the template, with this line {{lookup('file','roles/setup_account/files/disableKeys.py')|indent(width=12,indentfirst=False)}}

This way you can keep your code and your cloudformation seperate (so you can test it seperately from the deployment) and it allows you to skip the 'zip and copy to s3' steps during deployment.