EC2をSSL終端とするエンドツーエンドの暗号化設定

シンプルな構成ですとApplication Load Balancer(以下、ALB)をSSL終端としてALB - EC2間はhttp接続するケースを採用されることが多いと思います。



しかし、コンプライアンス上エンドツーエンドで暗号化を設定したいという顧客もいるため、今回はそれらをワンストップで実装できる手段を

## ユースケース

但しパフォーマンスなどのリクエストやパフォーマンス重視の場合は、エンドツーエンドでのSSL暗号化通信は不向きでしょう。

## 環境
今回使用する開発環境の紹介。
下記図の通り、AWS環境上で登録する。

- Node: **v18.0.0**
- 開発ツール: **AWS Cloud Development Kit (AWS CDK) v2** ... ver 2.144.0


## アーキテクチャ

本当だったら、EC2をプライベートサブネットに配置して直接インターネットからの接続を遮断する必要はあるのですが、今回はエンドツーエンドの検証目的のため ①Certbotを使用してEC2単体でHTTPS通信を設定した後、② Application Load Balancer を構築してALB経由でのエンドツーエンド通信を設定する予定です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/27c91150-05db-33c6-2d2e-f38144d8f034.png)


## セットアップ(CDKプロジェクト作成 〜 EC2作成)
https://letsencrypt.org/getting-started/
https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal

先ずはEC2までの環境までを構築してみます。
インフラのプロビジョングはAWS CDK を利用して、開発言語はTypeScriptで開発しています。

CDKのインストールから開始する際は、[こちら](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html)からお願いします。

CDK のインストールが完了した後、以下のようにCDKプロジェクトを作成します。

```sh
mkdir end_to_end_encryption
cd end_to_end_encryption
cdk init app --language typescript
```

CDKプロジェクトが作成されたら,AWSリソースを定義するため、各コンストラクトを呼び出します。
ひとまずはVPCとEC2の作成と、EC2がHTTP接続できるようになれれば下準備は完了です。
以下のファイルを下記のように修正します。

