AWS Client VPN(相互認証)のハンズオンを試してみた

AWS Client VPN(相互認証)のハンズオンを試してみた

2026.03.19

こんにちは!クラスメソッドオペレーションズ AWS テクニカルサポートチームの大高です。

今回は AWS が公開している Client VPN(相互認証)のハンズオンを試してみました。

https://catalog.us-east-1.prod.workshops.aws/workshops/be2b90c2-06a1-4ae6-84b3-c705049d2b6f/ja-JP

はじめに

ハンズオンの手順に沿って実際に試してみたところ、いくつかハマりポイントがありましたので、手順とあわせてご紹介します。

なお、操作は 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 操作を継続したい場合は、スプリットトンネルを有効にしてください。

スプリットトンネルの挙動は公式ドキュメントに以下のように記載されています。

https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/split-tunnel-vpn.html

クライアント 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 の挙動でした。
公式ドキュメントには以下のように記載されています。

https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/what-is.html#what-is-components

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 を指定することを覚えておくと役立つかと思いました。

今回のブログ記事がどなたかのお役に立てば幸いです。それでは!

参考情報

https://catalog.us-east-1.prod.workshops.aws/workshops/be2b90c2-06a1-4ae6-84b3-c705049d2b6f/ja-JP

https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/split-tunnel-vpn.html

https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/what-is.html#what-is-components

https://aws.amazon.com/jp/vpn/pricing/

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。

運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AI をフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026 年 1 月 アノテーション㈱から社名変更しました

この記事をシェアする

FacebookHatena blogX

関連記事