CloudFormation一撃でAWS Transfer for SFTP のパブリックアクセスを特定IP に限定する
おはようございます、もきゅりんです。
今回は特にひねりもなく、下記弊社ブログ記事で紹介された構成をCloudFormation一撃で構築するという内容になります。
構成は下図です。
構成内容
- NLB用のパブリックサブネット
- VPCエンドポイント
- VPCエンドポイント用のセキュリティグループ
- NLBのログ用のS3バケット
- SFTPのためのS3バケット
- SFTPサーバ
- SFTPサーバのログロール
- SFTPサーバを利用するユーザーのロール
- NLB
- VPCEndpointのNetworkInterfaceIdsからPrivateIPを取得するためのカスタムリソースのLambda関数
- 指定IPからのSSH接続以外を拒否するNACL
前提
利用するにあたっての前提条件は以下です。
- VPCおよびプライベート、パブリックルートテーブルは構築済みであること
(OutputsでVPCIDなどを出力していることを前提としていますが、実際に利用する際はうまく微調整して頂ければと思います。) - NACLはサブネットが対象のため、影響範囲が広いのでNLB用のサブネットを別で作成しています。
- 出力先S3バケット名を指定しますが、バケット名にAWSアカウント番号を付与します。
- Cloudformation削除時はS3バケット内のファイルを削除済みにしないとS3バケット削除できません。必要に応じて、DeletionPolicyをつけて削除除外してください。
パラメータ
以下を指定する仕様となります。
- NLBを配置するサブネットのCIDR表記
- SSH接続を許可するIP
テンプレート
AWSTemplateFormatVersion: 2010-09-09 Description: S3 and Access Restricted AWS Transfer for SFTP # ------------------------------------------------------------# # Parameters # ------------------------------------------------------------# Parameters: NameTagPrefix: Type: String Default: test Description: Prefix of Name tags. LowerNameTagPrefix: Type: String Default: test Description: Prefix of Lowercase Name tags For S3 BucketName. ENV: Type: String Default: stg Description: Prefix of Env tags. NLBSubnet1CidrBlock: Type: String Description: NLBSubnet1 CidrBlock. NLBSubnet2CidrBlock: Type: String Description: NLBSubnet2 CidrBlock. AllowIP: Type: String Description: Allowed IP # ------------------------------------------------------------# # Resources # ------------------------------------------------------------# Resources: # ------------------------------------------------------------# # Subnets # ------------------------------------------------------------# NLBSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [0, 'Fn::GetAZs': { Ref: 'AWS::Region' }] CidrBlock: !Ref NLBSubnet1CidrBlock Tags: - Key: Name Value: !Sub ${NameTagPrefix}-${ENV}-NLBSubnet1 VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' NLBSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref NLBSubnet1 RouteTableId: Fn::ImportValue: !Sub ${NameTagPrefix}-${ENV}-public-rtb NLBSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [1, 'Fn::GetAZs': { Ref: 'AWS::Region' }] CidrBlock: !Ref NLBSubnet2CidrBlock Tags: - Key: Name Value: !Sub ${NameTagPrefix}-${ENV}-NLBSubnet2 VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' NLBSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref NLBSubnet2 RouteTableId: Fn::ImportValue: !Sub ${NameTagPrefix}-${ENV}-public-rtb # ------------------------------------------------------------# # SecurityGroup # ------------------------------------------------------------# SFTPEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg' GroupDescription: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg' SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Ref NLBSubnet1CidrBlock Description: SSH From NLBSunbnet1 - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Ref NLBSubnet2CidrBlock Description: SSH From NLBSunbnet2 VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' Tags: - Key: Name Value: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg' # ------------------------------------------------------------# # VPC Endpoint # ------------------------------------------------------------# VPCTransferForSFTPEndpoint: Type: AWS::EC2::VPCEndpoint DependsOn: SFTPEndpointSecurityGroup Properties: VpcEndpointType: Interface SubnetIds: - !Ref NLBSubnet1 - !Ref NLBSubnet2 SecurityGroupIds: - !Ref SFTPEndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.transfer.server VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' # # ------------------------------------------------------------# # # S3 Bucket For SFTP # # ------------------------------------------------------------# HomeS3bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-sftp-for-test AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True # # ------------------------------------------------------------# # # IAM Role # # ------------------------------------------------------------# # SFTPサーバに適用するIAMRole SFTPRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: transfer.amazonaws.com Action: sts:AssumeRole SFTPPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyName: SFTPPolicy PolicyDocument: Statement: - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !GetAtt HomeS3bucket.Arn - Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:DeleteObjectVersion - s3:DeleteObject - s3:GetObjectVersion Resource: !Join - '' - - 'arn:aws:s3:::' - !Ref HomeS3bucket - /* Roles: - !Ref SFTPRole SFTPLogRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: transfer.amazonaws.com Action: sts:AssumeRole SFTPLogPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyName: CWLForSFTPPolicy PolicyDocument: Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams - logs:CreateLogGroup - logs:PutLogEvents Resource: '*' Roles: - !Ref SFTPLogRole # CustomResourceのLambdaに適用するIAMRole LambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole LambdaPolicy: Type: AWS::IAM::Policy Properties: PolicyName: LambdaPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:* - logs:* Resource: '*' Roles: - !Ref LambdaRole # # ------------------------------------------------------------# # # SFTP Server # # ------------------------------------------------------------# SFTPServer: Type: AWS::Transfer::Server Properties: EndpointType: VPC_ENDPOINT EndpointDetails: VpcEndpointId: !Ref VPCTransferForSFTPEndpoint IdentityProviderType: SERVICE_MANAGED LoggingRole: !GetAtt SFTPLogRole.Arn # # ------------------------------------------------------------# # # S3 Bucket For NLB Logs # # ------------------------------------------------------------# S3NLBLogBacket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True Tags: - Key: Name Value: !Ref ENV LifecycleConfiguration: Rules: - Id: !Sub ${LowerNameTagPrefix}-${ENV}-Log-Rules Status: Enabled ExpirationInDays: 400 BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog PolicyDocument: Statement: - Sid: 'AWSLogDeliveryWrite' Effect: 'Allow' Principal: Service: delivery.logs.amazonaws.com Action: - 's3:PutObject' Resource: Fn::Join: - '' - - 'arn:aws:s3:::' - !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog - '/*' Condition: StringEquals: s3:x-amz-acl: bucket-owner-full-control - Sid: AWSLogDeliveryAclCheck Effect: 'Allow' Principal: Service: delivery.logs.amazonaws.com Action: - 's3:GetBucketAcl' Resource: Fn::Join: - '' - - 'arn:aws:s3:::' - !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaFunction: Type: 'AWS::Lambda::Function' DeletionPolicy: 'Delete' Properties: Code: ZipFile: !Sub | import cfnresponse import json import boto3 def lambda_handler(event, context): print('REQUEST RECEIVED:\n' + json.dumps(event)) responseData = {} if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return if event['RequestType'] == 'Create': try: ec2 = boto3.resource('ec2') enis = event['ResourceProperties']['NetworkInterfaceIds'] for index, eni in enumerate(enis): network_interface = ec2.NetworkInterface(eni) responseData['IP' + str(index)] = network_interface.private_ip_address print(responseData) except Exception as e: responseData = {'error': str(e)} cfnresponse.send(event, context, cfnresponse.FAILED, responseData) return cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) Handler: index.lambda_handler Role: !GetAtt LambdaRole.Arn Runtime: python3.7 Timeout: 10 # # ------------------------------------------------------------# # # Custom Resource # # ------------------------------------------------------------# GetPrivateIPs: DependsOn: - VPCTransferForSFTPEndpoint Type: Custom::GetPrivateIPs Properties: ServiceToken: !GetAtt LambdaFunction.Arn NetworkInterfaceIds: !GetAtt VPCTransferForSFTPEndpoint.NetworkInterfaceIds # # ------------------------------------------------------------# # # NLB # # ------------------------------------------------------------# NlbEIP1: Type: AWS::EC2::EIP Properties: Domain: vpc NlbEIP2: Type: AWS::EC2::EIP Properties: Domain: vpc NLB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer DependsOn: NlbEIP2 Properties: Name: !Sub ${NameTagPrefix}-${ENV}-nlb LoadBalancerAttributes: - Key: access_logs.s3.enabled Value: true - Key: access_logs.s3.bucket Value: !Ref 'S3NLBLogBacket' - Key: access_logs.s3.prefix Value: !Sub '${NameTagPrefix}-${ENV}-nlb' SubnetMappings: - AllocationId: !GetAtt 'NlbEIP01.AllocationId' SubnetId: !Ref 'NLBSubnet1' - AllocationId: !GetAtt 'NlbEIP2.AllocationId' SubnetId: !Ref 'NLBSubnet2' Tags: - Key: ENV Value: !Ref ENV Type: network NLBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: TargetType: ip Targets: - Id: !GetAtt GetPrivateIPs.IP0 - Id: !GetAtt GetPrivateIPs.IP1 Port: 22 Protocol: TCP VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' NLBListenerSSH: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref 'NLBTargetGroup' LoadBalancerArn: !Ref 'NLB' Port: '22' Protocol: TCP # # ------------------------------------------------------------# # # NACL # # ------------------------------------------------------------# NLBNACL: Type: AWS::EC2::NetworkAcl Properties: Tags: - Key: Name Value: !Sub '${NameTagPrefix}-${ENV}-NLB-NACL' VpcId: Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc' NACLEntry100: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 100 Protocol: 6 PortRange: From: '22' To: '22' RuleAction: allow Egress: false CidrBlock: !Ref AllowIP NACLEntry101: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 101 Protocol: 6 PortRange: From: '22' To: '22' RuleAction: allow Egress: false CidrBlock: !Ref NLBSubnet1CidrBlock NACLEntry102: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 102 Protocol: 6 PortRange: From: '22' To: '22' RuleAction: allow Egress: false CidrBlock: !Ref NLBSubnet2CidrBlock NACLEntry200: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 200 Protocol: 6 PortRange: From: '22' To: '22' RuleAction: deny Egress: false CidrBlock: '0.0.0.0/0' NACLEntry300: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 300 Protocol: -1 PortRange: From: '-1' To: '-1' RuleAction: allow Egress: false CidrBlock: '0.0.0.0/0' NACLEntryOut100: Type: AWS::EC2::NetworkAclEntry Properties: NetworkAclId: !Ref NLBNACL RuleNumber: 100 Protocol: '-1' RuleAction: allow Egress: true CidrBlock: '0.0.0.0/0' PortRange: From: '-1' To: '-1' NACLAssociation1: Type: AWS::EC2::SubnetNetworkAclAssociation Properties: SubnetId: !Ref NLBSubnet1 NetworkAclId: !Ref NLBNACL NACLAclAssociation2: Type: AWS::EC2::SubnetNetworkAclAssociation Properties: SubnetId: !Ref NLBSubnet2 NetworkAclId: !Ref NLBNACL
最後に
これまでカスタムリソースにちゃんと向き合う機会がなかったため、勉強する良い機会になりました。
以上です。
どなたかのお役に立てば幸いです。