[アップデート] 接続元 IP 制限もできるように! AWS Client VPN で クライアント接続ハンドラ機能がサポートされました

クライアントが AWS Client VPN エンドポイントに接続する際にハンドラーとして Lambda を呼び出し、接続の認可を行うことができるようになりました。

コンバンハ、千葉(幸)です。

AWS Client VPN でクライアント接続ハンドラ機能がサポートされ、追加のセキュリティ承認ポリシーが設定できるようになりました!

Client VPN エンドポイントへの接続確立時に、カスタマー側で設定したロジックによる認可を行うことができます。

目次

何が嬉しいのか

AWS Client VPN における認証と認可としては、これまで以下を設定可能でした。

  • 認証
    • Active Directory 認証(ユーザーベース)
    • 相互認証(証明書ベース)
    • SAML ベースのフェデレーション認証(ユーザーベース)
  • 認可
    • Client VPN ネットワークインタフェースの SecuriryGroup
    • ネットワークベースの承認ルール

クライアント認証と認可 - AWS Client VPN

AWS Client VPN を運用する上では、もっと細かいコントロールをしたいという場面があったかもしれません。特定の IP アドレスからのみ接続を許可したいメンテナンス時間には接続させたくない一時的に特定のユーザーをブロックしたい、などです。

特に、接続元 IP を制限したい、というケースは多かったかと思います。認可で設定可能な SecuriryGroup や承認ルールは「接続が確立した後にアクセス可能な宛先をコントロールする」ためのものであり、そのような要件には対応できませんでした。

今回のアップデートで対応したクライアント接続ハンドラ機能を用いることによって、従来の認証・認可とは別の観点で接続時の認可を行えるようになりました。接続元の IP を判断対象とすることもできます。

どのように機能するのか

イメージとしては以下の通りです。

エンドユーザーからの接続が開始された際に、Client VPN サービスは Lambda 関数を同期的に呼び出します。この Lambda 関数はあらかじめカスタマー側で定義しておく必要があるものです。要件に応じたロジックを定めておきます。

Lambda 関数には新規接続に関する属性がインプットされ、それを元に接続の可否を返却します。 Lambda 関数からのリターンの値に応じて、 Client VPN はクライアントからの接続を確立するか、切断するかのいずれかを行います。

Lambda 関数へのインプット

Client VPN サービスから受け取る各種属性情報は以下の通りです。

{
    "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>,
    "schema-version": "v1"
}
  • connection-id —クライアント接続のID
  • endpoint-id —クライアントVPNエンドポイントのID
  • common-name—デバイス識別子
  • usernameユーザーベース認証におけるユーザー名
  • platform —クライアントの OS プラットフォーム
  • platform-version—クライアント OS のバージョン
  • public-ip接続デバイスのパブリックIPアドレス
  • client-openvpn-version —クライアントが使用しているOpenVPNのバージョン
  • schema-version—スキーマバージョン

ここから得られる情報を元に、接続可否を判断するロジックを組むことになります。

Lambda 関数からのリターン値

Client VPN に以下フィールドを返却するように Lambda 関数を構成する必要があります。

{
    "allow": boolean,
    "error-msg-on-failed-posture-compliance": "",
    "posture-compliance-statuses": [],
    "schema-version": "v1"
}
  • allow—接続を許可するか拒否するかを示すブール値
  • error-msg-on-failed-posture-compliance
    • —Lambda関数によって接続が拒否された場合にクライアントに返却するメッセージ
  • posture-compliance-statuses—接続デバイスのステータスのリスト
    • デバイス管理ソリューションと統合し posture compliance の評価を行なっている際に使用
  • schema-version—スキーマバージョン

