Edit Default Security Group created by aws for AWS Active Directory via cloudformation

2.4k views Asked by At

When AWS Active Directory(Type: "AWS::DirectoryService::MicrosoftAD") is created via cloud formation, the AWS also creates the security group for domain controllers. The description for such security group is "AWS created a security group for d-123456adb directory controllers".

This security group allows source in ingress as 0.0.0.0/0 for all ports.

I have to manually edit/set it to my vpc CIDR after cloud formation run and also I am not able to get its ID inside cloud formation.

Is there any way to edit OR self-define the security group while creating Microsoft AD("AWS::DirectoryService::MicrosoftAD") via cloud formation?

3

There are 3 answers

0
Ville On

This response is an edited reply by AWS support to my similar inquiry as the OP's question.

This feature is currently not available and AWS's CloudFormation developer team is aware of this issue and there is a feature request in place.

As a workaround, you can leverage a Lambda backed custom resource to get the security ID and pass it to the custom resource so that it can be accessed in the CF stack.

In this approach, you will create a Lambda function that can take the security group name and VPC-id as input and give the security group-id as output. The custom resource created is the piece of code which will signal the Lambda function with a group name and VPC-id. Lambda returns the security group-id to this custom resource, you can get the sg-id as shown below:

{ "Fn::GetAtt" : ["CustomResouce", "security_group_id"] }

A sample template and a sample Lambda function (for acquiring the security group-id) are included at the end of this message. In the template, the function has been used in the output of the code, but you can use the same in the client's security_group as shown below:

"SourceSecurityGroupId" : { "Fn::GetAtt" : ["CustomResouce", "security_group_id"] },
"SourceSecurityGroupName" : { "Fn::Sub": [ "${Alias}_controllers", { "Alias": {"Ref" : "Alias" }} ]},

Custom Lambda function, customresouce.py (to be placed in a S3 bucket where Lambda can access it):

import json
import boto3
import time
from botocore.vendored import requests
def lambda_handler(event, context):
    print event['RequestType']
    try:
        if event['RequestType'] == 'Delete':
            print "delete"
            responseData = {'response': 'Delete'}
            responseStatus = 'SUCCESS'
        elif event['RequestType'] == 'Create':
            print "blabla"
            ec2 = boto3.resource('ec2')
            vpc = ec2.Vpc(event['ResourceProperties']['vpc'])
            security_group_iterator = vpc.security_groups.filter(GroupNames = [event['ResourceProperties']['security_group']])
            sg_id = list(security_group_iterator.filter(GroupNames = [event['ResourceProperties']['security_group']]))[0].id
            print sg_id
            responseData = {'security_group_id': sg_id}
        elif event['RequestType'] == 'Update':
            print "update"
            responseData = {'response': 'Update'}
            responseStatus = 'SUCCESS'
        responseStatus = 'SUCCESS'
    except:
        responseStatus = 'FAILED'
        responseData = {'FAILED': 'Something bad happened.'}
    sendResponse(event, context, responseStatus, responseData)

def sendResponse(event, context, responseStatus, responseData, reason=None, physical_resource_id=None):
    responseBody = {'Status': responseStatus,
                    'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
                    'PhysicalResourceId': physical_resource_id or context.log_stream_name,
                    'StackId': event['StackId'],
                    'RequestId': event['RequestId'],
                    'LogicalResourceId': event['LogicalResourceId'],
                    'Data': responseData}
    print 'RESPONSE BODY:n' + json.dumps(responseBody)
    responseUrl = event['ResponseURL']
    json_responseBody = json.dumps(responseBody)
    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }
    try:
        response = requests.put(responseUrl,
                                data=json_responseBody,
                                headers=headers)
        print "Status code: " + response.reason
    except Exception as e:
        print "send(..) failed executing requests.put(..): " + str(e)

A sample template that utilizes the custom Lambda function:

{
"Resources": {
  "myDirectory" : {
    "Type" : "AWS::DirectoryService::SimpleAD",
    "Properties" : {
      "Name" : "corp.example.com",
      "Password" : "P@ssword",
      "Size" : "Small",
      "VpcSettings" : {
        "SubnetIds" : [ "subnet_value-1", "subnet_value-2" ],
        "VpcId" : "your_vpc-id"
      }
    }
  },

  "CustomResouce": {
         "DependsOn": "myDirectory",
         "Type": "Custom::GettingsecuritygroupId",
         "Version" : "1.0",
         "Properties" : {
            "ServiceToken": {"Fn::GetAtt" : ["Mylambda","Arn"]},
            "vpc" : "vpc-b0ee43c9",
            "security_group" : { "Fn::Sub": [ "${Alias}_controllers", {"Alias":{"Fn::GetAtt" : ["myDirectory","Alias"]} }]}
         }
      },

  "Mylambda":{
  "Type" : "AWS::Lambda::Function",
  "Properties" : {
    "Code" : {
      "S3Bucket": "Your_s3_bucket_name",
      "S3Key": "customresource.py.zip"
    },
    "Handler" : "customresource.lambda_handler",
    "Role" : "Role_whic_has_permissions_ec2:*",
    "Runtime" :"python2.7",
    "Timeout" : "60"
  }
}

 },

 "Outputs":{
   "SGID" : {
     "Value" : { "Fn::GetAtt" : ["CustomResouce", "security_group_id"] }
   }
 }
}

