🍉

AWS Client VPNで相互認証エンドポイントを構築する完全ガイド

に公開

AWS Client VPNは、スケーラブルで安全なVPN接続を提供するマネージドサービスです。今回は、証明書による相互認証を使用したVPNエンドポイントの構築手順を詳しく解説します。特に、AWS Client VPNの動的エンドポイント構造に対応した2レベルワイルドカード証明書の作成から、実際の接続確認まで、実践的な内容をお届けします。

相互認証とは

相互認証(Mutual Authentication)は、クライアントとサーバーが互いの身元を証明書で検証する認証方式です。

従来の認証との違い

従来の認証(サーバー認証のみ):

  • クライアント → サーバーの正当性を検証
  • ユーザー名/パスワードでクライアント認証

相互認証:

  • クライアント → サーバーの正当性を検証
  • サーバー → クライアントの正当性を証明書で検証
  • より高いセキュリティレベルを実現

構築手順

1. 証明書の作成

まず、相互認証に必要な証明書を作成します。

AWS Client VPNエンドポイントは、作成時に動的なFQDNが割り当てられます:

例:[RANDOM_PREFIX].cvpn-endpoint-[ENDPOINT_ID].prod.clientvpn.[REGION].amazonaws.com

この構造を分析すると:

  • [RANDOM_PREFIX] - ランダムなプレフィックス(予測不可能)
  • cvpn-endpoint-[ENDPOINT_ID] - エンドポイントID
  • prod.clientvpn.[REGION].amazonaws.com - 固定サフィックス

従来のワイルドカード証明書の問題点:

  • *.prod.clientvpn.[REGION].amazonaws.com では1レベルのサブドメインのみ対応
  • 実際のエンドポイントは2レベル(ランダム.cvpn-endpoint-ID.prod)なので証明書エラーが発生

2レベルワイルドカード対応の解決策:

  • *.*.prod.clientvpn.[REGION].amazonaws.com - 2レベル対応(必要十分)

実際のAWS Client VPNエンドポイントは以下の構造です:

  • AWS提供のDnsName: *.cvpn-endpoint-[ID].prod.clientvpn.[REGION].amazonaws.com
  • 実際の接続先: [RANDOM].cvpn-endpoint-[ID].prod.clientvpn.[REGION].amazonaws.com

つまり、2レベルワイルドカード(*.*.prod.clientvpn.[REGION].amazonaws.com)で十分対応可能です。

証明書作成スクリプト

以下は、AWS Client VPN相互認証に必要な証明書を作成する完全なスクリプトです:

#!/bin/bash
# 2レベルワイルドカード対応のAWS Client VPN証明書作成スクリプト

set -e
TIMESTAMP=$(date +%Y%m%d)

echo "=== AWS Client VPN証明書作成 ==="
echo "タイムスタンプ: ${TIMESTAMP}"

# ファイル名定義
CA_KEY="vpn-ca-ultra-${TIMESTAMP}.key"
CA_CERT="vpn-ca-ultra-${TIMESTAMP}.crt"
SERVER_KEY="vpn-server-ultra-${TIMESTAMP}.key"
SERVER_CERT="vpn-server-ultra-${TIMESTAMP}.crt"
SERVER_CSR="vpn-server-ultra-${TIMESTAMP}.csr"
CLIENT_KEY="vpn-client-ultra-${TIMESTAMP}.key"
CLIENT_CERT="vpn-client-ultra-${TIMESTAMP}.crt"
CLIENT_CSR="vpn-client-ultra-${TIMESTAMP}.csr"

echo "1. CA証明書作成(CA=TRUE critical設定)..."

# CA秘密鍵生成
openssl genrsa -out ${CA_KEY} 2048

# CA証明書用設定ファイル作成
cat > ca_config_ultra_${TIMESTAMP}.conf << EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no

[req_distinguished_name]
C = JP
ST = Tokyo
L = Tokyo
O = [COMPANY_NAME]
OU = IT Department
CN = [COMPANY_NAME] VPN CA
emailAddress = [EMAIL_ADDRESS]

[v3_ca]
basicConstraints = critical,CA:TRUE
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
EOF

# CA証明書作成(自己署名、CA=TRUE明示)
openssl req -new -x509 -key ${CA_KEY} -out ${CA_CERT} -days 3650 \
  -config ca_config_ultra_${TIMESTAMP}.conf -extensions v3_ca

echo "2. サーバー証明書作成(2レベルワイルドカード対応)..."

# サーバー秘密鍵生成
openssl genrsa -out ${SERVER_KEY} 2048

# サーバー証明書署名要求作成
openssl req -new -key ${SERVER_KEY} -out ${SERVER_CSR} \
  -subj "/C=JP/ST=Tokyo/L=Tokyo/O=[COMPANY_NAME]/OU=IT Department/CN=vpn-server/emailAddress=[EMAIL_ADDRESS]"