考慮事項

  • Lambda 関数の名前はAWSClientVPN-で始まる必要があります。
  • Lambda 関数のバージョンはサポートされていません。バージョンを含めた ARN を指定することはできません。
  • Lambda 関数は、クライアントVPNエンドポイントと同じAWSリージョンおよび同じAWSアカウントに存在する必要があります。
  • Lambda 関数は30秒後にタイムアウトします。この値は変更できません。
  • Lambda 関数は同期的に呼び出されます。これは、デバイスとユーザーの認証後、承認ルールが評価される前に呼び出されます。
  • Lambda 関数が新しい接続に対して呼び出され、クライアントVPNサービスが関数から期待される応答を受け取らない場合、クライアントVPNサービスは接続要求を拒否します。
    • Lambda 関数がスロットルされたり、タイムアウトしたり、その他の予期しないエラーが発生したり、関数の応答が有効な形式でない場合です。
  • Lambda 関数に「プロビジョニングされた同時実行性」を構成して、レイテンシーの変動なしにスケーリングできるようにすることをお勧めします。
  • Lambda 関数を更新しても、クライアントVPNエンドポイントへの既存の接続は影響を受けません。
    • 一度接続を切断してから接続し直すよう指示してください。
  • クライアントが AWS 提供のクライアントを使用している場合、以下のバージョンを使用する必要があります。
    • Windowsの場合はバージョン1.2.6以降
    • macOSの場合はバージョン1.2.4以降

詳細は以下を確認してください。

Connection authorization - AWS Client VPN

やってみた

クライアント接続ハンドラを使用し、以下のパターンで接続が拒否されることを確認してみます。

  • 接続元 IP 制限
  • ユーザ制限(Active Directory 認証)

Lambda 関数の作成

Client VPN に関連づける Lambda 関数をあらかじめ作成しておきます。名称のプレフィックスを意識する以外は、ほぼデフォルトで作成します。

  • 関数名:AWSClientVPN-Test
  • ランタイム:Python 3.8
  • IAM ロール:自動作成に任せる

後続の手順と説明が前後しますが、Client VPN と関連づけても Lambda 関数のリソースベースポリシーはブランクのままです。

Lambda 関数の呼び出しは Client VPN のサービスにリンクされたロールの権限によって行われます。

コードの内容は後述します。

Client VPN エンドポイントの設定変更

既存の Client VPN の設定を変更していきます。

クライアント接続ハンドラに関するパラメータが設定できるようになっています。有効化し、先ほど作成した Lambda 関数をプルダウンから選択します。

この状態で設定の変更を保存します。

エンドポイントの概要画面で、有効になったことが確認できます。

クライアント接続ハンドラの確認(接続元 IP 制限)

以下の設定で接続を試みます。

  • Client VPN エンドポイント
    • 相互認証
    • クライアント証明書ドメイン名:client1.domain.tld
  • クライアント
    • macOS
    • AWS VPN クライアント 1.2.4
    • パブリック IP 202.xx.xx.xx

この状態で接続した際のインプットはこのようになります。

{
    "endpoint-id": "cvpn-endpoint-05e3cd01fe10ad9ee",
    "connection-id": "cvpn-connection-0cf828dc9f63cd328",
    "common-name": "client1.domain.tld",
    "username": null,
    "platform": null,
    "platform-version": null,
    "public-ip": "202.xx.xx.xx",
    "client-openvpn-version": null,
    "schema-version": "v1"
}

Lambda 関数には以下のようなコードを設定しています。

拒否したい IP をベタ書きするという芸の無さですが、検証ということでシンプルにしています。

def lambda_handler(event, context):
    
    ip = event['public-ip']
    
    if ip == "202.xx.xx.xx":
        flag = 0
    else:
        flag = 1
    
    return {
            "allow": bool(flag),
            "error-msg-on-failed-posture-compliance": "Your IP is not allowed to access. Please Contact mail@example.co.jp",
            "posture-compliance-statuses": ["compliant"],
            "schema-version": "v1"
            }

接続元 IP がコード内のものと一致する場合、接続は拒否されます。そして、クライアントには設定したメッセージが表示されます。

試してみると、意図通りの結果となりました。

この時、 Client VPN から出力されるログは以下のようになっています。

