0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

シンプルな構成ですと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

セットアップ(CDKプロジェクト作成 〜 EC2作成)

https://letsencrypt.org/getting-started/
https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal

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

CDKのインストールから開始する際は、こちらからお願いします。

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

mkdir end_to_end_encryption
cd end_to_end_encryption
cdk init app --language typescript

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

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

certbot certificates

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

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

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

ドライランは以下の通り。

certbot renew --dry-run

証明書の更新は以下の通り.

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

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

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

./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)),
        });

    }

}

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?