# サーバー証明書用拡張設定(2レベルワイルドカード)
cat > server_ext_ultra_${TIMESTAMP}.conf << EOF
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
subjectAltName = DNS:*.*.prod.clientvpn.[REGION].amazonaws.com
EOF

# サーバー証明書署名
openssl x509 -req -in ${SERVER_CSR} -CA ${CA_CERT} -CAkey ${CA_KEY} -CAcreateserial \
  -out ${SERVER_CERT} -days 3650 -extfile server_ext_ultra_${TIMESTAMP}.conf

echo "3. クライアント証明書作成..."

# クライアント秘密鍵生成
openssl genrsa -out ${CLIENT_KEY} 2048

# クライアント証明書署名要求作成
openssl req -new -key ${CLIENT_KEY} -out ${CLIENT_CSR} \
  -subj "/C=JP/ST=Tokyo/L=Tokyo/O=[COMPANY_NAME]/OU=IT Department/CN=client1.[DOMAIN]/emailAddress=[EMAIL_ADDRESS]"

# クライアント証明書用拡張設定
cat > client_ext_ultra_${TIMESTAMP}.conf << EOF
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
subjectAltName = DNS:client1.[DOMAIN]
EOF

# クライアント証明書署名
openssl x509 -req -in ${CLIENT_CSR} -CA ${CA_CERT} -CAkey ${CA_KEY} -CAcreateserial \
  -out ${CLIENT_CERT} -days 3650 -extfile client_ext_ultra_${TIMESTAMP}.conf

echo "4. 証明書検証..."
echo "CA証明書のBasic Constraints確認:"
openssl x509 -in ${CA_CERT} -noout -text | grep -A 3 "Basic Constraints"

echo "サーバー証明書検証:"
openssl verify -CAfile ${CA_CERT} ${SERVER_CERT}

echo "クライアント証明書検証:"
openssl verify -CAfile ${CA_CERT} ${CLIENT_CERT}

echo "5. サーバー証明書のSAN確認:"
openssl x509 -in ${SERVER_CERT} -noout -text | grep -A 10 "Subject Alternative Name"

# 一時ファイル削除
rm ${SERVER_CSR} ${CLIENT_CSR} ca_config_ultra_${TIMESTAMP}.conf server_ext_ultra_${TIMESTAMP}.conf client_ext_ultra_${TIMESTAMP}.conf

echo "=== 作成完了 ==="
echo "作成されたファイル:"
echo "- CA証明書: ${CA_CERT}"
echo "- CA秘密鍵: ${CA_KEY}"
echo "- サーバー証明書: ${SERVER_CERT}"
echo "- サーバー秘密鍵: ${SERVER_KEY}"
echo "- クライアント証明書: ${CLIENT_CERT}"
echo "- クライアント秘密鍵: ${CLIENT_KEY}"

重要なポイント

1. CA証明書のBasic Constraints設定

CA証明書ではbasicConstraints = critical,CA:TRUEの設定が必須です:

[v3_ca]
basicConstraints = critical,CA:TRUE  # CA権限を明示的に付与
keyUsage = critical,keyCertSign,cRLSign  # 証明書署名権限
subjectKeyIdentifier = hash

この設定により、CA証明書が他の証明書に署名する権限を持つことが保証されます。

2. サーバー証明書の拡張設定

extendedKeyUsageserverAuthclientAuthの両方を指定することで、同じ証明書をサーバー認証とクライアント認証の両方に使用できます:

extendedKeyUsage = serverAuth,clientAuth  # 重要:両方指定
subjectAltName = DNS:*.*.prod.clientvpn.[REGION].amazonaws.com

なぜ同じ証明書を両方に使用するのか:

AWS Client VPNでは以下の設定が可能です:

  • ServerCertificateArn: サーバー認証用(クライアントがサーバーを検証)
  • ClientRootCertificateChain: クライアント証明書を検証するためのCA証明書

同じ証明書ARNを両方に指定することで:

  • 証明書管理の簡素化
  • ACMでの証明書数削減
  • 運用コストの削減

ただし、この場合サーバー証明書にはserverAuthclientAuthの両方のExtended Key Usageが必要になります。

3. 証明書検証の重要性

スクリプトには証明書の検証ステップが含まれており、以下を確認できます:

  • CA証明書のBasic Constraints設定
  • 証明書チェーンの正当性
  • サーバー証明書のSAN(Subject Alternative Name)設定

2. 証明書のセキュアな管理(Parameter Store)

証明書と秘密鍵を安全に管理するため、AWS Systems Manager Parameter Storeを使用します。

Parameter Storeへの登録:

