弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

FireHOL で公開されているブラックリストからの接続 Akamai でブロックする

FireHOL で公開されているブラックリストからの接続 Akamai でブロックする  この記事は、弁護士ドットコム株式会社の Advent Calendar 2023 の 21 日目の記事です。


 皆さん、こんにちは!弁護士ドットコム SRE 室の @et_tei です。国籍は中国で、今年は来日13年目です。今回は FireHOL で公開されているブラックリストからの接続 Akamai でブロックする方法をご紹介します。

背景

 FireHOL で公開しているブラックリストの IP から当社のサイトへアクセスし、ログインできたログが検知されたことがあります。悪用の恐れがありますので、ブラックリストの IP からの接続を全部ブロックするように決定しました。FireHOL で公開しているブラックリストに記録されている IP レンジは数千件、unique IP は数億がありまして、そしてそのブラックリストが常に更新されるため、手動登録及び更新が不可能です。
 当社は Akamai という CDN サービスを利用していますので、Akamai が提供している WAP(Web Application Protector) に FireHOL で公開しているブラックリストの IP を登録してブロックすることを考えます。

 やることは以下の三点です。

  1. Akamai WAP で NetworkList を作成します。
  2. Lambda 関数を作成し、FireHOL から IP リストをダウンロードして、Akamai API で NetworkList を更新します。
  3. 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のポイント」です。お楽しみに。