NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

NLBを使ってターゲットコンテナでクライアント認証させる方法

本記事は  【コンテナウィーク】  2日目の記事です。
💻  1日目  ▶▶ 本記事 ▶▶  3日目  📱

始めまして、堀と申します。2022年4月にキャリアでネットコムに入社して、約1年半AWSをメインにインフラの開発・保守・運用をしています。

今回は業務で行ったNLBを使ってターゲットのNginxコンテナでクライアント認証させる方法を紹介します。

構成

構成図はこちらです。

非常にシンプルですね。流れとしては下記になります。

  1. NLBにリクエストを投げる。
  2. リクエストがNLBを通過しそのままNginxを起動しているFargateコンテナに到達し、クライアント認証する。

今回の要件としてはロードバランサでSSL終端させるのではなく、nginxでSSL終端させるようにする必要がありました。 ALBだとクライアント認証をせずにリクエストを通過させることができないため、ALBではなくNLBを採用しています。

また証明書のデータについてはそれぞれSSM Parameter Storeに格納しています。 こちらをタスク実行時に環境変数として取得させるようにしています。

やってみた

今回リソースは全てTerraformで作成しました。コードはこちらです。 他にもネットワーク周りのリソースやIAMロールなど作成していますが、全て記述すると結構な量になるため抜粋します。