Rather kludgy, but seems like it might work as an interim solution until AWS gets around to implementing the functionality into their APIs/CloudFormation/Hosted AD. NOTE: I have not had the opportunity to test the above yet, but I'm posting it here for the OP and anyone else who might be looking for a solution to this issue.

References:

Custom Resource Reference

AWS Lambda-backed Custom Resources

AWS::Lambda::Function

0
Tim On

This isn't quite what was asked, but is related and may be helpful.

When a VPC or other resource is created, a security is created which can have rules that are too permissive. As documented on this page under "Remove Default Rule" you can remove the default rules by specifying new rules. The following CloudFormation template part can help remove the old rules to be replaced with something less permissive.

I found that it works fine to remove the egress 0.0.0.0/0 rule. It does not remove the ingress self-referential rule to the current security group, but that probably isn't as important as the egress rule.

# Remove default security group rules in the VPC
VpcDefaultSecurityGroupEgressRemove:
Type: AWS::EC2::SecurityGroupEgress
  Properties:    
    GroupId:
      Fn::GetAtt: [VPCReference, DefaultSecurityGroup]
    IpProtocol: icmpv6
    CidrIp: 127.0.0.1/32
    Description: Effectively no access

VpcDefaultSecurityGroupIngressRemove:
  Type: AWS::EC2::SecurityGroupIngress
  Properties:    
    GroupId:
      Fn::GetAtt: [VPCReference, DefaultSecurityGroup]
    IpProtocol: icmpv6
    CidrIp: 127.0.0.1/32
    Description: Effectively no access

I've tested this and it works ok. I haven't managed to work out a way to remove all security group rules in CloudFormation, I think it'll have to be a lambda.

VPC Template

VPCDefaultSecurityGroup:
  Value: !GetAtt VPC.DefaultSecurityGroup
  Export:
    Name: "VPCDefaultSecurityGroup"

Security Group Template

GroupId: !ImportValue VPCDefaultSecurityGroup
0
Timothy Carpenter On

Here is a working version using a custom resource with a backend lambda function.

lambda:
  Type: AWS::Lambda::Function
  Properties:
    FunctionName: !Sub ${prefix}-function
    Runtime: python3.9
    Role: !GetAtt role.Arn
    Handler: index.lambda_handler # default file name of 'index.py', then function name
    Timeout: 60
    Code:
      ZipFile: |
        import boto3
        import cfnresponse

        def lambda_handler(event, context):
            print (event['RequestType'])
            responseStatus = 'SUCCESS'

            try:
                if event['RequestType'] == 'Delete':
                    responseData = {'response': 'Delete'}
                elif event['RequestType'] == 'Create':
                    ec2 = boto3.client('ec2')
                    groups = ec2.describe_security_groups()
                    for group in groups['SecurityGroups']:
                        if(group['GroupName'] == event['ResourceProperties']['security_group_name']):
                            responseData = {'security_group_id': group['GroupId'] }
                elif event['RequestType'] == 'Update':
                    responseData = {'response': 'Update'}
            except Exception as e:
                responseStatus = 'FAILED'
                responseData = {'FAILED': str(e)}
            
            try:
                cfnresponse.send(event, context, responseStatus, responseData)
                print ('SUCCESS')
            except Exception as e:
                print ('FAILURE: ' + str(e))

custom:
  Type: Custom::GetDirectorySecurityGroup
  Properties:
    ServiceToken: !GetAtt lambda.Arn
    vpc: !Ref vpc
    security_group_name: !Sub ${directory}_controllers
  DependsOn:
    - directory
    - lambda

ingressTCP: 
  Type: AWS::EC2::SecurityGroupIngress
  Properties:
    GroupId: !GetAtt custom.security_group_id # calls custom to get new sg
    IpProtocol: tcp
    FromPort: 49152
    ToPort: 65535
    SourceSecurityGroupId: !Ref security # id of other security group
  DependsOn:
    - security # logical-id of new sg from full cloudformation
    - directory # logical-id of ds from full cloudformation

I'm posting this for anyone looking to modify inbound rules for a new directory service. I tried every which way to get the security group ID and this is the only method that worked.