AWS CDK를 사용하는 Linux EC2 배스천 호스트

31513 단어 cdksecurityawspython

요구 사항.



퍼블릭 IP를 사용하여 퍼블릭 네트워크 서브넷에 데이터베이스를 배치하는 것과 비교하여 최종 사용자를 위해 RDS 인스턴스에 대한 더 안전한 액세스를 제공합니다.

아래 솔루션은 AWS EC2 클라우드 초기화 기능을 사용하여 새 SSH 공개 키가 public_keys 디렉터리에 배치될 때마다/home/ec2-user/.ssh/authorized_keys 파일을 업데이트하기 위해 필수 Galaxy 모듈과 함께 ansible을 설치합니다.

이렇게 하면 cloud-init 정의가 변경되기 때문에 SSH 키가 추가되거나 제거될 때마다 EC2가 다시 생성됩니다.

이 솔루션은 ansible에 의존하므로 새 키 파일 이름이 ansible/bastion/roles/ssh/tasks/main.yaml에 추가되도록 해야 합니다.

다음은 작업 main.yaml 파일의 예입니다.

- name: Gather EC2 metadata facts
  amazon.aws.ec2_metadata_facts:

- name: Set up multiple authorized key taken from files
  ansible.posix.authorized_key:
    user: ec2-user
    state: present
    key: '{{ item }}'
  with_file:
    - public_keys/tom_id_rsa.pub



플레이북 아래 정의:

---
- hosts: localhost
  connection: local
  remote_user: ec2-user
  become: yes
  become_method: sudo
  gather_facts: yes
  roles:
    - {role: 'ssh'}


