クライアント接続ハンドラーを利用して AWS Client VPN の接続を検知してみた

2023.08.29

こんにちは、コンサル部の有福です。

前々回、前回のブログ記事で AWS Clinet VPN の接続検知を行う方法として、

  • CloudWatch メトリクスを利用した方法

  • CloudWatch Logs サブスクリプションフィルターを利用した方法

を試してきました。

ただし、ここまで試してきた方法では検知に取りこぼしがあったり、検知のリアルタイム性に欠けるなど、それぞれ課題を抱えた結果となりました。

私の中では「他にも何か方法があるんじゃないか?」と思っていた矢先、AWS Client VPN の機能の1つである「クライアント接続ハンドラー」を接続検知に使うという方法を思いついたので、今回も試した内容をブログにまとめました。

やりたかったこと

AWS Client VPN エンドポイントへの接続があった際に、その接続を検知して通知を行う

前提条件

  • AWS Client VPN を設定済み

  • VPN エンドポイントの設定で「クライアント接続ハンドラー」を有効化していない

  • VPN 接続時の認証は「相互認証」を使用

  • 通知用の Amazon SNS トピックを作成済み

試した方法

今回やろうとしたのは、AWS Client VPN の機能の1つである「クライアント接続ハンドラー」として呼び出される Lambda の処理の中で、Amazon SNS によるメール通知を行うという方法です。

イメージ

「クライアント接続ハンドラー」について

クライアント接続ハンドラーは、VPN エンドポイントへの接続確立時にLambda関数を呼び出し、カスタマー側で設定したカスタムロジックによる接続認可を行うことができる機能です。

クライアント接続ハンドラー機能を有効にした場合、Clinet VPN サービスは、相互認証や Active Directory 認証といった認証プロセスの後続処理として、Labmda 関数を同期的に呼び出し認可プロセスを実行するようになります。

クライアント接続ハンドラーについては以下の記事でも解説されています。

本来は接続認可を行うための機能ですが、カスタマー側で用意した Lambda 関数を使えるようなので、Lambda 関数から Amazon SNS を介した通知処理を行うことで接続検知に使えるのではないかと考えました。

クライアント接続ハンドラーを利用するうえでの注意点

クライアント接続ハンドラーを利用するうえで、特に注意しておくべきと思われる事項を AWS 公式ドキュメントに記載の「要件と考慮事項」から抜粋して以下にまとめました。(全ての項目については、公式ドキュメントをご覧ください)

  • Lambda 関数の名前は、 AWSClientVPN- プレフィックスで始まる必要があります。

  • Lambda 関数は、クライアント VPN エンドポイントと同じ AWS リージョンおよび同じ AWS アカウントに存在する必要があります。

  • Lambda 関数は 30 秒後にタイムアウトします。この値は変更できません。

  • 新しい接続に対して Lambda 関数が呼び出され、クライアント VPN サービスが関数から期待されるレスポンスを取得しない場合、クライアント VPN サービスは接続要求を拒否します。これは、Lambda 関数がスロットルされた、タイムアウトした、またはその他の予期しないエラーが発生した場合、関数のレスポンスが有効な形式でない場合などに発生します。

関数作成時には、リージョン・アカウントが VPN エンドポイントと同一であること関数名に指定されたプリフィックスがあることが要件となっている点に注意が必要です。

また、クライアント接続ハンドラーとして呼び出される Lambda 関数は、30秒という実行時間の制限があります。

今回の想定では Lambda 関数での主たる処理が Amazon SNS へのメッセージ発行をするだけなので影響ないと考えています。

そのほか、VPN 接続への影響がある部分として注意しておきたいのが、Lambda から期待されるレスポンスが返ってこない場合にエンドユーザーからの接続要求が拒否されるという点です。

なお、クライアント接続ハンドラーが返すレスポンススキーマは以下である必要があります。

{
    "allow": boolean,
    "error-msg-on-denied-connection": "",
    "posture-compliance-statuses": [],
    "schema-version": "v2"
}

以上を踏まえたうえで、接続検知のための実装を行っていきます。