```typescript:./lib/end_to_end_encryption-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2tgt from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as r53 from 'aws-cdk-lib/aws-route53';
import * as r53_tgt from 'aws-cdk-lib/aws-route53-targets';

export class SimpleEc2WebappStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        /* ---------- IAM Role ---------- */
        const roleEC2 = new iam.Role(this, `iam-role-ec2`, {
            assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
        });
        roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
        roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("IAMFullAccess"));   // upload-server-certificate実行用に一時的に付与。設定後はコメントアウト

        /* ---------- VPC ---------- */
        const vpc = new ec2.Vpc(this, `vpc`, {
            ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
            subnetConfiguration: [
                {
                    cidrMask: 24,
                    name: 'Public',
                    subnetType: ec2.SubnetType.PUBLIC,
                },
                // {
                //     cidrMask: 24,
                //     name: 'ProtectedApp',
                //     subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
                // },
                // {
                //     cidrMask: 24,
                //     name: 'PrivateIsolatedDB',
                //     subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
                // },
            ],
            // natGateways: 2,
        });

        /* ---------- SG(ALB) ---------- */
        const sgEc2Alb = new ec2.SecurityGroup(this, `sg-alb`, {
            vpc,
        });
        sgEc2Alb.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));

        /* ---------- SG(EC2 Web) ---------- */
        const sgEc2Web = new ec2.SecurityGroup(this, `sg-ec2`, {
            vpc,
        });
        sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(443)); 
        sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(80));  //   CertbotでSSL証明書を導入するため、一時的に解放
        // sgEc2Web.addIngressRule(sgEc2Alb, ec2.Port.tcp(443));


        /* ---------- EC2 ---------- */
        const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
            vpc,
            vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
            instanceType: new ec2.InstanceType("t3.micro"),
            machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
            role: roleEC2,
            securityGroup: sgEc2Web,
            associatePublicIpAddress: true,
        });

        /* ---------- EIP ---------- */
        const eip = new ec2.CfnEIP(this, 'eip', { domain: 'vpc' });
        new ec2.CfnEIPAssociation(this, 'eip-associate', {
            allocationId: eip.attrAllocationId,
            instanceId: ec2Instance.instanceId,
          });

        /* ---------- ALB ---------- */
        // const alb = new elbv2.ApplicationLoadBalancer(this, `alb`, {
        //     vpc: vpc,
        //     internetFacing: true,
        //     securityGroup: sgEc2Alb,
        // });
        /* ---------- TargetGroup ---------- */
        // const tg = new elbv2.ApplicationTargetGroup(this, `tg`, {
        //     vpc: vpc,
        //     port: 443,
        //     targetType: elbv2.TargetType.INSTANCE,
        //     targets: [
        //         new elbv2tgt.InstanceIdTarget(ec2Instance.instanceId, 443)
        //     ],
        // });

        /* ---------- Listner ---------- */
        // alb.addListener(`listener-443`, {
        //     port: 443,
        //     defaultTargetGroups: [tg],
        //     certificates: [
        //         {
        //             certificateArn: "arn:aws:iam::123456789012:server-certificate/ssl-cert-letsencrypt-domainname" // Certbot設定後、設定
        //         }
        //     ],
        // });
        /* ---------- R53 HostZone ---------- */
        const hostzone = r53.HostedZone.fromLookup(this, `r53`, {
            domainName: "example.com",
        });

        // /* ---------- R53 Record ---------- */
        new r53.ARecord(this, `r53-record`, {
            zone: hostzone,
            recordName: `www.example.com`,
            target: r53.RecordTarget.fromIpAddresses(eip.attrPublicIp), // Certbot設定後はコメントアウト
            // target: r53.RecordTarget.fromAlias(new r53_tgt.LoadBalancerTarget(alb)),
        });

    }

}
```




セキュリティグループは80と443ポートを一時的に解放しています。
80番についてはcertbotコマンドによってサーバへSSL証明書の登録をするため、一時的にAnywhereで登録する。

 ## Certbotセットアップ


これでcertbotによるセットアップは完了です。

certbotから取得した証明書の情報について確認するには、以下のコマンドを実行し、先ほどセットアップした証明書がサーバに登録されていることを確認します。

```sh
certbot certificates
```

https接続すると問題なく接続することができます。
https://letsencrypt.org/ja/certificates/

このタイミングでセキュリティグループで一時的に解放した80番ポートを削除しても問題ないです。
(80番ポートを解放していないくても更新することができたので、おそらく証明書登録以降は443で通信するから？)


証明書の更新は以下の通りです。


ドライランは以下の通り。
```sh
certbot renew --dry-run
```

証明書の更新は以下の通り. 
```sh
certbot renew
# 証明書の更新は30日を切らないと更新がスキップされる. オプションとして「--force-renewal」を指定して強制的に更新させる
certbot renew --force-renewal
```


## ALB作成 & 証明書登録

証明書は`/etc/letsencrypt/live/[ドメイン名]/`に作成されています。

EC2から証明書をIAMに登録します.
https://repost.aws/ja/knowledge-center/import-ssl-certificate-to-iam

```sh
cd /etc/letsencrypt/live/[ドメイン名]

# 登録
aws iam upload-server-certificate \
    --server-certificate-name ssl-cert-letsencrypt-domainname \
    --certificate-body file://cert.pem \
    --private-key file://privkey.pem \
    --tags '{ "Key": "ExpiredDate", "Value": "202408" }'


# 確認
aws iam list-server-certificates

```

IAMでのサーバ証明書の管理について詳しくは[こちら](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_server-certs.html)を参考に。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_server-certs.html