{
    "connection-log-type": "connection-attempt",
    "connection-attempt-status": "failed",
    "connection-attempt-failure-reason": "client-connect-failed",
    "connection-id": "cvpn-connection-00928475175520a2c",
    "client-vpn-endpoint-id": "cvpn-endpoint-05e3cd01fe10ad9ee",
    "transport-protocol": "udp",
    "connection-start-time": "NA",
    "connection-last-update-time": "2020-11-07 08:27:46",
    "client-ip": "NA",
    "common-name": "client1.domain.tld",
    "device-ip": "202.xx.xx.xx",
    "port": "63479",
    "ingress-bytes": "0",
    "egress-bytes": "0",
    "ingress-packets": "0",
    "egress-packets": "0",
    "posture-compliance-statuses": [
        {
            "S": "compliant"
        }
    ],
    "connection-end-time": "NA",
    "connection-duration-seconds": "NA"
}

異なるパブリック IP を使用して接続を試みると、問題なく接続できます。

クライアント接続ハンドラの確認(ユーザー制限)

使用する Client VPN エンドポイントを変更して、以下の条件で試してみます。

  • Client VPN エンドポイント
    • Active Directory 認証
  • クライアント
    • macOS
    • AWS VPN クライアント 1.2.4
    • 使用ユーザー名Batchi

この状態で接続した際の Lambda 関数へのインプットは以下のようになります。

{
    "endpoint-id": "cvpn-endpoint-0b359ceb0b7e702ca",
    "connection-id": "cvpn-connection-081b8badf6850d907",
    "common-name": null,
    "username": "Batchi",
    "platform": null,
    "platform-version": null,
    "public-ip": "202.xx.xx.xx",
    "client-openvpn-version": null,
    "schema-version": "v1"
}

Lambda 関数のコードは以下のように設定します。相変わらず手抜きですね。

def lambda_handler(event, context):

    user = event['username']
    
    if user == "Batchi":
        flag = 0
    else:
        flag = 1
    
    return {
            "allow": bool(flag),
            "error-msg-on-failed-posture-compliance": "This user is not permitted",
            "posture-compliance-statuses": ["compliant"],
            "schema-version": "v1"
            }

接続を試みます。今回はユーザーベース認証のため、ユーザー名とパスワードを入力します。

同じように接続が拒否されました。

接続ログはこのようになります。

{
    "connection-log-type": "connection-attempt",
    "connection-attempt-status": "failed",
    "connection-attempt-failure-reason": "client-connect-failed",
    "connection-id": "cvpn-connection-02705e0c71e8d0c4f",
    "client-vpn-endpoint-id": "cvpn-endpoint-0b359ceb0b7e702ca",
    "transport-protocol": "udp",
    "connection-start-time": "NA",
    "connection-last-update-time": "2020-11-07 09:12:30",
    "client-ip": "NA",
    "username": "Batchi",
    "device-ip": "202.xx.xx.xx",
    "port": "64624",
    "ingress-bytes": "0",
    "egress-bytes": "0",
    "ingress-packets": "0",
    "egress-packets": "0",
    "posture-compliance-statuses": [
        {
            "S": "compliant"
        }
    ],
    "connection-end-time": "NA",
    "connection-duration-seconds": "NA"
}

これでクライアント接続ハンドラの動作確認ができました!

終わりに

AWS Client VPN の接続認可を行う、クライアント接続ハンドラーに関するアップデートでした。

「AWS Client VPN って接続元 IP で制限できないんですか?」と聞かれることがしばしばあり、ずっと「残念ながらできないんですよ」と答えていたのですが、その回答が変わる時が来ました。

Lambda 関数のコードは自前で書かなくてはいけないというのが(特に自分のような人間には)若干ハードルが高いですが、より柔軟に、要件に応じたロジックを設定できるというのは助かりますね。ご活用ください。

以上、千葉(幸)がお送りしました。

おまけ

検証をする際に、何度かこのようなメッセージが表示されました。

これは Lambda 関数からのリターンに問題があった場合に表示されるデフォルのメッセージです。

Lambda 関数からのリターンにおいて、allowは boolean でなければならないのですが、それをずっと "true" や "false" という String を入れていた、という凡ミスでした。はい。

{
    "allow": boolean,
    "error-msg-on-failed-posture-compliance": "",
    "posture-compliance-statuses": [],
    "schema-version": "v1"
}