가정.


  • VPC 및 서브넷(퍼블릭 및 프라이빗)
  • Route53 DNS 공개 영역
  • Ansible 플레이북을 호스팅하는 S3 버킷
  • KMS 키(고객 관리)

  • /ansible이라는 이름의 루트 디렉터리에 다음 디렉터리 구조를 가진 ansible 플레이북이 있습니다.

    
    ├── ansible
    │   └── bastion
    │       ├── ansible.cfg
    │       ├── main.yml
    │       └── roles
    │           └── ssh
    │               ├── files
    │               │   └── public_keys
    │               │       └── tom_id_rsa.pub
    │               └── tasks
    │                   └── main.yaml
    
    


    데이터 흐름이 있는 아키텍처



    아래 아키텍처는 다음과 같은 기사에서 가져온 것입니다.



    CDK 코드



    가정 섹션에서 필수 구성 요소를 가져오겠습니다.




    imported_vpc = ec2.Vpc.from_lookup(
        self,
        id="imported_vpc",
        vpc_id=ssm.StringParameter.value_from_lookup(
            self, parameter_name=self.object_names["vpc_id_ssm_param_name"]
        ),
    )
    imported_kms_key = kms.Key.from_lookup(
        self, id="imported_kms_key", alias_name=f'alias/{self.object_names["shared_kms_key_alias"]}'
    )
    imported_route53_public_zone = route53.PublicHostedZone.from_public_hosted_zone_attributes(
        self,
        id="imported_public_hosted_zone_id",
        hosted_zone_id=ssm.StringParameter.value_for_string_parameter(
            self, parameter_name=self.object_names["ssm_public_dns_zone_id"]
        ),
        zone_name=self.object_names["ssm_public_dns_zone_name"],
    )
    


    Bastion을 생성하고 필요한 권한을 부여하자




    bastion = self.bastion_host(
        props=props,
        ansible_bucket=shared_ansible_s3_bucket,
        route53_public_zone=imported_route53_public_zone,
        shared_kms_key=imported_kms_key,
        vpc=imported_vpc,
    )
    shared_ansible_s3_bucket.grant_read(bastion)
    imported_kms_key.grant_decrypt(bastion)
    


    Ansible 플레이북을 S3에 복사해 봅시다.




    self.bucket_deployment(
        destination_key_prefix="bastion", object_path="../../ansible/bastion", bucket=shared_ansible_s3_bucket
    )
    
    


    bastion_host 및 bucket_deployment에 대한 메서드를 만들어 봅시다.




    
    def bucket_deployment(self, destination_key_prefix: str, object_path: str, bucket: s3.IBucket) -> None:
        """Deploy directory or zip archive to S3 bucket.
    
        :param bucket: The AWS S3 Bucket CDK object to which deployment will occur
        :param destination_key_prefix: The prefix which will be used to deploy object into
        :param object_path: Path to the directory or zip archive in filesystem
        :return:
        """
        this_dir = path.dirname(__file__)
        source_asset = s3_deployment.Source.asset(path.join(this_dir, object_path))
        s3_deployment.BucketDeployment(
            self,
            id=f"{destination_key_prefix}_deployment",
            destination_bucket=bucket,
            sources=[source_asset],
            destination_key_prefix=destination_key_prefix,
        )
    
    
    def bastion_host(
        self,
        props: Dict,
        ansible_bucket: s3.IBucket,
        route53_public_zone: route53.IPublicHostedZone,
        shared_kms_key: kms.IKey,
        vpc: ec2.IVpc,
    ) -> ec2.BastionHostLinux:
        """Create bastion host to route network traffic (grant access) to the
        resources placed inside private subnets.
    
        :param ansible_bucket: The CDK instance of existing s3 bucket that host ansible playbooks
        :param route53_public_zone: The CDK object for Route53 zone.
        In this zone the DNS entry for bastion host will be created
        :param props: The dictionary which contain configuration values loaded initially from /config/config-env.yaml
        :param shared_kms_key: The AWS KMS key shared for this project
        :param vpc: The EC2 VPC object, this vpc will be used to place bastion host in it
        """
        ansible_copy_keys_init_list: List[ec2.InitCommand] = []
        ssh_pub_keys_path = "ansible/bastion/roles/ssh/files/public_keys"
        # pylint: disable=W0612
        for dir_path, dirs, files in walk(ssh_pub_keys_path):
            ansible_copy_keys_init_list.extend(
                ec2.InitCommand.shell_command(
                    shell_command=f"aws s3 cp s3://{ansible_bucket.bucket_name}/bastion/roles/ssh/files/public_keys/{file_name} /tmp/"
                )
                for file_name in files
            )
    
        init = ec2.CloudFormationInit.from_config_sets(
            config_sets={
                # Applies the configs below in this order
                "default": [
                    "config",
                    "yum_packages",
                    "ansible_galaxy_modules_installation",
                    "ansible_playbook_from_s3",
                    "copy_ssh_pub_keys_from_s3",
                    "enable_fail2ban",
                ]
            },
            configs={
                "config": ec2.InitConfig(
                    [
                        ec2.InitCommand.shell_command(shell_command="yum update -y"),
                        ec2.InitCommand.shell_command(shell_command="amazon-linux-extras install ansible2 -y"),
                        ec2.InitCommand.shell_command(shell_command="amazon-linux-extras install epel -y"),
                    ]
                ),
                "yum_packages": ec2.InitConfig(
                    [
                        ec2.InitPackage.yum("htop"),
                        ec2.InitPackage.yum("fail2ban"),
                    ]
                ),
                "ansible_galaxy_modules_installation": ec2.InitConfig(
                    [
                        ec2.InitCommand.shell_command(
                            cwd="/root", shell_command="ansible-galaxy collection install ansible.posix"
                        ),
                        ec2.InitCommand.shell_command(
                            cwd="/root", shell_command="ansible-galaxy collection install amazon.aws"
                        ),
                    ]
                ),
                "ansible_playbook_from_s3": ec2.InitConfig(
                    [
                        ec2.InitCommand.shell_command(shell_command="mkdir /root/ansible"),
                        ec2.InitCommand.shell_command(
                            shell_command=f"aws s3 cp s3://{ansible_bucket.bucket_name}/bastion /root/ansible/bastion --recursive"
                        ),
                        ec2.InitCommand.shell_command(
                            cwd="/root/ansible/bastion", shell_command="ansible-playbook main.yml"
                        ),
                    ]
                ),
                # This section does not implement any real change on Bastion host. It exists only for
                # changing the EC2 cloud-init definition that will force EC2 replacement whenever
                # a new ssh public key file will be added to the repository
                "copy_ssh_pub_keys_from_s3": ec2.InitConfig(ansible_copy_keys_init_list),
                "enable_fail2ban": ec2.InitConfig(
                    [
                        ec2.InitCommand.shell_command(shell_command="systemctl enable fail2ban"),
                        ec2.InitCommand.shell_command(shell_command="systemctl start fail2ban"),
                    ]
                ),
            },
        )
    
        init_options = ec2.ApplyCloudFormationInitOptions(
            config_sets=["default"], timeout=cdk.Duration.minutes(30), include_url=True, include_role=True
        )
        security_group = ec2.SecurityGroup(
            self,
            id="bastion_security_group",
            vpc=vpc,
            security_group_name=f'{self.object_names["standard_prefix"]}-bastion',
        )
        security_group.add_ingress_rule(peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(port=22))
    
        ssm.StringParameter(
            self,
            id="bastion_security_group_id_ssm_param",
            string_value=security_group.security_group_id,
            parameter_name=self.object_names["bastion_security_group_id_ssm_param"],
        )
    
        bastion = ec2.BastionHostLinux(
            self,
            id="bastion_host",
            block_devices=[
                ec2.BlockDevice(
                    device_name="/dev/xvda",
                    volume=ec2.BlockDeviceVolume.ebs(
                        volume_size=10,
                        encrypted=True,
                        volume_type=ec2.EbsDeviceVolumeType.GP3,
                        kms_key=shared_kms_key,
                        delete_on_termination=True,
                    ),
                )
            ],
            init=init,
            init_options=init_options,
            instance_name=f'{props["stage"]}-{props["project"]}-bastion',
            instance_type=ec2.InstanceType.of(
                instance_class=ec2.InstanceClass.BURSTABLE4_GRAVITON, instance_size=ec2.InstanceSize.MICRO
            ),
            security_group=security_group,
            subnet_selection=ec2.SubnetSelection(
                subnet_group_name="public",
            ),
            vpc=vpc,
        )
    
        route53.CnameRecord(
            self,
            id="bastion_dns_record",
            domain_name=bastion.instance_public_dns_name,
            record_name=f"bastion.{route53_public_zone.zone_name}",
            zone=route53_public_zone,
            comment="bastion host",
            ttl=cdk.Duration.minutes(1),
        )
    
        return bastion
    
    
    

    좋은 웹페이지 즐겨찾기