上記が完了したら ALBを作成します。
ALBの作成にあたりリスナールールの追加やRoute 53 のレコード変更(EC2 -> ALBのエイリアスレコード)を実施するため、再度`./lib/end_to_end_encryption-stack.ts`を変更し、`cdk deploy`を実施しスタックを更新します。

```typescript:./lib/end_to_end_encryption-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2tgt from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as r53 from 'aws-cdk-lib/aws-route53';
import * as r53_tgt from 'aws-cdk-lib/aws-route53-targets';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class SimpleEc2WebappStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        /* ---------- IAM Role ---------- */
        const roleEC2 = new iam.Role(this, `iam-role-ec2`, {
            assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
        });
        roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
        // roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("IAMFullAccess"));   // upload-server-certificate実行用に一時的に付与。設定後はコメントアウト

        /* ---------- VPC ---------- */
        const vpc = new ec2.Vpc(this, `vpc`, {
            ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
            subnetConfiguration: [
                {
                    cidrMask: 24,
                    name: 'Public',
                    subnetType: ec2.SubnetType.PUBLIC,
                },
                // {
                //     cidrMask: 24,
                //     name: 'ProtectedApp',
                //     subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
                // },
                // {
                //     cidrMask: 24,
                //     name: 'PrivateIsolatedDB',
                //     subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
                // },
            ],
            // natGateways: 2,
        });

        /* ---------- SG(ALB) ---------- */
        const sgEc2Alb = new ec2.SecurityGroup(this, `sg-alb`, {
            vpc,
        });
        sgEc2Alb.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));

        /* ---------- SG(EC2 Web) ---------- */
        const sgEc2Web = new ec2.SecurityGroup(this, `sg-ec2`, {
            vpc,
        });
        // sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(443)); 
        // sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(80));  //   CertbotでSSL証明書を導入するため、一時的に解放
        sgEc2Web.addIngressRule(sgEc2Alb, ec2.Port.tcp(443));


        /* ---------- EC2 ---------- */
        const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
            vpc,
            vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
            instanceType: new ec2.InstanceType("t3.micro"),
            machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
            role: roleEC2,
            securityGroup: sgEc2Web,
            associatePublicIpAddress: true,
        });

        /* ---------- EIP ---------- */
        const eip = new ec2.CfnEIP(this, 'eip', { domain: 'vpc' });
        new ec2.CfnEIPAssociation(this, 'eip-associate', {
            allocationId: eip.attrAllocationId,
            instanceId: ec2Instance.instanceId,
          });

        /* ---------- ALB ---------- */
        const alb = new elbv2.ApplicationLoadBalancer(this, `alb`, {
            vpc: vpc,
            internetFacing: true,
            securityGroup: sgEc2Alb,
        });
        /* ---------- TargetGroup ---------- */
        const tg = new elbv2.ApplicationTargetGroup(this, `tg`, {
            vpc: vpc,
            port: 443,
            targetType: elbv2.TargetType.INSTANCE,
            targets: [
                new elbv2tgt.InstanceIdTarget(ec2Instance.instanceId, 443)
            ],
        });

        /* ---------- Listner ---------- */
        alb.addListener(`listener-443`, {
            port: 443,
            defaultTargetGroups: [tg],
            certificates: [
                {
                    certificateArn: "arn:aws:iam::123456789012:server-certificate/ssl-cert-letsencrypt-domainname" // Certbot設定後、設定
                }
            ],
        });
        /* ---------- R53 HostZone ---------- */
        const hostzone = r53.HostedZone.fromLookup(this, `r53`, {
            domainName: "example.com",
        });

        // /* ---------- R53 Record ---------- */
        new r53.ARecord(this, `r53-record`, {
            zone: hostzone,
            recordName: `www.example.com`,
            // target: r53.RecordTarget.fromIpAddresses(eip.attrPublicIp), // Certbot設定後はコメントアウト
            target: r53.RecordTarget.fromAlias(new r53_tgt.LoadBalancerTarget(alb)),
        });

    }

}

```

これでエンドツーエンドでの