NLB
resource "aws_lb" "this" {
  name               = "nlb"
  load_balancer_type = "network"
  subnets            = ["subnet-xxxxxxxxxxxxx]
}

resource "aws_lb_listener" "this" {
  port              = 443
  protocol          = "TCP"
  load_balancer_arn = aws_lb.this.arn

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

resource "aws_lb_target_group" "this" {
  name                 = "tg"
  port                 = 443
  protocol             = "TCP"
  target_type          = "ip"
  vpc_id               = "vpc-xxxxxxxxxxxxx"
  deregistration_delay = 300

  health_check {
    port                = 443
    protocol            = "TCP"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }
}
ECS
resource "aws_ecr_repository" "this" {
  name                 = "nginx"
  image_tag_mutability = "MUTABLE"

  encryption_configuration {
    encryption_type = "KMS"
  }

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecs_cluster" "this" {
  name = "nginx"
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_service" "this" {
  name                               = nginx
  cluster                            = resource.aws_ecs_cluster.this.arn
  task_definition                    = resource.aws_ecs_task_definition.nginx.arn
  launch_type                        = "FARGATE"
  platform_version                   = "LATEST"
  propagate_tags                     = "SERVICE"
  wait_for_steady_state              = true
  desired_count                      = 1
  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100
  health_check_grace_period_seconds  = 360

  load_balancer {
    target_group_arn = resource.aws_lb_target_group.this.arn
    container_name   = "nginx"
    container_port   = 443
  }

  network_configuration {
    subnets          = ["subnet-xxxxxxxxxxxxx"]
    security_groups  = ["sg-xxxxxxxxxxxxx"]
    assign_public_ip = true
  }

}


resource "aws_ecs_task_definition" "nginx" {
  family                   = "nginx"
  task_role_arn            = "arn:aws:iam::xxxxxxxxxxxx:role/ecs-task-role"
  execution_role_arn       = "arn:aws:iam::xxxxxxxxxxxx:role/ecs-task-execution-role"
  network_mode             = "awsvpc"
  cpu                      = 1024
  memory                   = 2048
  requires_compatibilities = ["FARGATE"]

  container_definitions = jsonencode([
    {
      name      = "nginx"
      image     = "xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/nginx:latest"
      cpu       = 1024
      memory    = 2048
      essential = true
      portMappings = [
        {
          containerPort = 443
          hostPort      = 443
        }
      ],
      logConfiguration = {
        logDriver = "awslogs",
        options = {
          "awslogs-region"        = "us-east-1",
          "awslogs-group"         = "/ecs/nginx",
          "awslogs-stream-prefix" = "nginx"
        }
      }
      secrets = [
        {
          "name" : "SSL_SERVER_CERTIFICATE",
          "valueFrom" : "arn:aws:ssm:us-east-1:xxxxxxxxxxxx:parameter/SSL_SERVER_CERTIFICATE"
        },
        {
          "name" : "SSL_SERVER_KEY",
          "valueFrom" : "arn:aws:ssm:us-east-1:xxxxxxxxxxxx:parameter/SSL_SERVER_KEY"
        },
        {
          "name" : "SSL_CLIENT_CERTIFICATE",
          "valueFrom" : "arn:aws:ssm:us-east-1:xxxxxxxxxxxx:parameter/SSL_CLIENT_CERTIFICATE"
        },
      ]
    }
  ])
}
Dockerfile

続いてnginxコンテナの設定ファイルです。 まずDockerfileですが下記になります。

FROM public.ecr.aws/docker/library/nginx:1.23-alpine

RUN apk update && \
  apk upgrade

COPY ./conf/ssl.conf /etc/nginx/conf.d/ssl.conf
COPY ./init.sh /docker-entrypoint.d/
ssl.conf

sslの設定情報を/etc/nginx/conf.d/の下に格納しています。 ssl_verify_client とssl_client_certificateディレクティブを設定することでクライアント認証を有効化しています。

server {
    listen       443 ssl;
    server_name  _;

    ssl_certificate      /etc/nginx/certs/server_cert.cer;
    ssl_certificate_key  /etc/nginx/certs/server_key.key;

    ssl_verify_client      on;
    ssl_client_certificate /etc/nginx/certs/client_cert.pem;

    ssl_session_timeout  5m;

    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers   on;

    proxy_set_header Host             $host;
    proxy_set_header X-Real-IP        $remote_addr;
    proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-User $remote_user;
    proxy_set_header X-Forwarded-Proto https;
    add_header X-Content-Type-Options nosniff;

    client_body_buffer_size 64k;


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

こちらのシェルスクリプトをdocker-entrypoint.dに配置し、コンテナ起動時に実行させます。 処理としては環境変数として渡した証明書の値をファイルに書き込み適切な場所に配置します。

#!/bin/sh
set -e

: "${SSL_SERVER_CERTIFICATE:=}"
: "${SSL_SERVER_KEY:=}"
: "${SSL_CLIENT_CERTIFICATE:=}"

mkdir -p /etc/nginx/certs

echo "${SSL_SERVER_CERTIFICATE}" > /etc/nginx/certs/server_cert.cer
echo "${SSL_SERVER_KEY}" > /etc/nginx/certs/server_key.key
echo "${SSL_CLIENT_CERTIFICATE}" > /etc/nginx/certs/client_cert.pem

これでbuildしたコンテナイメージをecrにpushして、terraform apply実行するとリソースが作成されました。 それではクライアント認証できるかどうか確認するため、NLBにcurlコマンド叩いてみます。今回証明書はオレオレ証明書で作成しているため、-kをつけて実行しています。

するとこのようにNLBをそのまま通過してNginxにHTTPSで接続しクライアント認証が行われていることが確認できました。

❯ curl -I -v -k  --key /cert/CLIENT_CERT.key --cert /cert/CLIENT_CERT-ca.pem https://nlb-xxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com/
*   Trying xxx.xxx.xxx.xxx:8080...
* Connected to hogehoge.com (xxx.xxx.xxx.xxx) port 8080
* CONNECT tunnel: HTTP/1.1 negotiated
* allocate connect buffer
* Proxy auth using Basic with user 'hoge'
* Establish HTTP proxy tunnel to nlb-xxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:443
> CONNECT nlb-xxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:443 HTTP/1.1
> Host: nlb-xxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:443
> Proxy-Authorization: Basic ay1ob3JpOkIzZnBLemdI
> User-Agent: curl/8.4.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.0 200 Connection established
HTTP/1.0 200 Connection established
<

* CONNECT phase completed
* CONNECT tunnel established, response 200
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: C=JP; ST=Osaka; L=Osaka; O=dev; OU=dev
*  start date: Feb 13 05:36:11 2023 GMT
*  expire date: Feb 10 05:36:11 2033 GMT
*  issuer: C=JP; ST=Osaka; L=Osaka; O=dev; OU=dev
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
* using HTTP/1.1
> HEAD / HTTP/1.1
> Host: xxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com
> User-Agent: curl/8.4.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: nginx/1.23.4
Server: nginx/1.23.4
< Date: Thu, 09 Nov 2023 11:39:42 GMT
Date: Thu, 09 Nov 2023 11:39:42 GMT
< Content-Type: text/html
Content-Type: text/html
< Content-Length: 615
Content-Length: 615
< Last-Modified: Tue, 28 Mar 2023 17:09:24 GMT
Last-Modified: Tue, 28 Mar 2023 17:09:24 GMT
< Connection: keep-alive
Connection: keep-alive
< ETag: "64231f44-267"
ETag: "64231f44-267"
< X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
< Accept-Ranges: bytes
Accept-Ranges: bytes

<

まとめ

最後までご覧いただきありがとうございます。

正直認証周りを触ったり考えたりするのが初めてだったため、SSLとは?というところの理解から始める必要があり色々苦労することも多かったです。一方で、周辺知識も合わせて幅広く理解できたと感じるので、今後はより深いところまで理解できるように勉強したいなあと思いました。同じような構成で悩まれている方の一助となれば幸いです。

執筆者: 堀 晃太郎 クラウドエンジニア