やってみた

Lambda 関数の作成

作業は、Labmda 関数を作成した後、これを AWS Client VPN のクライアント接続ハンドラーとして設定するという流れになります。

まずは Lambda コンソールを開き、「関数の作成」ボタンをクリックして作成画面に遷移します。

作成画面での設定項目については以下のとおりとします。

  • 関数名:(「AWSClientVPN-」で始まる名称)

  • ランタイム:Python3.10

  • アーキテクチャ:arm64

  • 実行ロール:基本的な Lambda アクセス権限で新しいロールを作成

クライアント接続ハンドラーに指定する Lambda 関数は、関数名が「AWSClientVPN-」のプレフィックスで始まる必要があることに注意が必要です。

その他の設定項目についてはデフォルトのままにしています。

画面右下の「関数の作成」ボタンをクリックすると Lambda 関数が作成されます。

Lambda 関数コードの作成

今回、検証用に以下のコードを用意しました。

import os
import boto3

# SNSトピックARN:環境変数から取得
sns_topic_arn = os.environ['SNS_TOPIC_ARN']
# SNSクライアントを作成
sns_client = boto3.client('sns')

def lambda_handler(event, context):
    
    try:
        # SNSメッセージ件名
        sns_subject = "クライアントVPN接続通知"
        # SNSメッセージ本文
        sns_message = "Client VPN エンドポイントへの接続を検知しました。\n\n---\n"
        
        # リクエストデータを展開してSNSメッセージ本文に追加
        for key, value in event.items():
            sns_message += f"- {key}: {value}\n"

        # SNSメッセージを発行
        sns_response = sns_client.publish(
            TopicArn=sns_topic_arn,
            Message=sns_message,
            Subject=sns_subject,
        )

        # 接続ハンドラーから Client VPN サービスへのレスポンス    
        allow = True    # 今回は認可ロジック未実装につき、常に接続許可
        return {
            "allow": allow,
            "error-msg-on-failed-posture-compliance": "Error establishing connection. Please contact your administrator.",
            "posture-compliance-statuses": [],
            "schema-version": "v2"
        }
        
    except Exception as e:
        print(e)
        raise e

今回作成する Lambda 関数では認可ロジックの実装は行わずに、接続「許可」のレスポンスを常に返すようにしています。

認可ロジックと組み合わせて接続検知を行いたい場合には、コードの修正が必要です。

このコードを Lambda コンソール上で入力(もしくはコピペ)して「Deploy」ボタンをクリックしてコードを適用します。

ちなみに、クライアント接続ハンドラーとして呼び出される Lambda 関数に渡されるデータは以下のとおりです。

{
    "connection-id": <connection ID>,
    "endpoint-id": <client VPN endpoint ID>,
    "common-name": <cert-common-name>,
    "username": <user identifier>,
    "platform": <OS platform>,
    "platform-version": <OS version>,
    "public-ip": <public IP address>,
    "client-openvpn-version": <client OpenVPN version>,
    "groups": <group identifier>,
    "schema-version": "v2"
}

上記の関数コードでは、このデータに含まれる情報を通知内容に含めるようにしています。

以上でLambda 関数へのコード適用は完了ですが、Lambda 関数が Amazon SNS へのメッセージ発行処理を行うために「Lambda 実行用の IAM ロールへの権限追加」「環境変数の設定」が必要です。

Lambda 実行ロールの権限追加

Lambda コンソールの「設定」タブで「アクセス権限」を選び、表示されたロール名のリンクをクリックします。

 IAM ロールの詳細画面が開くので、「許可を追加」をクリックしてプルダウンから「ポリシーをアタッチ」を選びます。

検索ボックスにsnsと入力してEnterキーを押すと、AWSIoTDeviceDefenderPublishFindingsToSNSMitigationActionという長い名称の AWS 管理ポリシーが検索結果に出てくるこれにチェックを入れて「許可を追加」ボタンをクリックします。

追加したポリシーの内容は以下のとおり、Amazon SNS にメッセージを発行するための許可となります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

