こしあん
2025-01-07

ALBでSSLを終端し、EC2間をHTTPSで暗号化してみる


5{icon} {views}

ACM証明書を利用してALBでSSLを終端し、自己署名証明書を使ってALBからEC2までの通信もHTTPS化する手順をTerraformで解説。Route 53の独自ドメイン設定を組み合わせることで、ブラウザからのアクセスからEC2間まで全て暗号化された安全な構成を実現する。

はじめに

  • SCSを勉強していてよく出てきたSSLのTermination
  • ALBを一旦SSLの末端として、そこからEC2までの通信をHTTPではなくHTTPSとしてサードパーティのSSL証明書をEC2に接続するというもの
  • SSL TerminationをすることでEC2までの接続がオールHTTPSになり、セキュアになるよというもの

SSL Termination とは

SSL Terminationとは、クライアントとサーバー間のSSL/TLS暗号化通信を特定のポイント(この場合はALB)で終端し、その後の通信を平文または再暗号化された状態で内部ネットワークに転送するプロセス。

やりかた

  • ALBをHTTPSにするためにACMで発行したSSL証明書が必要。これにはカスタムドメインが必要
    • Route 53で別途ドメインを発行し(.clickドメインが3ドルなので、テスト用にはいい)、パブリックホストゾーンを作っておく
    • ACMから証明書を発行し、Route 53にCNAMEレコードを登録しておく
  • EC2側は外部認証したサードパーティの証明書だが、今回は面倒なので自己署名証明書(オレオレ証明書)で代用する

その他前提

  • VPCとパブリックサブネット、プライベートサブネットは作成済みとする
  • プライベートサブネットには、NAT(ゲートウェイ)、実際はfck-natを使ったNATインスタンスを用意しておく
  • Route 53のドメインと、ACMの証明書は作成済みとする

Terraformのコード

コード構成は以下の通り

.  
├── main.tf  
├── alb.tf  
└── ec2.tf  

main.tf

以下の変数を定義する

variable "public_subnet_ids" {
  description = "ALBを配置するパブリックサブネットのID"
  type        = list(string)
}

variable "private_subnet_ids" {
  description = "ALBを配置するプライベートサブネットのID"
  type        = list(string)
}

variable "vpc_id" {
  description = "VPCのID"
  type        = string
}

variable "certificate_arn" {
  description = "ALBで使用するACM証明書のARN"
  type        = string
}

variable "domain_name" {
  description = "証明書に紐づけるドメイン名。例: example.com"
  type        = string
}

variable "hosted_zone_id" {
  description = "Route 53ホストゾーンのID"
  type        = string
}

ドメイン名はwww.example.comだったら、example.comとする。これはEC2内の自己署名証明書の作成で使用する(ここはユーザースクリプトで実装)

ec2.tf

  • 2つのプライベートサブネットにまたがるEC2インスタンス(Webサーバー)を作成
    • OSはAmazon Linux2023を使用
    • セキュリティグループは、ALBのセキュリティグループからのインバウンド443のみ許可。よって、ALBからはHTTPS接続のみ許可され、HTTP接続(80)は弾かれる
  • EC2のユーザースクリプト内で、nginx(Webサーバー)と、OpenSSLをインストール。OpenSSLは自己署名証明書の発行に作成。
  • ちゃんとした開発では自己署名証明書ではなく、外部認証された証明書を使うこと
resource "aws_security_group" "ec2_sg" {
  name        = "ec2_sg"
  description = "A security group for EC2 instances"
  vpc_id      = var.vpc_id

  ingress {
    description     = "Allow HTTPS from ALB"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Amazon Linux 2023
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["137112412989"] # AmazonのAMI所有者ID

  filter {
    name = "name"
    # Amazon Linux 2023 AMIの名前パターン。minimumを除外する
    values = ["al2023-ami-2023*-kernel-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# WebServerのEC2
resource "aws_instance" "web_server" {
  count                  = 2
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = element(var.private_subnet_ids, count.index % length(var.public_subnet_ids))
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  user_data = <<-EOF
              #!/bin/bash
              dnf update -y
              dnf install -y nginx openssl

              mkdir /etc/nginx/ssl
              openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
                -keyout /etc/nginx/ssl/selfsigned.key \
                -out /etc/nginx/ssl/selfsigned.crt \
                -subj "/C=JP/ST=Tokyo/L=Chiyoda/O=Example Company/OU=IT Department/CN=${var.domain_name}"

              cat > /etc/nginx/conf.d/default.conf <<EOL
              server {
                  listen 443 ssl;
                  server_name ${var.domain_name};

                  ssl_certificate /etc/nginx/ssl/selfsigned.crt;
                  ssl_certificate_key /etc/nginx/ssl/selfsigned.key;

                  location / {
                      root /usr/share/nginx/html;
                      index index.html index.htm;
                  }
              }
              EOL

              systemctl enable nginx
              systemctl start nginx
              EOF

  tags = {
    Name = "WebServer-${count.index + 1}"
  }
}

alb.tf

  • HTTPS接続(443)のみ許可するセキュリティグループをALBにアタッチ
  • ターゲットグループはHTTPSとして、作成した2つのEC2インスタンスをアタッチする
  • リスナールールはHTTPS(443)として、ここにACMの証明書をアタッチする
  • 独自ドメインと紐づけるために、Route 53にエイリアス(A)レコードを追加する
resource "aws_security_group" "alb_sg" {
  name        = "alb_sg"
  description = "A security group for ALB"
  vpc_id      = var.vpc_id

  ingress {
    description      = "Allow HTTPS inbound"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}


resource "aws_lb" "application_lb" {
  name               = "application-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = var.public_subnet_ids

  enable_deletion_protection = false
}

resource "aws_lb_target_group" "alb_target_group" {
  name     = "alb-target-group"
  port     = 443
  protocol = "HTTPS"
  vpc_id   = var.vpc_id

  health_check {
    protocol            = "HTTPS"
    port                = "443"
    path                = "/"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 5
    unhealthy_threshold = 2
    matcher             = "200-299"
  }

  # 証明書の指定はリスナーで行うため不要
}

resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.application_lb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = var.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_target_group.arn
  }
}

resource "aws_lb_target_group_attachment" "tg_attachment" {
  count            = 2
  target_group_arn = aws_lb_target_group.alb_target_group.arn
  target_id        = aws_instance.web_server[count.index].id
  port             = 443
}

resource "aws_route53_record" "alb_alias" {
  zone_id = var.hosted_zone_id
  name    = "www.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_lb.application_lb.dns_name
    zone_id                = aws_lb.application_lb.zone_id
    evaluate_target_health = true
  }
}

結果

ブラウザからアクセスすると自己署名証明書の警告は出るものの、独自ドメインとHTTPSで接続できた(nginxの画面が出るのが想定された挙動)。

EC2のセキュリティグループを見ると、HTTP(80)のインバウンドが許可されていないので、ALB→EC2間も暗号化されている。

所感

  • 難しそうかと思ったけど意外と簡単だった。一回やってみるものだね
  • NLBでTCPパススルーを構成し、EC2にHTTPSで接続するのは試してなかったけどやってみたくなったらで!


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です