AWS Client VPN(相互認証)のハンズオンを試してみた
こんにちは!クラスメソッドオペレーションズ AWS テクニカルサポートチームの大高です。
今回は AWS が公開している Client VPN(相互認証)のハンズオンを試してみました。
はじめに
ハンズオンの手順に沿って実際に試してみたところ、いくつかハマりポイントがありましたので、手順とあわせてご紹介します。
なお、操作は AWS マネジメントコンソールを使わず CLI で実施し、事前準備リソース(VPC・EC2・セキュリティグループ等)は CloudFormation テンプレートで構築しています。
VPN への接続には AWS VPN Client アプリを使用しています。
前提条件
- AWS アカウントを持っていること
- AWS CLI が設定済みであること
- macOS を使用していること(VPN クライアントは AWS VPN Client ARM64 版)
やってみた
1. 事前準備:CloudFormation テンプレートのデプロイ
ハンズオンの本質(証明書作成、ACM インポート、Client VPN エンドポイントの作成)に集中するため、以下のリソースについては CloudFormation で事前に構築しました。
- VPC + プライベートサブネット
- EC2 インスタンス(Amazon Linux 2023、t3.nano)
- IAM ロール + インスタンスプロファイル(SSM Session Manager アクセス用)
- SSM VPC エンドポイント × 3(ssm / ssmmessages / ec2messages)
- セキュリティグループ(VPC CIDR からの ICMP を許可)
- CloudWatch Logs ロググループ(VPN アクセスログ用)
CloudFormation テンプレート(client-vpn-handson-prereq.yml)
AWSTemplateFormatVersion: "2010-09-09"
Description: "Prerequisites for AWS Client VPN Hands-on (Mutual Authentication)"
Parameters:
Prefix:
Type: String
Default: "clientvpn-handson"
VpcCidr:
Type: String
Default: "10.0.0.0/16"
PrivateSubnetCidr:
Type: String
Default: "10.0.0.0/24"
VpnClientCidr:
Type: String
Default: "10.10.0.0/22"
Description: "CIDR block assigned to VPN clients. Must not overlap with VpcCidr."
EC2InstanceAMI:
Type: AWS::EC2::Image::Id
Default: "ami-0b6e7ccaa7b93e898"
EC2InstanceType:
Type: String
Default: "t3.nano"
EC2InstanceVolumeType:
Type: String
Default: "gp3"
EC2InstanceVolumeSize:
Type: String
Default: "8"
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${Prefix}-vpc
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref PrivateSubnetCidr
VpcId: !Ref Vpc
AvailabilityZone:
Fn::Select:
- "0"
- Fn::GetAZs: ""
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-subnet
Ec2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${Prefix}-ec2-sg
GroupDescription: !Sub ${Prefix}-ec2-sg
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: !Ref VpcCidr
Description: "Allow ICMP from VPC CIDR (Client VPN uses source NAT, so source IP is VPN gateway ENI in VPC subnet)"
Tags:
- Key: Name
Value: !Sub ${Prefix}-ec2-sg
SsmVpcEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${Prefix}-ssm-endpoint-sg
GroupDescription: !Sub ${Prefix}-ssm-endpoint-sg
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref Ec2SecurityGroup
Description: "Allow HTTPS from EC2"
Tags:
- Key: Name
Value: !Sub ${Prefix}-ssm-endpoint-sg
SsmVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
VpcId: !Ref Vpc
SubnetIds:
- !Ref PrivateSubnet
SecurityGroupIds:
- !Ref SsmVpcEndpointSecurityGroup
SsmMessagesVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
VpcId: !Ref Vpc
SubnetIds:
- !Ref PrivateSubnet
SecurityGroupIds:
- !Ref SsmVpcEndpointSecurityGroup
Ec2MessagesVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
VpcId: !Ref Vpc
SubnetIds:
- !Ref PrivateSubnet
SecurityGroupIds:
- !Ref SsmVpcEndpointSecurityGroup
Ec2Role:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${Prefix}-ec2-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Ec2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub ${Prefix}-ec2-instance-profile
Roles:
- !Ref Ec2Role
Ec2Instance:
Type: AWS::EC2::Instance
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-ec2
ImageId: !Ref EC2InstanceAMI
InstanceType: !Ref EC2InstanceType
IamInstanceProfile: !Ref Ec2InstanceProfile
DisableApiTermination: false
EbsOptimized: false
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
DeleteOnTermination: true
VolumeType: !Ref EC2InstanceVolumeType
VolumeSize: !Ref EC2InstanceVolumeSize
SecurityGroupIds:
- !Ref Ec2SecurityGroup
SubnetId: !Ref PrivateSubnet
UserData: !Base64 |
#!/bin/bash
yum update -y
VpnLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/clientvpn/${Prefix}
RetentionInDays: 30
Outputs:
VpcId:
Description: "VPC ID"
Value: !Ref Vpc
PrivateSubnetId:
Description: "Private Subnet ID (use for Client VPN target network association)"
Value: !Ref PrivateSubnet
VpcCidr:
Description: "VPC CIDR (use for Client VPN authorization rule)"
Value: !Ref VpcCidr
Ec2InstanceId:
Description: "EC2 Instance ID (ping target after VPN connection)"
Value: !Ref Ec2Instance
Ec2InstancePrivateIp:
Description: "EC2 Private IP (ping target after VPN connection)"
Value: !GetAtt Ec2Instance.PrivateIp
VpnLogGroupName:
Description: "CloudWatch Logs log group name (use for Client VPN connection logging)"
Value: !Ref VpnLogGroup
スタックをデプロイします。
aws cloudformation deploy \
--template-file client-vpn-handson-prereq.yml \
--stack-name client-vpn-handson-prereq \
--capabilities CAPABILITY_NAMED_IAM \
--region ap-northeast-1
完了したら Outputs を確認してメモしておきます。
以降の手順でサブネット ID、VPC CIDR、EC2 のプライベート IP、ロググループ名を使用します。
aws cloudformation describe-stacks \
--stack-name client-vpn-handson-prereq \
--region ap-northeast-1 \
--query 'Stacks[0].Outputs'
2. easy-rsa で証明書を生成
相互認証に必要なサーバー証明書、クライアント証明書を easy-rsa で生成します。
cd /tmp
git clone https://github.com/OpenVPN/easy-rsa.git
cd easy-rsa/easyrsa3
./easyrsa init-pki
./easyrsa build-ca nopass # Common Name はデフォルト(Enter)でよい
./easyrsa --san=DNS:server build-server-full server nopass # 確認プロンプトは yes
./easyrsa build-client-full client1.domain.tld nopass # 確認プロンプトは yes
生成されるファイルは以下のとおりです。
| ファイル | 用途 |
|---|---|
| pki/ca.crt | CA 証明書 |
| pki/issued/server.crt | サーバー証明書 |
| pki/private/server.key | サーバー秘密鍵 |
| pki/issued/client1.domain.tld.crt | クライアント証明書 |
| pki/private/client1.domain.tld.key | クライアント秘密鍵 |
3. ACM に証明書をインポート
作業しやすいよう、必要なファイルをひとつのディレクトリにまとめてからインポートします。
mkdir ~/custom_folder
cp pki/ca.crt ~/custom_folder/
cp pki/issued/server.crt ~/custom_folder/
cp pki/private/server.key ~/custom_folder/
cp pki/issued/client1.domain.tld.crt ~/custom_folder/
cp pki/private/client1.domain.tld.key ~/custom_folder/
cd ~/custom_folder
サーバー証明書とクライアント証明書をそれぞれ ACM にインポートします。
# サーバー証明書
aws acm import-certificate \
--certificate fileb://server.crt \
--private-key fileb://server.key \
--certificate-chain fileb://ca.crt \
--region ap-northeast-1
# クライアント証明書
aws acm import-certificate \
--certificate fileb://client1.domain.tld.crt \
--private-key fileb://client1.domain.tld.key \
--certificate-chain fileb://ca.crt \
--region ap-northeast-1
それぞれ CertificateArn が返ってくるので、次の手順で使用します。
4. Client VPN エンドポイントを作成
メモした証明書 ARN を差し替えて実行します。
スプリットトンネル(--split-tunnel)は必ず有効にします。
aws ec2 create-client-vpn-endpoint \
--client-cidr-block "10.10.0.0/22" \
--server-certificate-arn "arn:aws:acm:ap-northeast-1:123456789012:certificate/サーバー証明書のARN" \
--authentication-options Type=certificate-authentication,MutualAuthentication={ClientRootCertificateChainArn="arn:aws:acm:ap-northeast-1:123456789012:certificate/クライアント証明書のARN"} \
--connection-log-options Enabled=true,CloudwatchLogGroup=/aws/clientvpn/clientvpn-handson \
--split-tunnel \
--tag-specifications 'ResourceType=client-vpn-endpoint,Tags=[{Key=Name,Value=clientvpn-handson}]' \
--region ap-northeast-1
成功すると ClientVpnEndpointId(cvpn-endpoint-xxxxxxxxxx 形式)が返ってきます。
以降のコマンドで使用するのでメモしておきます。
5. ターゲットネットワーク関連付けと認可ルールの追加
ターゲットネットワークを関連付けます。
--subnet-id には CFn Outputs の PrivateSubnetId を指定します。
aws ec2 associate-client-vpn-target-network \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--subnet-id subnet-xxxxxxxxxx \
--region ap-northeast-1
次に VPC 宛の通信を許可する認可ルールを追加します。
--target-network-cidr には CFn Outputs の VpcCidr(10.0.0.0/16)を指定します。
aws ec2 authorize-client-vpn-ingress \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--target-network-cidr "10.0.0.0/16" \
--authorize-all-groups \
--region ap-northeast-1
ステータスが available になるまで数分待ちます。(ここは少し時間が掛かります)
aws ec2 describe-client-vpn-endpoints \
--client-vpn-endpoint-ids cvpn-endpoint-xxxxxxxxxx \
--region ap-northeast-1 \
--query 'ClientVpnEndpoints[0].Status'
6. クライアント設定ファイルの取得・接続
クライアント設定ファイルをダウンロードします。
aws ec2 export-client-vpn-client-configuration \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--region ap-northeast-1 \
--query ClientConfiguration \
--output text > ~/custom_folder/downloaded-client-config.ovpn
ダウンロードした .ovpn ファイルにはクライアント証明書と秘密鍵が含まれていないため、以下のコマンドで追記します。
cd ~/custom_folder
cat >> downloaded-client-config.ovpn << EOF
<cert>
$(sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' client1.domain.tld.crt)
</cert>
<key>
$(sed -n '/-----BEGIN PRIVATE KEY-----/,/-----END PRIVATE KEY-----/p' client1.domain.tld.key)
</key>
EOF
追記できているか確認します。
grep -n "<cert>\|</cert>\|<key>\|</key>" downloaded-client-config.ovpn
<cert>、</cert>、<key>、</key> の 4 行が表示されれば OK です。
AWS VPN Client に .ovpn ファイルを読み込んで接続し、EC2 への ping で疎通確認します。
ping <CFn Outputs の Ec2InstancePrivateIp>
ハマったポイントと注意点
スプリットトンネルを必ず有効にする
Client VPN エンドポイント作成時にスプリットトンネルを有効にしないと、接続中のすべての通信が VPN 経由になります。
今回作成した VPC には IGW も NAT ゲートウェイもないため、VPN 接続中はインターネット通信が完全に失われます。
AWS CLI を含む通常の PC 操作を継続したい場合は、スプリットトンネルを有効にしてください。
スプリットトンネルの挙動は公式ドキュメントに以下のように記載されています。
クライアント VPN エンドポイントで分割トンネルを有効にすると、クライアント VPN エンドポイントルートテーブル上のルートがクライアント VPN エンドポイントに接続されているデバイスにプッシュされます。これにより、クライアント VPN エンドポイントルートテーブルからのルートと一致するネットワークへの送信先を持つトラフィックだけがクライアント VPN トンネル経由でルーティングされます。
EC2 のセキュリティグループは VPN クライアント CIDR ではなく VPC CIDR で設定する
今回、一番はまったポイントはこれでした。
VPN 接続後に EC2 へ ping を試みたところ、まったく応答がありませんでした。
VPN トンネル自体が機能しているかを確認するため、VPC の DNS サーバー(10.0.0.2)に DNS クエリを投げてみると、正常に応答が返ってきました。
dig @10.0.0.2 example.com
これにより、VPN トンネル自体は問題なく、EC2 のセキュリティグループで止まっていることが分かりました。
原因は AWS Client VPN のソース NAT の挙動でした。
公式ドキュメントには以下のように記載されています。
IPv4 トラフィックについては、ソースネットワークアドレス変換 (SNAT) が適用され、クライアント CIDR 範囲からのソース IP アドレスがクライアント VPN ネットワークインターフェイス IP アドレスに変換されます。
EC2 に届くパケットのソース IP は VPN クライアントの IP(10.10.0.x)ではなく、VPN ゲートウェイの ENI IP(VPC サブネット内の 10.0.0.x)に変換されています。
そのため、セキュリティグループで VPN クライアント CIDR(10.10.0.0/22)からの ICMP を許可していても、実際のソース IP は 10.0.0.0/16 内のアドレスになるためルールがマッチしません。
このあたりの理解を誤っており、当初はセキュリティグループのインバウンドルールを VPN クライアント CIDR としていました。
セキュリティグループのインバウンドルールを VPN クライアント CIDR ではなく、VPC CIDR(10.0.0.0/16)からのトラフィックを許可するよう変更したところ、ping が通るようになりました。
CloudFormation テンプレートでは以下のように設定します。
SecurityGroupIngress:
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: !Ref VpcCidr # VPN クライアント CIDR ではなく VPC CIDR を指定する
AWS VPN Client が起動直後に終了する(環境依存)
古いバージョンの AWS VPN Client と新しい macOS の非互換が原因で、起動直後にウィンドウが消えることがあります。
旧バージョンをアンインストールし、公式サイトから最新の ARM64 版をインストールすることで解消しました。
ハンズオン後のリソース削除
ハンズオンが終わったらリソースを削除します。
放置すると費用が発生し続けるので、必ず以下の順番で削除することをオススメします。
| リソース | 費用の発生条件 |
|---|---|
| エンドポイント関連付け(サブネット 1 個あたり) | 関連付けているだけで課金(≒ $0.10/時間) |
| VPN 接続(アクティブな接続 1 本あたり) | 接続中は課金(≒ $0.05/時間) |
| SSM VPC エンドポイント × 3 | 存在するだけで課金(≒ $0.014/時間 × 3) |
| EC2(t3.nano) | 起動中は課金(≒ $0.006/時間) |
まず、ターゲットネットワーク関連付けを解除します。
関連付け ID を取得してから解除を実行します。
# 関連付け ID を取得
aws ec2 describe-client-vpn-target-networks \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--region ap-northeast-1 \
--query 'ClientVpnTargetNetworks[0].AssociationId'
# 解除
aws ec2 disassociate-client-vpn-target-network \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--association-id cvpn-assoc-xxxxxxxxxx \
--region ap-northeast-1
ステータスが null(完全削除)になるまで待ちます。(ここは少し時間が掛かります)
aws ec2 describe-client-vpn-target-networks \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--region ap-northeast-1 \
--query 'ClientVpnTargetNetworks[0].Status'
解除が完了したら、Client VPN エンドポイントを削除します。
aws ec2 delete-client-vpn-endpoint \
--client-vpn-endpoint-id cvpn-endpoint-xxxxxxxxxx \
--region ap-northeast-1
最後に CFn スタックを削除します。
aws cloudformation delete-stack \
--stack-name client-vpn-handson-prereq \
--region ap-northeast-1
まとめ
AWS Client VPN(相互認証)のハンズオンを実施しました。
EC2 セキュリティグループの CIDR 設定でハマりましたが、特に AWS Client VPN がソース NAT を行う という挙動を知らないとなかなか原因にたどり着けないポイントでした。
EC2 を独自で用意して AWS Client VPN の疎通確認をする際は、セキュリティグループのインバウンドルールに VPN クライアント CIDR ではなく VPC CIDR を指定することを覚えておくと役立つかと思いました。
今回のブログ記事がどなたかのお役に立てば幸いです。それでは!
参考情報
クラスメソッドオペレーションズ株式会社について
クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AI をフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026 年 1 月 アノテーション㈱から社名変更しました