Lambda 環境変数の設定

Lambda コンソールに戻って「設定」タブの「環境変数」を選択します。

「編集」ボタンをクリックすると環境変数の編集画面になります。

「環境変数の追加」ボタンをクリックして、キーにSNS_TOPIC_ARN、値には通知先となるAmazon SNS トピックのARN を入力して「保存」ボタンを押します。

これで Lambda 関数の作成は完了です。

クライアント接続ハンドラーの有効化

Lambda 関数を作成したので、VPN エンドポイントのクライアント接続ハンドラーを有効化します。

VPC コンソールを開き、サイドメニューから「クライアント VPN エンドポイント」を選択します。

一覧から検知対象の VPN エンドポイントを選択した状態で「アクション」ボタンをクリックし、プルダウンの「クライアント VPN エンドポイントを変更」を選択します。

変更画面に遷移するので、「クライアント接続ハンドラーを有効化」をチェックオンにします。

「クライアント接続ハンドラー ARN」には、先ほど作成した Labmda 関数の ARN を選択します。

画面右下の「クライアント VPN エンドポイントを変更」ボタンをクリックすると変更が適用されます。

下画像は変更適用直後のコンソール画面です。

「クライアント接続ハンドラーARN」に指定した Lambda 関数の ARN が表示され、「クライアント接続ハンドラーの状態」がApplyingとなっています。

当然ではありますが、適用が完了するまでは VPN 接続を行っても Lambda 関数は呼び出されません。

変更適用から1分ぐらいでAppliedになりました。

動作確認

実際に接続の検知・通知が行われるかを検証するために、検知対象の VPN エンドポイントに接続を行いました。

VPN 接続を試すとすぐに通知メールが届きました。

接続ログの内容を含む CloudWatch Logs サブスクリプションフィルターを利用した通知に比べると情報量が若干減りますが、それでもエンドユーザーに関する情報があるので接続元のユーザーを識別したり、IPアドレスを確認することが可能です。

通知タイミングに関してですが、私の環境では VPN エンドポイントの設定で「クライアントログインバナー」と呼ばれるVPN 接続開始時にクライアント側にメッセージを表示させる機能を有効にしています。

接続確立時にこのログインバナーが表示される訳ですが、今回試した方法ではログインバナーが表示されるとほぼ同時かそれより以前に通知メールを受信しました。

また、同じタンミングで2つの異なる端末からの VPN 接続を試みてみたところ、2つの通知メールが届きました。

今回の検知方法では エンドユーザーからの接続ごとに Lambda 関数が個別に呼び出されるため、仮に同じタイミングで複数の VPN 接続が開始されたとしても通知はひとまとめにされるずに、その数だけ通知が来ることになります。

そのほか、Lambda 関数の実行が失敗した場合の挙動についても確認しました。

Lambda 関数の実行ロールにアタッチしている AWSIoTDeviceDefenderPublishFindingsToSNSMitigationAction ポリシーを削除して、わざと Lambda 関数の実行が失敗するようにしてみました。

すると、クライアント側の端末で以下のようなエラーメッセージが表示され、接続は確立されませんでした。

※ クライアントソフトに「接続完了。」と表示されていますが、接続は確立されず切断しました。

さいごに

今回は、AWS Client VPN の接続検知を行う方法としてクライアント接続ハンドラーを利用する方法を試してみました。

ここまでで3パターンの接続検知の方法を試してきましたが、リアルタイムな接続検知を必要とする場合には有用性が高い方法ではないかと考えます。

また、Lambda 関数のコードを修正することで認可ロジックと組み合わせた処理を行うことも可能で、ロジックの組み方次第では接続を拒否するものについても通知を行うといったことも可能だと思います。

とはいえ、クライアント接続ハンドラーとして呼び出される Lambda 関数には一部制約が存在したり、実行エラーなどによって期待されるレスポンスを返せない場合にエンドユーザーからの VPN 接続ができないといった影響が生じ得る点は念頭に置いておく必要があります。

この記事が何かの参考となれば幸いです。