Skip to content

Instantly share code, notes, and snippets.

@adamcousins
Last active May 4, 2021 06:44
Show Gist options
  • Save adamcousins/1849a603660b9b6bb9939cd8ad37cb14 to your computer and use it in GitHub Desktop.
Save adamcousins/1849a603660b9b6bb9939cd8ad37cb14 to your computer and use it in GitHub Desktop.
AWSTemplateFormatVersion: '2010-09-09'
Description: "Security: Delegate an account for AWS SecurityHub centralisation within a single region within an AWS Organisation."
Parameters:
ServicePrincipal:
Type: String
Description: The Service Principal to delegate access to
Default: securityhub.amazonaws.com
AdminAccountId:
Type: String
Description: Delegated AWS Account Id to manage Security Hub across an AWS Organisation
LogRetention:
Type: String
Description: Log retention in number of days
Default: '7'
Resources:
### Cloudformation Custom Resource ###
### AWS Security Hub - Delegates an AWS Account as the AWS Security Hub Admin Account. ###
### Required Properties:
### ServiceToken: [Type: String]
### AdminAccountId: [Type: String]
### ServicePrincipal: [Type: String]
### Attributes (Output):
### None
### Notes:
### Defining this resource delegates an AWS Account as the AWS Security Hub Admin Account.
### It is expected this stack to be launched within the AWS Organisation Master Account
### This resource will also enable the securityhub principal in an AWS Organisation
### Example Usage ###
# EnableSecurityHubAdminAccount:
# Type: Custom::EnableSecurityHubAdminAccount
# Properties:
# ServiceToken: !GetAtt Function.Arn
# AdminAccountId: !Ref AdminAccountId
# ServicePrincipal: !Ref ServicePrincipal
### End Cloudformation Custom Resource ###
FunctionLogGroup:
Type: "AWS::Logs::LogGroup"
DeletionPolicy: Retain
Properties:
RetentionInDays: !Ref LogRetention
LogGroupName: !Sub "/aws/lambda/${Function}"
Function:
Type: AWS::Lambda::Function
Properties:
Description: Delegate an account for AWS Security Hub centralisation within a single region within an AWS Organisation
Handler: index.lambda_handler
Code:
ZipFile: |
import os, logging, boto3
from botocore.exceptions import ClientError
import cfnresponse
log = logging.getLogger()
log.setLevel(logging.INFO)
client = boto3.client('securityhub')
org_client = boto3.client('organizations')
region = os.environ['AWS_REGION']
def enable_service(service_principal):
log.info('enabling ' + service_principal + ' service in AWS Organizations')
response = org_client.enable_aws_service_access(
ServicePrincipal=service_principal
)
return response
def disable_service(service_principal):
log.info('disabling ' + service_principal + ' service in AWS Organizations')
response = org_client.disable_aws_service_access(
ServicePrincipal=service_principal
)
return response
def create_admin(admin_account_id):
log.info('enabling account id: ' + admin_account_id + ' as the master account in AWS region: ' + region)
client.enable_organization_admin_account(
AdminAccountId=admin_account_id
)
return admin_account_id
def delete_admin(admin_account_id):
log.info('disabling account id: ' + admin_account_id + ' as the master account in AWS region: ' + region)
client.disable_organization_admin_account(
AdminAccountId=admin_account_id
)
return admin_account_id
def lambda_handler(event, context):
log.info('REQUEST RECEIVED:\n %s', event)
admin_account_id = event['ResourceProperties']['AdminAccountId']
service_principal = event['ResourceProperties']['ServicePrincipal']
return_msg = {}
return_status = "FAILED"
physicalResourceId = admin_account_id
try:
if(event['RequestType'] == 'Create'):
enable_service(service_principal)
physicalResourceId = create_admin(admin_account_id)
elif(event['RequestType'] == 'Update'):
delete_admin(physicalResourceId)
physicalResourceId = create_admin(admin_account_id)
else:
delete_admin(physicalResourceId)
#wont disable service upon deletion as it fails with registered admins even though there is none
#disable_service(service_principal)
return_msg = {"Message": "master account id: " + admin_account_id + " has been " + event['RequestType'] + "d"}
return_status = "SUCCESS"
except ClientError as e:
log.info(e.response['Error']['Message'])
return_msg = {"Message": "Exception Found: " + e.response['Error']['Message']}
cfnresponse.send(event, context, return_status, return_msg, physicalResourceId)
Runtime: python3.6
Role: !GetAtt FunctionRole.Arn
Timeout: 120
FunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Resource: "*"
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Resource: '*'
Effect: Allow
Action:
- securityhub:EnableOrganizationAdminAccount
- securityhub:DisableOrganizationAdminAccount
- organizations:RegisterDelegatedAdministrator
- organizations:DeregisterDelegatedAdministrator
- organizations:DescribeOrganization
- organizations:ListDelegatedServicesForAccount
- organizations:ListDelegatedAdministrators
- organizations:ListAWSServiceAccessForOrganization
- Resource: '*'
Effect: Allow
Action:
- organizations:EnableAWSServiceAccess
- organizations:DisableAWSServiceAccess
Condition:
StringLikeIfExists:
organizations:ServicePrincipal: !Ref ServicePrincipal
# Usage ###
EnableSecurityHubAdminAccount:
DependsOn: FunctionLogGroup
Type: Custom::EnableSecurityHubAdminAccount
Properties:
ServiceToken: !GetAtt Function.Arn
AdminAccountId: !Ref AdminAccountId
ServicePrincipal: !Ref ServicePrincipal
Outputs:
FunctionArn:
Description: The Lambda Function Arn
Value: !GetAtt Function.Arn
AdminAccountId:
Description: The SecurityHub Admin Account Id
Value: !Ref AdminAccountId
ServicePrincipal:
Description: The Service Principal to delegate access to
Value: !Ref ServicePrincipal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment