nginxで国単位のIPアドレス制限

セキュリティのために日本国外からのアクセスをブロックしようとしたときの覚書。

環境: CentOS Stream 8, nginx 1.20.2, PostgreSQL 14.2

IPアドレスから国を判定するためにMaxMind社が提供しているデータを使う。
MaxMind社はIPアドレスの位置情報を提供しているアメリカの会社。
参考: MaxMind - Wikipedia

nginxにGeoIP2モジュールを追加するとパフォーマンスに影響が出そうな気がするので、まずは国ごとのIPアドレスリストを生成することから始めてみた。

MaxMindはAPIを幅広く提供していて使いやすい。

GeoLite2はクリエイティブコモンズライセンスで提供されている。
配布する場合はクレジット表示が必要。
参考: GeoLite2 Free Geolocation Data | MaxMind Developer Portal


国別IPアドレスリストをPostgreSQLにインポート

公式サイトを参考に。MySQLへのインポート方法もある。
PostgreSQLにはcidrというIPv4とIPv6を格納するデータ型があるので便利。


簡単な手順

  1. メールアドレスでサインアップする。
  2. マイページ左メニューの「Download Files」から「GeoLite2-Country-CSV」zipファイルをダウンロード。
    ASNはAS番号を割り当てられた組織
    参考: 自律システム (インターネット) - Wikipedia
  3. PostgreSQLにインポート

CSVファイルを開発サーバーに置いて、DBeaverでデータベースを作成して、公式サイトのcreate table文を実行。
参考: Importing GeoIP2 and GeoLite2 databases to PostgreSQL | MaxMind Developer Portal

あとはコンソールで作業する。作成したgeoデータベースにアクセス。
# su - postgres
$ psql geo

確認のためテーブル一覧表示。
geo=# \d

IPv4リストのCSVをgeoip2_networkテーブルへインポート実行。
geo=# \copy geoip2_network(network, geoname_id, registered_country_geoname_id, represented_country_geoname_id,is_anonymous_proxy, is_satellite_provider) from '/home/httpd/GeoLite2-Country-Blocks-IPv4.csv' with (format csv, header);

IPv6リストのCSVをgeoip2_networkテーブルへインポート実行。
geo=# \copy geoip2_network(network, geoname_id, registered_country_geoname_id, represented_country_geoname_id,is_anonymous_proxy, is_satellite_provider) from '/home/httpd/GeoLite2-Country-Blocks-IPv6.csv' with (format csv, header);

IPアドレスと国コードを紐づけるテーブルをインポート実行。
geo=# \copy geoip2_location(geoname_id, locale_code, continent_code, continent_name, country_iso_code, country_name, is_in_european_union) from '/home/httpd/GeoLite2-Country-Locations-ja.csv' with (format csv, header);

試しに自分のIPがどこの国かselectしてみる。
geo=# SELECT * FROM geoip2_network gn LEFT JOIN geoip2_location gl on gn.geoname_id = gl.geoname_id WHERE gn.network >> '210.191.11.22';

nginxのアクセスログで変な動きのIPアドレスの送信元を調べてみる。
geo=# SELECT * FROM geoip2_network gn LEFT JOIN geoip2_location gl on gn.geoname_id = gl.geoname_id WHERE gn.network >> '185.153.199.66';

ビット演算子を初めて使った。
参考: PostgreSQL: Documentation: 14: 9.6. Bit String Functions and Operators

geo=# quit


nginxの拒否/許可リストを生成

nginxの設定ファイル用にdeny/allow文字を付けてSQL文を実行する。
(SQLの実行結果をファイルに書き出す)

ロシアからの拒否リスト生成。
$ psql geo -c "SELECT concat('deny ', network, ';')  FROM geoip2_network gn LEFT JOIN geoip2_location gl on gn.geoname_id = gl.geoname_id WHERE gl.country_iso_code = 'RU';" >> deny_RU.conf

中国からの拒否リスト生成。
$ psql geo -c "SELECT concat('deny ', network, ';')  FROM geoip2_network gn LEFT JOIN geoip2_location gl on gn.geoname_id = gl.geoname_id WHERE gl.country_iso_code = 'CN';" >> deny_CN.conf

日本からの許可リスト生成。
$ psql geo -c "SELECT concat('allow ', network, ';')  FROM geoip2_network gn LEFT JOIN geoip2_location gl on gn.geoname_id = gl.geoname_id WHERE gl.country_iso_code = 'JP';" >> allow_JP.conf

ファイルの先頭と末尾のいらない文を削除。
viコマンドは「1G」で先頭に、「G」でファイル末尾に移動できる。
「dd」で行削除。
# less deny_RU.conf

rootに戻ってnginx設定。
conf構成はGistに置いてあるので参考に。
参考: Nginx Configuration Files for WordPress. Nginx is installed by YUM. @see:https://codex.wordpress.org/Nginx
# cd /var/lib/pgsql/
# mv deny_CN.conf deny_RU.conf allow_JP.conf /etc/nginx/conf.d/global/
# cd /etc/nginx/conf.d/global/
# less wordpress_restrictions.conf

#
# Black List
#
include conf.d/global/deny_RU.conf;
include conf.d/global/deny_CN.conf;

nginxは上から順に評価していく。
なのでdenyのあとに「allow all;」は必要ない。
allow_JP.confを使う場合はincludeのあとに「deny all;」は必要。

例えば中国の一部のIPアドレスを許可する場合はdeny_CN.confの前にallowしてあげればOK。

nginx再読み込み。
# nginx -t
# systemctl reload nginx

ロシアのdenyリストは22,316行、中国は13,831行ある。
これだけ毎回チェックするのはパフォーマンスに影響しそう。
nginxモジュールの方がいいかもしれないと思い始めた。

結局Cloudflareと連携する方が幸せになるのかもしれない。
(CloudflareにはIPアドレスから国コードを判定する機能がある)


定期更新

公式サイトでも定期的に更新する方法の説明がある。
GeoLite2 Countryは毎週火曜日に更新される。

そのうち自動化しようと思う。



【関連記事】