# CA秘密鍵(SecureString)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/ca-private-key" \
  --description "AWS Client VPN用CA秘密鍵" \
  --type "SecureString" \
  --value file://vpn-ca-ultra-20250730.key

# サーバー秘密鍵(SecureString)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/server-private-key" \
  --description "AWS Client VPN用サーバー秘密鍵" \
  --type "SecureString" \
  --value file://vpn-server-ultra-20250730.key

# クライアント秘密鍵(SecureString)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/client-private-key" \
  --description "AWS Client VPN用クライアント秘密鍵" \
  --type "SecureString" \
  --value file://vpn-client-ultra-20250730.key

# CA証明書(String)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/ca-certificate" \
  --description "AWS Client VPN用CA証明書" \
  --type "String" \
  --value file://vpn-ca-ultra-20250730.crt

# サーバー証明書(String)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/server-certificate" \
  --description "AWS Client VPN用サーバー証明書" \
  --type "String" \
  --value file://vpn-server-ultra-20250730.crt

# クライアント証明書(String)
aws ssm put-parameter \
  --name "/vpn/certificates/20250730/client-certificate" \
  --description "AWS Client VPN用クライアント証明書" \
  --type "String" \
  --value file://vpn-client-ultra-20250730.crt

Terraformでの管理例:

# CA秘密鍵の管理
resource "aws_ssm_parameter" "vpn_ca_private_key" {
  name        = "/vpn/certificates/${local.certificate_version}/ca-private-key"
  description = "AWS Client VPN用CA秘密鍵"
  type        = "SecureString"
  value       = file("vpn-ca-ultra-${local.certificate_version}.key")

  tags = {
    Name        = "vpn-ca-private-key"
    Type        = "ca-private-key"
    Usage       = "certificate-signing"
    Environment = "production"
  }
}

# サーバー秘密鍵の管理
resource "aws_ssm_parameter" "vpn_server_private_key" {
  name        = "/vpn/certificates/${local.certificate_version}/server-private-key"
  description = "AWS Client VPN用サーバー秘密鍵"
  type        = "SecureString"
  value       = file("vpn-server-ultra-${local.certificate_version}.key")

  tags = {
    Name        = "vpn-server-private-key"
    Type        = "server-private-key"
    Usage       = "server-authentication"
    Environment = "production"
  }
}

Parameter Storeを使用する利点:

  • 暗号化: SecureStringによる秘密鍵の暗号化保存
  • アクセス制御: IAMによる細かいアクセス権限設定
  • 監査: CloudTrailによるアクセスログ記録
  • バージョン管理: 証明書更新時の履歴管理
  • 自動化: TerraformやLambdaからの自動取得

3. AWS Certificate Manager (ACM) への証明書インポート

Parameter Storeから証明書を取得してACMにインポートします。

Parameter Storeからの取得とACMインポート:

# Parameter Storeから証明書を取得
aws ssm get-parameter \
  --name "/vpn/certificates/20250730/server-certificate" \
  --output text --query 'Parameter.Value' > server.crt

aws ssm get-parameter \
  --name "/vpn/certificates/20250730/server-private-key" \
  --with-decryption \
  --output text --query 'Parameter.Value' > server.key

aws ssm get-parameter \
  --name "/vpn/certificates/20250730/ca-certificate" \
  --output text --query 'Parameter.Value' > ca.crt

# ACMにインポート
aws acm import-certificate \
  --certificate fileb://server.crt \
  --private-key fileb://server.key \
  --certificate-chain fileb://ca.crt \
  --region ap-northeast-1

# 一時ファイルを削除
rm server.crt server.key ca.crt

4. Terraformでのエンドポイント作成

resource "aws_ec2_client_vpn_endpoint" "mutual_auth_vpn" {
  client_cidr_block = "192.168.x.0/22"
  description       = "Mutual Authentication VPN Endpoint"
  vpc_id            = var.vpc_id

  dns_servers = ["[DNS_SERVER_1]", "[DNS_SERVER_2]"]
  
  self_service_portal    = "enabled"
  split_tunnel           = false
  transport_protocol     = "udp"
  vpn_port              = 443
  session_timeout_hours = 8

  # サーバー証明書ARN
  server_certificate_arn = aws_acm_certificate.server_cert.arn

  authentication_options {
    type = "certificate-authentication"
    # 同じサーバー証明書ARNを使用(serverAuth + clientAuth対応のため)
    root_certificate_chain_arn = aws_acm_certificate.server_cert.arn
  }

  connection_log_options {
    enabled               = true
    cloudwatch_log_group  = aws_cloudwatch_log_group.client_vpn.name
    cloudwatch_log_stream = aws_cloudwatch_log_stream.client_vpn.name
  }

  tags = {
    Name = "mutual-auth-vpn-endpoint"
  }
}

