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とは?というところの理解から始める必要があり色々苦労することも多かったです。一方で、周辺知識も合わせて幅広く理解できたと感じるので、今後はより深いところまで理解できるように勉強したいなあと思いました。同じような構成で悩まれている方の一助となれば幸いです。

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