はじめに
昨年のre:Invent 2023で発表されたアップデートにより、AWSのApplication Load Balancer(ALB)はmTLS(相互TLS認証)をサポートするようになりました。
これにより、クライアント認証が以前よりも格段に簡単に実装できるようになりました。
現在関わっているプロジェクトで、この機能を取り入れるということで検証も兼ねて構築してみたいと思います。
TLS認証とmTLS認証の概要図
そもそもTLSとは?
TLS (Transport Layer Security) は、インターネット上でデータを安全に送受信するために広く利用されるプロトコルです。
SSL (Secure Sockets Layer) の後継として開発され、データの暗号化、通信相手の認証、データの完全性保証を提供します。
TLS認証では、サーバ証明書を利用することで、クライアント側にアクセスしたサーバの身元を確認させ、これによりクライアントは自分が信頼できるサイトに情報を送信していることを確認することができます。
そもそもmTLSとは?
mTLS (Mutual Transport Layer Security) は、TLSプロトコルの一形態であり、双方向の認証プロセスを実現します。
mTLS認証では、従来のTLSがサーバ証明書のみを要求していたのに対し、クライアントも自身の証明書を用いて身元を証明することが求められます。
この相互認証により、通信の両端点が正当であることを互いに確認できるため、セキュリティが強化されます。
これによって、特定のクライアントのみがサーバへアクセスできるように制限することが可能となります。
構築ハンズオン
1.CA証明書の生成
opensslコマンドで CA証明書を生成
作成したCA証明書は、この後の作業で利用するため自身のS3に配置する
#秘密鍵の生成
openssl genrsa -out rootCA_key.pem 2048
#CA証明書の生成
openssl req \
-x509 \
-new \
-days 7 \
-key rootCA_key.pem \
-out rootCA_cert.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:root-ca
Email Address []:
#確認
ls -la
rootCA_cert.pem
rootCA_key.pem
※rootCA_cert.pem
については、この後のトラストストア作成時に利用するため、自身のS3に配置する。
2.クライアント証明書の生成
自身環境でそのままクライアント証明書を生成するコマンドを実施すると、バージョン1フォーマットで作成されてしまい クライアント証明書が失敗する。
そのため、バージョン3フォーマットを利用(x509v3 拡張属性を含ませる)ため、v3_req.txt
を作成。
参照:証明書に x509v3 拡張属性を追加する
vi v3_req.txt
cat v3_req.txt
extendedKeyUsage = serverAuth, clientAuth, codeSigning, emailProtection
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
opensslコマンドで クライアント証明書を生成
# クライアントの秘密鍵とCSRの生成
openssl req -new -nodes -keyout client_key.pem -out client_csr.pem
# クライアント証明書の生成
openssl x509 \
-req \
-CA rootCA_cert.pem \
-CAkey rootCA_key.pem \
-CAcreateserial -days 7 \
-in client_csr.pem \
-out client_cert.pem \
-extfile v3_req.txt
# 確認
ls -la
client_cert.pem
client_csr.pem
client_key.pem
rootCA_cert.pem
rootCA_cert.srl
rootCA_key.pem
v3_req.txt
# 内容確認
# Version: 3になっていること、作成したCAで署名されていることが確認できる
openssl x509 -text -noout -in client_cert.pemCertificate:
Data:
Version: 3 (0x2)
Serial Number:
dd:a5:37:a4:3d:aa:5e:23
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=JP, ST=Tokyo, L=Default City, O=Default Company Ltd, CN=root-ca
Validity
Not Before: Feb 17 23:13:31 2024 GMT
Not After : Feb 24 23:13:31 2024 GMT
Subject: C=JP, ST=Tokyo, L=Default City, O=Default Company Ltd, CN=client-ca
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d9:6c:f2:bb:13:f5:4a:dd:97:aa:6e:38:e6:50:
6f:07:55:15:be:6f <省略> :56:a3:26:c7:39:03:
3d:78:ba:60:2b:98:1b:86:a2:82:70:39:d4:38:df:
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication, Code Signing, E-mail Protection
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
Signature Algorithm: sha256WithRSAEncryption
05:51:7a:5c:88:b1:01:8a:60:a7:c2:8b:d8:47:7c:b2:4a:48:
34:ab:2b:04:22:f2:5 <省略> 7:f9:4a:26:2d:b3:ac::ac:
ea:02:bb:7e:23:33:86:c2:31:73:cf:d9:20:be:fd:50:ae:99:
3.Lambda構築
ALBターゲットとなるLambda関数の作成
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps('Hello mTLS !!')
}
4. ALB構築
Cloudshellを利用してALB構築
#構築前準備
export VPC_ID=<VPC-ID>
export SUBNET_A=<Subnet-A-ID>
export SUBNET_B=<Subnet-B-ID>
export MY_IP=<MY-IP>
export LAMBDA_NAME=<手順3で構築したLambdaの名前>
export LAMBDA_ARN=<手順3で構築したLambdaのARN>
export BUCKET_NAME=<手順1で作成したRoot証明書を配置したバケット名>
export FQDN_DOMAIN_NAME=<ALB HTTPS化のためのACM証明で利用するドメイン名>
# ACMのARN取得
export ACM_ARN=$(aws acm list-certificates \
--output json | jq -r '.CertificateSummaryList[] | select(.DomainName==env.FQDN_DOMAIN_NAME) | .CertificateArn')
#ALBのSG作成
export SG_ID=$(aws ec2 create-security-group \
--group-name tetutetu-sg-for-alb \
--description "SG for ALB with access from my IP only" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 443 \
--cidr ${MY_IP}/32
# ターゲットグループ作成
export TG_ARN=$(aws elbv2 create-target-group \
--name tetutetu-alb-tg-mtls \
--target-type lambda \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
# ALBがLambdaを呼び出すための権限設定
aws lambda add-permission \
--function-name $LAMBDA_NAME \
--statement-id load-balancer \
--principal elasticloadbalancing.amazonaws.com \
--action lambda:InvokeFunction \
--source-arn $TG_ARN
# ターゲットグループにLambdaを登録
aws elbv2 register-targets \
--target-group-arn $TG_ARN \
--targets Id=$LAMBDA_ARN
# トラストストア作成
export TRUSTSTORE_ARN=$(aws elbv2 create-trust-store --name tetutetu-trust-store \
--ca-certificates-bundle-s3-bucket $BUCKET_NAME \
--ca-certificates-bundle-s3-key 20240218/rootCA_cert.pem \
--query 'TrustStores[0].TrustStoreArn' --output text)
#ALB作成
export ALB_ARN=$(aws elbv2 create-load-balancer --name tetutetu-alb-mtls \
--subnets ${SUBNET_A} ${SUBNET_B} --security-groups ${SG_ID} \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
# リスナー作成
aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=$ACM_ARN \
--mutual-authentication Mode=verify,TrustStoreArn=$TRUSTSTORE_ARN \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
Route53の設定
#構築前準備
export DOMAIN_NAME=<ホストゾーン名>
#ALBのDNS名取得
ALB_DNS_NAME=$(aws elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
#ホストゾーンのID取得
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name \
--dns-name $DOMAIN_NAME \
--query 'HostedZones[0].Id' \
--output text)
#Route53にエイリアスレコードを作成(ALBがap-northeast-1の場合、HostedZonedIdは'Z14GRHDCWA56QT')
aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch '{
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "'"$FQDN_DOMAIN_NAME"'",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z14GRHDCWA56QT",
"DNSName": "'"$ALB_DNS_NAME"'",
"EvaluateTargetHealth": false
}
}
}
]
}'
動作確認
クライアントの秘密鍵及び、クライアント証明書を利用してTLSクライアント認証が確認できる
# 成功
curl --key client_key.pem --cert client_cert.pem https://<<FQDN_DOMAIN_NAME>>
"Hello mTLS !!"
# エラーが返却される
curl https://<<FQDN_DOMAIN_NAME>>
curl: (35) Recv failure: Connection reset by peer
さいごに
バックエンドでクライアント認証をしていない場合、容易に導入すること考えらるかと思いました。
クライアント証明がバージョン3形式ではないといけない部分で、かなり詰まりました。以下記事を参考させていただきました。