この記事は、弁護士ドットコム株式会社の Advent Calendar 2023 の 21 日目の記事です。
皆さん、こんにちは!弁護士ドットコム SRE 室の @et_tei です。国籍は中国で、今年は来日13年目です。今回は FireHOL で公開されているブラックリストからの接続 Akamai でブロックする方法をご紹介します。
- 背景
- Akamai API Client の発行と Akamai NetworkList の作成
- Lambda 関数の準備
- EventBridge の scheduler を作成する
- Akamai で Network List を確認する
背景
FireHOL で公開しているブラックリストの IP から当社のサイトへアクセスし、ログインできたログが検知されたことがあります。悪用の恐れがありますので、ブラックリストの IP からの接続を全部ブロックするように決定しました。FireHOL で公開しているブラックリストに記録されている IP レンジは数千件、unique IP は数億がありまして、そしてそのブラックリストが常に更新されるため、手動登録及び更新が不可能です。
当社は Akamai という CDN サービスを利用していますので、Akamai が提供している WAP(Web Application Protector) に FireHOL で公開しているブラックリストの IP を登録してブロックすることを考えます。
やることは以下の三点です。
- Akamai WAP で NetworkList を作成します。
- Lambda 関数を作成し、FireHOL から IP リストをダウンロードして、Akamai API で NetworkList を更新します。
- EventBridge の scheduler を作成し、定期的に Lambda 関数を実行します。
Akamai API Client の発行と Akamai NetworkList の作成
API Client の発行
Akamai API を実行するため、API Client が必要です。以下の画像のように、「ユーザーおよび API クライアント」画面で作成します。
API は NetWorkLists のみ選択し、READ-WRITE
権限を付与します。
API Client が作成された後、API 操作に必要な Credential 情報も生成されました。以下の Credential 情報は API 操作時に必ず必要なものです。
- ACCESS_TOKEN
- CLIENT_SECRET
- API_HOST
- CLIENT_TOKEN
Akamai NetworkList の作成
Akamai の管理コンソールのメニューから、「Security Configrations」→「Network Lists」の順で NetworkList の管理画面に入ります。画面の右上にある新規リストの作成
ボタンをクリックして、Network List を作成します。
NetworkList が作成された後、アクション
からアクティブ化する必要があります。ステージングと本番環境のネットワークを順番でアクティブ化します。
Lambda 関数の準備
ソースコードの準備
API Client と Network List が作成した後、API を実行するソースコードを準備します。Akamai API はたくさんの言語がサポートしますが、今回は Python を利用します。
EdgeGrid の準備
本来は Akamai API で定義されているスキームに沿って、API Client を作成時に発行された Credential 情報を利用したやり取りをもって認証処理行い、問題がなければ API を利用できます。しかしこれらの過程は API 利用者にとっては不便なもので、API 利用のハードルを上げてしまいます。この不便さを改善するため、Akamai は API Client の Credential 情報を利用した認証スキームを処理するライブラリーを用意し、各種プログラミング言語やツールで利用できるよう配布しています。それが EdgeGrid です。Akamai が用意されている Python 用 EdgeGrid はここからダウンロード可能です。全部必要ではありませんので、akamai フォルダだけ自分のローカルにコピーすれば大丈夫です。
Lambda 関数用ライブラリの準備
EdgeGrid の akamai フォルダが準備できた後、Akamai API を Lambda で実行できるために必要なライブラリを準備します。ここからダウンロードできます。
以下の必要なライブラリのみ、EdgeGrid の akamai フォルダと同じ場所にコピーすれば大丈夫です。
- bin
- certifi
- certifi-2019.3.9.dist-info
- chardet
- chardet-3.0.4.dist-info
- edgegrid_python-1.1.1.dist-info
- idna
- idna-2.8.dist-info
- requests
- requests-2.21.0.dist-info
- urllib3
- urllib3-1.24.3.dist-info
Lambda 関数作成
必要なものが揃いましたら、lambda_function.py を作成します。
まず、以下のパラメータを設定します。
# API 認証情報取得 AKAMAI_ACCESS_TOKEN = os.environ['AKAMAI_ACCESS_TOKEN'] AKAMAI_CLIENT_SECRET = os.environ['AKAMAI_CLIENT_SECRET'] AKAMAI_API_HOST = os.environ['AKAMAI_API_HOST'] AKAMAI_CLIENT_TOKEN = os.environ['AKAMAI_CLIENT_TOKEN'] NETWORK_LIST_ID = os.environ['NETWORK_LIST_ID']
Akamai Network List を更新する時に、リスト名で更新するのではなく、リスト ID で更新します。残念ですが、このリスト ID は Akamai のコンソールから確認できませんので、Akamai API で確認しなければいけません。
get-network-lists
API をシンプルに実行できるのは edgegrid-curl です。設定はREADME.md
の通りに httpie-edgegrid
をインストールして、~/.edgerc
を作成すれば OK です。最後、以下のコマンドを実行します。
$ ./egcurl -sSik https://xxxxxxxxxx.luna.akamaiapis.net/network-list/v2/network-lists | grep "/network-list/v2/network-lists" | jq '.networkLists[] | select(.name == "firehol level1 IPs")'
結果は下記の JSON で出力されます。uniqueId
は NetworkList ID をなります。
{ "accessControlGroup": "Top-Level Group: G-xxxxxx", "elementCount": 1890, "links": { "activateInProduction": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS/environments/PRODUCTION/activate", "method": "POST" }, "activateInStaging": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS/environments/STAGING/activate", "method": "POST" }, "appendItems": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS/append", "method": "POST" }, "retrieve": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS" }, "statusInProduction": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS/environments/PRODUCTION/status" }, "statusInStaging": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS/environments/STAGING/status" }, "update": { "href": "/network-list/v2/network-lists/111111_FIREHOLLEVEL1IPS", "method": "PUT" } }, "name": "firehol level1 IPs", "networkListType": "networkListResponse", "readOnly": false, "shared": false, "syncPoint": 1, "type": "IP", "uniqueId": "111111_FIREHOLLEVEL1IPS" }
次は、FireHOL から IP リストをダウンロードするファンクションを作成します。Akamai NetworkList は0.0.0.0/8
のような特殊な IP をサポートしませんので、ダウンロードした IP リストから除外しなければいけません。
def get_ip_items(): ip_items = [] # firehol_level1 のファイルをダウンロードする firehol_url = "https://iplists.firehol.org/files/firehol_level1.netset" urlData = requests.get(firehol_url).text # コメント部分を削除し、リストに変換する for item in urlData.splitlines(): if not item.startswith("#"): ip_items.append(item) # 特殊な IP が指定することができないため、除外する ip_items.remove("0.0.0.0/8") return ip_items
次は、整理された IP リストで Akamai NetworkList を更新するファンクションを作成します。syncPoint
は NetworkList の各バージョンを識別します。変更されるたびに値が増加します。現在のバージョンに対する更新ではないと、エラーが発生しますので、更新する前に、今の syncPoint を取得する必要があります。
def update_network_list(apiRequest, apiBaseUrl, ip_list): api_url = apiBaseUrl + "?extended=false&includeElements=true" # 今のシンクポイントを取得する get_headers = {"accept": "application/json"} get_response = apiRequest.get(api_url, headers=get_headers) syncPoint = json.loads(get_response.text)["syncPoint"] print(syncPoint) # 更新用 payload を用意する payload = { "list": ip_list, "syncPoint": syncPoint, "name": "firehol level1 IPs", "type": "IP" } put_headers = { "accept": "application/json", "content-type": "application/json" } # Network List を更新する put_response = apiRequest.put(api_url, json=payload, headers=put_headers) return json.loads(put_response.text)["syncPoint"]
NetworkList を更新した後、ステージングと本番環境のネットワークを順番にアクティブ化します。ステージングネットワークをアクティブ化した後、すぐ本番環境ネットワークをアクティブ化すると、エラーになりますので、間に数秒を待ったほうが安全です。
def active_network_list(apiRequest, apiBaseUrl, new_syncPoint): # IP 更新後、アクティブ化する staging_url = apiBaseUrl + "/environments/STAGING/activate" production_url = apiBaseUrl + "/environments/PRODUCTION/activate" # アクティブ用 payload を用意する post_payload = { "name": "firehol level1 IPs", "syncPoint": new_syncPoint } post_headers = { "accept": "application/json", "content-type": "application/json" } staging_response = apiRequest.post(staging_url, json=post_payload, headers=post_headers) print(staging_response.text) # 3秒を待つ time.sleep(3) production_response = apiRequest.post(production_url, json=post_payload, headers=post_headers) print(production_response.text)
全体の Lambda 関数はこんな感じです。
import json, os, requests, time from akamai.edgegrid import EdgeGridAuth def lambda_handler(event, context): # API 認証情報取得 AKAMAI_ACCESS_TOKEN = os.environ['AKAMAI_ACCESS_TOKEN'] AKAMAI_CLIENT_SECRET = os.environ['AKAMAI_CLIENT_SECRET'] AKAMAI_API_HOST = os.environ['AKAMAI_API_HOST'] AKAMAI_CLIENT_TOKEN = os.environ['AKAMAI_CLIENT_TOKEN'] NETWORK_LIST_ID = os.environ['NETWORK_LIST_ID'] apiRequest = requests.Session() apiRequest.auth = EdgeGridAuth( client_token = AKAMAI_CLIENT_TOKEN, client_secret = AKAMAI_CLIENT_SECRET, access_token = AKAMAI_ACCESS_TOKEN ) apiBaseUrl = "https://" + AKAMAI_API_HOST + "/network-list/v2/network-lists/" + NETWORK_LIST_ID ip_list = get_ip_items() new_syncPoint = update_network_list(apiRequest, apiBaseUrl, ip_list) active_network_list(apiRequest, apiBaseUrl, new_syncPoint) # 最新の IP ブラックリストを取得する def get_ip_items(): ip_items = [] # firehol_level1 のファイルをダウンロードする firehol_url = "https://iplists.firehol.org/files/firehol_level1.netset" urlData = requests.get(firehol_url).text # コメント部分を削除し、リストに変換する for item in urlData.splitlines(): if not item.startswith("#"): ip_items.append(item) # 特殊な IP が指定することができないため、除外する ip_items.remove("0.0.0.0/8") return ip_items # NetworkList を更新する def update_network_list(apiRequest, apiBaseUrl, ip_list): api_url = apiBaseUrl + "?extended=false&includeElements=true" # 今のシンクポイントを取得する get_headers = {"accept": "application/json"} get_response = apiRequest.get(api_url, headers=get_headers) syncPoint = json.loads(get_response.text)["syncPoint"] print(syncPoint) # 更新用 payload を用意する payload = { "list": ip_list, "syncPoint": syncPoint, "name": "firehol level1 IPs", "type": "IP" } put_headers = { "accept": "application/json", "content-type": "application/json" } # Network List を更新する put_response = apiRequest.put(api_url, json=payload, headers=put_headers) return json.loads(put_response.text)["syncPoint"] # 更新済みの NetworkList をアクティブ化する def active_network_list(apiRequest, apiBaseUrl, new_syncPoint): # IP 更新後、アクティブ化する staging_url = apiBaseUrl + "/environments/STAGING/activate" production_url = apiBaseUrl + "/environments/PRODUCTION/activate" # アクティブ用 payload を用意する post_payload = { "name": "firehol level1 IPs", "syncPoint": new_syncPoint } post_headers = { "accept": "application/json", "content-type": "application/json" } staging_response = apiRequest.post(staging_url, json=post_payload, headers=post_headers) print(staging_response.text) # 3秒を待つ time.sleep(3) production_response = apiRequest.post(production_url, json=post_payload, headers=post_headers) print(production_response.text)
lambda_function.py
の準備が終わりましたら、EdgeGrid や ライブラリと一緒に Lambda にアップロードします。
EventBridge の scheduler を作成する
最後に、定期的に Lambda 関数を実行するため、EventBridge の scheduler を作成します。EventBridge scheduler の作成手順 は AWS のドキュメントにご参考ください。ここでは割愛させていただきます。
Akamai で Network List を確認する
EventBridge scheduler が時間通りに実行された後、Akamai のコンソールで Network List の状態が確認できます。
FireHOL で公開している IP リストが対象 Network List のリスト内の項目に登録されていることが確認できます。そして、ステージングと本番環境のネットワークもアクティブ状態となっています。
最後、この Network List を Akamai WAP の保護ポリシーに登録すれば、ブロックが開始します。
以上、FireHOL で公開されているブラックリストからの接続 Akamai でブロックする方法をご紹介させていただきました。少しでも参考になれば嬉しいです。
明日の弁護士ドットコム株式会社の Advent Calendar 2023 の担当は @komtaki さんの「型パズルを理解しTypeScript中級者になる8のポイント」です。お楽しみに。