本記事は
【コンテナウィーク】
2日目の記事です。
💻
1日目
▶▶ 本記事 ▶▶
3日目
📱
始めまして、堀と申します。2022年4月にキャリアでネットコムに入社して、約1年半AWSをメインにインフラの開発・保守・運用をしています。
今回は業務で行ったNLBを使ってターゲットのNginxコンテナでクライアント認証させる方法を紹介します。
構成
構成図はこちらです。
非常にシンプルですね。流れとしては下記になります。
- NLBにリクエストを投げる。
- リクエストが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とは?というところの理解から始める必要があり色々苦労することも多かったです。一方で、周辺知識も合わせて幅広く理解できたと感じるので、今後はより深いところまで理解できるように勉強したいなあと思いました。同じような構成で悩まれている方の一助となれば幸いです。