2026/06/27(土)nginxでGeoIP2とUserAgentを利用したアクセス制御を実装してみた
投稿日:
過去に何度かBOTなどを避けるためのアクセス制御を導入していたが、結果どれも微妙だった。しかしどうにかしたい、そうだ、nginxは高機能なのでできるんじゃないか?と思って実践してみたログ。
User-AgentはGEOIP2と何も関係ないが、ついでにやったので書いている。
確認環境
| Env | Ver(apt上) | 備考 |
|---|---|---|
| nginx | 1.24.0-2ubuntu7.13 | nginx -vではnginx/1.24.0 (Ubuntu) |
| nginx-common | 1.24.0-2ubuntu7.13 | |
| libnginx-mod-stream | 1.24.0-2ubuntu7.13 | |
| libnginx-mod-http-geoip2 | 1:3.4-5build2 |
やったこと
互換性確保のためnginxのダウングレード
インストールされていたnginxが1.26.1-2~jammyで、libnginx-mod-http-geoip2との互換性がなかったためダウングレードすることにした。
まず次のコマンドで整合性を確認した。コマンド自体はGPT-5.5が作ってくれたものであるが、こうすることでダウングレードを許可した上での互換性チェックができるようだ。
コマンドの実行結果としては既に追加されているコンポーネント、ダウングレードされるコンポーネント、新たに追加されるコンポーネントの一覧が出てくる。
# ngx_http_geoip2_module.soが存在しないことを確認。既にある場合は本項の工程は飛ばせると思う
ls -la /usr/lib/nginx/modules
sudo apt install -s --allow-downgrades \
nginx=1.24.0-2ubuntu7.13 \
nginx-common=1.24.0-2ubuntu7.13 \
libnginx-mod-stream=1.24.0-2ubuntu7.13 \
libnginx-mod-http-geoip2
上記の構成でインストールできることが分かったので-sを外してnginx本体をダウングレードし、他をインストールする。
sudo apt install --allow-downgrades \
nginx=1.24.0-2ubuntu7.13 \
nginx-common=1.24.0-2ubuntu7.13 \
libnginx-mod-stream=1.24.0-2ubuntu7.13 \
libnginx-mod-http-geoip2
# ngx_http_geoip2_module.soが増えていることを確認
ls -la /usr/lib/nginx/modules
ngx_http_geoip2_module.so
GEOIP2データベースの入手と配置
MMDBなら何でも行けると思うので、Matomoで使ってる無料の奴を使う。理想的にはCRONで定期取得するのがいいと思うが、面倒なので今回はやってない。
sudo mkdir -p /usr/share/GeoIP
sudo wget https://download.db-ip.com/free/dbip-country-lite-2026-06.mmdb.gz -O /usr/share/GeoIP/dbip-country-lite.gz
gunzip /usr/share/GeoIP/dbip-country-lite.gz
nginxの設定でGEOIPの有効化を行う
/etc/nginx/nginx.confに以下の設定を追記すると、$geoip2_で始まる変数にMMDBの中身が入るようになる。
load_module modules/ngx_http_geoip2_module.so;
http {
...
geoip2 /usr/share/GeoIP/dbip-country-lite {
auto_reload 24h;
$geoip2_metadata_country_build metadata build_epoch;
$geoip2_country_code country iso_code;
$geoip2_country_name country names en;
}
...
nginxのJSONログに国コードを含める
JSONログにキーを追加してやればよい。
http {
...
log_format main_json escape=json
'{'
...
'"country_code":"$geoip2_country_code"'
'}';
国コードとUserAgentの判定フィルタの元ネタを作る
nginxのmap構文を使って判定用の元ネタを作る。
左側は~で始めると正規表現扱いになり、~*とすると大文字小文字の違いを無視してくれるようになるらしい。
http {
# headlesschromeを2にしてるのは、これはちょっと毛色が違うと思ったため、便宜上分類分けしている。
# 但し現時点でこの値を特別扱いしておらず、1と等価なので無駄ではある。
map $http_user_agent $deny_ua {
default 0;
"~*meta-externalagent" 1;
"~*baidu" 1;
"~*headlesschrome" 2;
"" 1;
}
map $geoip2_country_code $deny_ca {
default 0;
"CN" 1;
"BR" 1;
"IN" 1;
}
# nginxのif文にはandやorに相当する演算子がないのでmapで合成しておく
map "$deny_ua:$deny_ca" $deny_client {
default 1;
"0:0" 0;
}
....
}
雰囲気で読んでいるが構文の意味合いはこうだと思う。KeyValueで並べていくと入力値($input_variable)のKey(input_value)に対するValue(output_value)が出力値($output_variable)に入るのだと思う。
map $input_variable $output_variable {
default <default_value>; # デフォルト値
input_value output_value;
...
}
フィルタ用のsnippetsを作る
vhostに撒くように/etc/nginx/snippets/deny_client.confのようなsnippetsを作る。
前述のmapで判定をまとめているのでif文一つで制御できるようになっている。
if ($deny_client = 1) {
return 403;
}
snippetsをvhostに撒く
反映したいvhostの設定にsnippetsを撒いていく。
server {
...
include snippets/deny_client.conf;
...
動作検証
こんな感じで検証して弾かれてれることを確認した。国コードはどうにもならないが原理上問題ないはずなので大丈夫だろう。
curl -H "User-Agent:meta-externalagent/1.1" https://lycolia.info
あとは念のために国コードが出ているかどうかをnginxのログを見て出ていたらOK。
関連記事
- 自宅サーバーの一部がダウンしたが監視を入れていてよかった話
- Facebook(Meta)のBOTのせいでサーバーが落ちた話
- Anubisを雑に設置したが、結果微妙だったので撤去した話
- Anubisで防げないか試してみたが、Anubisのストレージや、アクセス解析との親和性などの関係でなかったことにしたやつ
- このブログにやってくる海外IPのBOTの挙動を軽く調べた
- 取り敢えずどんな迷惑アクセスがあるか調べたときの奴
- CGIのラッパーCGIを作る方法
- この記事ではadiaryに対するアクセス制御のラッパーを書いていた
あとがき
今回のアクセス制御の導入にあたり、運用面で課題のあるAnubisを回避しつつ、大量に存在するCGIにラッパーCGIを嚙ますなどの対応を回避できたのはよかった。
nginxは何とも高性能だ。そして自宅サーバーに全部乗せしたおかげで、レンタルサーバーでは決して手が届かないことができるのが良いと思った。
しかしググってもGEOIP無印の情報ばかりで、GEOIP2の情報が全然なかったので地味にハマってしまった。こういう時LLMに情報を漁ってもらうと取っ掛かりが得られて突破口を見つけられたりするので便利だと感じる。
パッケージを見ていて気になったこと
パッケージを見ていて気になったことは元のコードは保守されてないにもかかわらず、ディストリごとにコードが保守されているように見えたことだ。
今回導入したのはlibnginx-mod-http-geoip2だが、これは元を辿ればDebianのlibnginx-mod-http-geoip2のようで、更に元を辿るとleev/ngx_http_geoip2_module: Nginx GeoIP2 moduleに行きつく。
何故ならDebianのリポジトリがGitHubより新しく、READMEの中身が同じだからだ。設定方法もまるっきり同じに見える。
GitHubのリポジトリは2年以上放置されており「Support nginx 1.23.0」で時が止まっているが、Ubuntuでは「Rebuild against new nginx 1.30.1.」とあるため、だいぶ新しいところまでサポートが進んでいる。
よく考えるとパッケージのバージョンに-ubuntuみたいなのがついているのも良くあることなので、もしかしたらディストリごとにこういったアプリケーションを保守していたりするのだろうか?
そういえばPHPなんかも本家がサポート切れててもUbuntuではサポート内とか聞いたことがある。動作サポートなのかセキュリティサポートなのか、何のサポート課なのかまでは知らないが、星の数ほどあるパッケージをディストリごとに保守しているとしたら、これは凄いことだなと思ったし、他人が書いた得体のしれないパッケージをどう保守しているのかも気になった。
Linuxの世界は興味深い…。
nginxのダウングレードからのバージョンの復旧について
ダウングレードしても基本的に影響はないと思うが、元のバージョンに上げようとするとUbuntuそのもののバージョンアップが必要なことを知った。
しかし24.04.04 LTSから26.04への安全なアップグレードはまだ提供されていないようなのでいったん断念した。26.04.01が出ると上げられるようになるらしく、現状はお試し版みたいな感じらしい。
よくあるx.00は安定板だけど不具合がある、x.01で真の安定板になるみたいな話はUbuntuにもあるんだなと思った。
あとがき
昔nginx公式のどこかにnginxでifを使うなみたいなのがあったと思う。たぶん今はもう消えていて、GitHubに残ってる残骸に残るだけになっているようだ。
ここを見る限りifを使うとifの判定後、別のifにかかることで想定外の挙動をするから、使うならreturnやrewrite ... lastと組み合わせて、確実にリクエストを殺すようにしろということのように読めた。
公式リファレンスを見てもifそのものを使っている個所はngx_http_rewrite_moduleに存在するため、「If is Evil...」だから使ってはいけないというより、適切に使えればよいのだと思った。
あと思ったのだが、恐らくこの「If is Evil...」はnginx公式ではなく、nginx plusの方にあったドキュメントだった気がする。サイトのカラーが緑だった記憶があるからというのと、このリポジトリがそれっぽいからだ。
2026/06/26(金)nginxに未定義のホスト名で要求が来たときにエラーを返せるようにした
投稿日:
nginxのバーチャルホストを増やしたり減らしたりしていて未定義のドメインにアクセスしたとき、最も若い設定ファイルに飛ばされることに気づいた。
これだと未定義のバーチャルホストを叩いたときに変なところに飛んでいくので、エラーにするようにした。
起きていた現象
git.lycolia.infoの設定をnginxから削除し、DNSにレコードが残っている状態でhttps://git.lycolia.infoを叩くと他の特定のドメインでホストしているサイトに飛ばされる状態になっていた。
例えば/etc/nginx/conf.d/access-analizer.confがあるとした場合、未定義の任意のドメイン名を叩くと、この設定ファイルで定義した場所が表示される状態だった。リダイレクトなどはなく、URLそのままで表示される感じ。
確認環境
- nginx/1.26.1
対処方法
/etc/nginx/conf.d/default.confなど、適当に設定ファイルを作って次の設定を記述すると対処できる。
server {
listen 80 default_server;
listen 443 ssl default_server;
listen [::]:80 default_server;
listen [::]:443 ssl default_server;
ssl_reject_handshake on;
return 444;
}
default_serverと書くと、未定義のホストに対するアクセスがすべてここに吸収される。
server_name ""にしておくとIP直打ちのアクセスもトラップできるらしいが、手元で確認した感じは、あってもなくても機能するように見えたので付けていない。
ssl_reject_handshake onにしておくことで、未定義のホストに対してHTTPSでアクセスしてきたときに、ハンドシェイクをせず突き返す事ができるようだ。わずかではあるもののサーバーの負荷やネットワークのトラフィックが抑えられそうだ。
以下のようにlistenの443にsslをつけなくても同じように動いたが、違いはよく分かっていない。
server {
listen 80 default_server;
listen 443 default_server;
listen [::]:80 default_server;
listen [::]:443 default_server;
ssl_reject_handshake on;
return 444;
}
余談がうちのサーバーは外向きに80番ポートを開けていないので、80に対して制御を入れているのはもっぱらローカル用だ。開発環境を増減させてる時に変なところに繋がると面倒なので入れている。
検証結果
この設定をしたことで次のようなHTTP要求をすべてエラーで返せるようになった。
curl -H "Host:hoge.lycolia.info" "https://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]"
curl: (35) error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 unrecognized name
curl "https://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]"
curl: (35) error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 unrecognized name
curl -H "Host:hoge.lycolia.info" "https://180.33.219.150"
curl: (35) error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 unrecognized name
curl "https://180.33.219.150"
curl: (35) error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 unrecognized name
但しhttpスキーマにして443ポートに投げるとHTMLが返ってくるようだった。まぁ別にいいかと思ったが、サーバーのバージョンが出てると微妙な気がしたのでserver_tokens off;を/etc/nginx/nginx.confに足しておいた。記事で明かしているとはいえ、悪意のあるBOTが機械的に叩いてきたときに見られると余りよくない。
curl -H "Host:hoge.lycolia.info" "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:443"
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
curl "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:443"
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
curl -H "Host:hoge.lycolia.info" "http://180.33.219.150:443"
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
curl "http://180.33.219.150:443"
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
おまけで80番を叩いて見たらそもそも繋がらなかった。ルーターで塞いでるのでnginxに届いていない。
curl -H "Host:hoge.lycolia.info" "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]"
curl: (7) Failed to connect to 2400:4153:8f01:c800:c14b:3f7a:2b54:353a port 80 after 4 ms: 接続が拒絶されました
curl "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]"
curl: (7) Failed to connect to 2400:4153:8f01:c800:c14b:3f7a:2b54:353a port 80 after 8 ms: 接続が拒絶されました
curl -H "Host:hoge.lycolia.info" "http://180.33.219.150"
curl: (7) Failed to connect to 180.33.219.150 port 80 after 7 ms: 接続が拒絶されました
curl "http://180.33.219.150"
curl: (7) Failed to connect to 180.33.219.150 port 80 after 6 ms: 接続が拒絶されました
あとがき
HTTPステータスコードに444なんてあったっけ?と思ったらnginxの特殊コードの様で、このコードが指定されるとnginxはレスポンスを返さず、その場でTCPコネクションを切るらしい。
前述のdefault_serverのマニュアルを読み返すと軽く触れられていることに気がついたのでドキュメントを漁ったら根拠が出てきた。
公式ドキュメントのreturnに以下のようにあるため、コネクションを閉じ何も返さないという事は間違いなさそうだ。
Stops processing and returns the specified to a client. The non-standard code 444 closes a connection without sending a response header. code
あとがきのあとがき
80に対して設定するとnginx_exporterにホスト名を指定できない関係上、上手くいかなくなるので80向けの設定は後日外した。
恐らくstub用のvhostにsocketを指定してnginx_exporterから読めるようにしたらいい気はするのだが、面倒なのでそれはまた今度やろうと思う。
2026/06/22(月)nginxで設定をモジュール化して再利用する
PHP-FPMとかの設定がvhostに散らばってて一元化したかったけど/etc/nginx/nginx.confにlocationディレクティブを書くと構造上エラーになるため、再利用可能なモジュールと切り出し各vhostの設定から呼び出して解決したログ。
確認環境
- nginx/1.26.1
やり方の一例
PHP-FPMを設定する例
/etc/nginx/snippetsに適当な名前で.confを作る- 例:
/etc/nginx/snippets/php-fpm.conf
- 例:
- 作ったファイルに設定を書く
location ~ ^.+\.php$ { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS on; fastcgi_param SERVER_PORT 443; } 読み込みたいvhostの
.confから参照するserver { listen [::]:443 ssl; server_name hoge.example.com; # ワイルドカード証明の設定も外出ししておくと共通化できて便利 include snippets/cert.conf; client_max_body_size 100M; root /var/www/hoge; index index.php; location / { index index.php; try_files $uri $uri/ =404; } include snippets/php-fpm.conf; }
2026/06/10(水)今日のサーバー整備の話と、サイト改築を始めた話。
今日のサーバー整備
nginxの共通設定にgzipの設定を入れて転送量を削減した。
この設定を入れただけ。
gzip on;
gzip_types
text/plain
text/css
text/javascript
text/xml
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
application/font-woff
application/octet-stream
image/gif
image/png
image/jpeg
image/gif
image/webp
image/svg+xml;
nginxとApacheのリバースプロキシについて色々確かめた。
nginxからApacheにリバースプロキシしてる時、nginxを前段のTLS終端、Apacheを後段にしているときにApacheの中でリダイレクトが走るとhttpsが壊れることがある現象を確認した。
これはディレクトリに対しhttps://example.com/hogeのようにアクセスするとhttp://example.com/hoge/に301リダイレクトがかかるのだが、どうもこの時にスキーマが引き継がれずHTTPになってしまうようだ。初めからhttp://example.com/hoge/としてアクセスすれば問題ない。
またこの現象が起きると証明書があるのにエラー表示になることも確認しているが、サブ機やスマホでは起きないようにも見えるので、この端末の証明書が何かしら変になっている可能性がある。キャッシュクリアしてもhttp降格が起きると再発するので、原因はよくわかっていない。GPT-5.5に聞いたもののハルシネーションしか得られず、ググってもnginx→Apacheの情報はほとんど得られなかった。多分ニッチな構成なのだろう。
さくらのレンタルサーバーもnginxからApacheに繋いでいるので、これをどうやっているかが知れれば改善の手掛かりになりそうだが、知る術がないので何ともだ。まさか聞くわけにもいかないし…。
そういえばさくらのレンタルサーバーではSNI SSL[1]が使われていると聞くので、これを使えば上手くいくのかもしれない。
SSL/TLS接続のはじめに、クライアントはSSL/TLSのサーバから(サーバとCAの)証明書を受け取り、証明書の改ざんされていないことなどを確認する。サーバ証明書にはホスト名が書かれており、それが今接続しようとしているホスト名と一致することをクライアントは確認する。そうでない場合、なりすましや中間者攻撃の恐れがあるため、クライアントはユーザに警告をする。ユーザの責任で証明書を信用し、警告を迂回することができるアプリケーションも存在する。
というかWikipediaに上記の記述があったので、これのせいかもしれない。問題が起きているドメインはlycolia.infoで、元々さくら側で証明書を設定していたのでHTTPに落ちたときにその証明書が引っかかってエラーになっているとかはあるのかも?
さくらのドメインであるsakura.ne.jpを踏むとhttpでもhttpsに転送されるのと、ひょっとしたら類似の現象が起きているのかも…?いや、わからないが…。
サイト構成の変更をし始めた
このサイトの構成がイケてないなという思いが強くなったのでやることにした。
まずlycolia.infoが単なるLPに成り下がっていることが勿体なかったので、このドメインの傘下にコンテンツを増やすことにした。
一つ目はtool.lycolia.infoのドメインを廃止して、lycolia.info/tool/にぶら下げた。これはそもそもドメインを分割してやるようなサイトではなかったし、コンテンツの一部でいいと思ったからだ。
二つ目はこのブログにあったリンク集をlycolia.info/link/にぶら下げた。そもそも、本質的にサイトトップではない、このブログにリンク集があることに長らく違和感があったので丁度よかった。また移設に伴い紹介コメントも多少変更しているほか、元々のリンクが間違っていたなどもあったので、そのあたりも修正している。
そして今後は自己紹介などもブログから剝がして移動したいと思っている。ただ今のところすべてサーバー内からのHTML手打ちで、厳しさを感じるので、将来的にはCMSにしたいとも思った。
少なくとも今今は自宅サーバーである以上、家の中にいれば直にファイルをいじれるので問題はない。出先で書き換えたくなった時に困るくらいだ(一応頑張れば出先からもいじれなくはないが…)
サイト構成の変遷
本日リアルタイムでサイトを書き換えていて本番Hot Module Replacementみたいなことになっていた風景の一部を残しておく。
これが元々の状態。
改築途中の状態、なんか色々アレなことに…。
そして一旦本日の作業が終わった状態。また明日以降ちょいちょい直していくと思う。
移設したリンク集
見た目的には大きく変わり映えしないどころか、まともにスタイルを当ててないので殺風景だが、その辺りは追々どうにかしていこうと思う。
これでも一応レスポンシブデザイン、しかもモバイルファーストで作っているのでスマホで見たときの見やすさは向上している。
何せフルスクラッチでHTMLを手書きして、置換ではなく手作業で移設を行ったので移設するだけで時間が溶けてしまったので、今日はこんなところといった感じだ。
- Server Name Indication SSL ↩