# ネットワーク関連付け
resource "aws_ec2_client_vpn_network_association" "vpn_assoc" {
  for_each = var.subnet_associations

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.mutual_auth_vpn.id
  subnet_id              = each.value
}

# 認証ルール
resource "aws_ec2_client_vpn_authorization_rule" "vpn_auth" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.mutual_auth_vpn.id
  target_network_cidr    = "[TARGET_NETWORK_CIDR]"
  authorize_all_groups   = true
}

# ルート設定
resource "aws_ec2_client_vpn_route" "vpn_route" {
  for_each = var.route_subnets

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.mutual_auth_vpn.id
  destination_cidr_block = "[DESTINATION_CIDR]"
  target_vpc_subnet_id   = each.value
}

5. セキュリティグループの設定

重要なポイントとして、セキュリティグループでUDP 443を許可する必要があります。

resource "aws_security_group" "client_vpn_sg" {
  name_prefix = "client-vpn-"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "udp"  # 重要:UDPを許可
    cidr_blocks = ["[ALLOWED_CIDR_RANGE]"]  # 必要に応じて制限
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]  # 通常は全許可
  }

  tags = {
    Name = "client-vpn-security-group"
  }
}

6. OpenVPN設定ファイルの作成

クライアント側の設定ファイルを作成します。

client
dev tun
proto udp
remote *.cvpn-endpoint-[ENDPOINT_ID].prod.clientvpn.[REGION].amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
reneg-sec 0

<ca>
-----BEGIN CERTIFICATE-----
[CA証明書の内容をここに貼り付け]
-----END CERTIFICATE-----
</ca>

<cert>
-----BEGIN CERTIFICATE-----
[クライアント証明書の内容をここに貼り付け]
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
[クライアント証明書の秘密鍵をここに貼り付け]
-----END PRIVATE KEY-----
</key>

トラブルシューティング

接続タイムアウトが発生する場合

  1. セキュリティグループの確認

    • UDP 443が許可されているか確認
    • TCP 443のみでは接続できません
  2. エンドポイントFQDNの確認

    • ワイルドカード形式(*.cvpn-endpoint-[ENDPOINT_ID].prod.clientvpn.[REGION].amazonaws.com)を使用
    • 具体的なプレフィックス付きFQDNでも接続可能
  3. 証明書の検証

    • サーバー証明書にserverAuthclientAuthの両方が含まれているか確認
    • クライアント証明書が同じCAで署名されているか確認

証明書関連のエラー

# 証明書の詳細確認
openssl x509 -in server.crt -noout -text | grep -A 5 "Extended Key Usage"
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt client.crt

問題:
AWS Client VPNエンドポイントの証明書を更新した場合、通常は最大24時間の反映時間が必要です。

解決策:
ターゲットネットワークの関連付けを一度解除し、再関連付けすることで即座に適用可能です。

# 1. 現在の関連付けを確認
aws ec2 describe-client-vpn-target-networks \
  --client-vpn-endpoint-id [ENDPOINT_ID] \
  --region [REGION]

# 2. ターゲットネットワークの関連付けを解除
aws ec2 disassociate-client-vpn-target-network \
  --client-vpn-endpoint-id [ENDPOINT_ID] \
  --association-id [ASSOCIATION_ID] \
  --region [REGION]

# 3. 関連付けを再作成
aws ec2 associate-client-vpn-target-network \
  --client-vpn-endpoint-id [ENDPOINT_ID] \
  --subnet-id [SUBNET_ID] \
  --region [REGION]

注意点:

  • 関連付け解除中は一時的にVPN接続が利用できなくなります
  • 複数のサブネットがある場合は、すべてに対して実行が必要です
  • 本番環境では事前にメンテナンス時間を設定することを推奨します

重要:
上記のAWS CLIを使用した手動での関連付け解除・再作成が、証明書更新を即座に適用する最も確実な方法です。Terraformでの自動化については、環境によって動作が異なる可能性があるため、本番環境では手動での実行を推奨します。

この方法により、証明書更新後の待機時間を大幅に短縮できます。

まとめ

AWS Client VPNの相互認証エンドポイント構築では、以下のポイントが重要です:

  1. 証明書設計: サーバー証明書にserverAuthclientAuthの両方を含める
  2. ワイルドカード対応: AWS Client VPNの2レベル構造に対応した証明書作成
  3. ネットワーク設定: セキュリティグループでUDP 443を許可
  4. 設定ファイル: 正しいエンドポイントFQDNとプロトコル設定

この構成により、証明書ベースの認証を活用したVPN環境を構築できます。証明書による相互認証は、パスワード漏洩リスクを排除し、中間者攻撃への耐性を提供します。ただし、証明書の管理・更新・配布には適切な運用体制が必要であり、企業のセキュリティポリシーと運用能力に応じて導入を検討することが重要です。

Discussion