2026/07/06(月)nginxのリバースプロキシが上手くいかない問題を解消した

ConoHa VPSをフロントエンドにし、自宅サーバーにリバースプロキシする構成を組んだ時のトラブルメモ。

起きていた条件

nginxからnginxへリバースプロキシする構成で前段がTLS終端で、後段へはHTTPで接続していた。前段はVPS、後段は物理サーバーで別の環境。

起きていた事象

lycolia.info*.lycolia.info全体で前段が後段に接続できない状態になっていた。

curl -iSsl4 https://blog.lycolia.info/
HTTP/2 000 
server: nginx
date: Fri, 03 Jul 2026 15:24:44 GMT

前段のnginxにcurlを投げるとステータスコード000が返ってきて、curlが終了コード23で異常終了していた。

2026/07/04 00:11:33 [error] 2537#2537: *12468 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: xxx.xxx.xxx.xxx, server: lycolia.info, request: "GET /pub/lycolia/rss.xml HTTP/2.0", upstream: "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/pub/lycolia/rss.xml", host: "blog.lycolia.info", referrer: "https://blog.lycolia.info/pub/lycolia/rss.xml"

nginxのエラーログには「アップストリームからのレスポンスヘッダーの読み取り中に、アップストリームが有効な HTTP/1.0 ヘッダーを送信しませんでした。」という意味のエラーを出力していた。

後段のnginxには何もログが出ていなかった。

curl -v --http1.1 -H 'Host: blog.lycolia.info' "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/"
*   Trying [2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80...
* Connected to 2400:4153:8f01:c800:c14b:3f7a:2b54:353a (2400:4153:8f01:c800:c14b:3f7a:2b54:353a) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: blog.lycolia.info
> User-Agent: curl/8.14.1
> Accept: */*
> 
* Request completely sent off
* Received HTTP/0.9 when not allowed
* closing connection #0
curl: (1) Received HTTP/0.9 when not allowed

前段のnginxのあるVPSから、後段のnginxに対してhttpバージョン1.1を明示して叩くと「HTTP/0.9 when not allowed」というエラーが出てきた。HTTP/0.9…?初めて聞く概念だ…。

curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd           
00000000: 0000 1204 0000 0000 0000 0300 0000 8000  ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800  ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000  ................
00000030: 0000 0000 0000 0000 01                   .........

httpバージョン0.9を明示して叩き、帰ってきたレスポンスのバイナリ。端的に言うとプロトコルエラーで帰れと言われているが、理由は後述する。

解決した方法

+listen [::]:80;
-listen [::]:80 http2;

後段のmstdn.lycolia.infoの設定を上記に変更することで、他のドメインでも問題が解決した。

何故解決したかはわからないが、恐らく80 http2と書くと、他の80番ポートのリッスンにも影響が波及するのだと思う。

http2通信にしなくても問題ないのかでいうと、Mastodonは問題なさそうに見えたのでたぶん大丈夫なんだと思う。知らんけど。

何故解決したのか

使用しているnginxのバージョンがproxy_passするときにhttp 2.0をサポートしてなかったからだ。

nginx 1.29.4になるとproxy_passでhttp 2.0がサポートされるようで、2025年12月9日のリリースには以下の一文があった。

*) Feature: the ngx_http_proxy_module supports HTTP/2.

この時ばかりは今までnginxのバージョンに興味が微塵になかった私もバージョンを上げたくなった瞬間だった。

ただ後続のバージョンのバグフィックスを見るに結構バグが出てそうなので、今あげるのは時期尚早かもしれない。まぁ現状困ってないので上げなくてもいいのは幸いだ。

ところでv1.25.1になるとlisten単位のhttp2が非推奨になり、代わりにhttp2 onという全体単位?が推奨設定になるため、将来的に現状のポート単位にhttpバージョンを分ける技は使えなくなりそうである。

あとがき

バイナリ読解

前述したHTTP 2.0のバイナリレスポンスを読んでみようのコーナー。

curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd           
00000000: 0000 1204 0000 0000 0000 0300 0000 8000  ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800  ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000  ................
00000030: 0000 0000 0000 0000 01                   .........

そのバイナリを見るといいという情報をやってみたので試してみた結果。

まず16進数なので1桁に0-Fが入る。これは0000~1111ということだ。つまり1桁が4bitであることが分かる。

さて、HTTP/0.9は一旦忘れて、先ほどやってきたHTTP/2.0として解釈してRFC 9113 - HTTP/2を読んでみる。

「4. HTTPフレーム」によればHTTPフレームの先頭24bitがLength、8bitがType、8bitがFlag、1bitがReserved、31bitがStream Identifierらしい。

つまり先頭を分解するとこうなる。

Length: 00 00 12
Type: 04
Flag: 00
Reserved+Stream Identifier: 00 00 00 00

Lengthだ。0x000012なので18個の8bitフィールドがある。

次の8bitがTypeで0x04となっている。0x046.5.1. 設定フォーマットだ。

Reserved+Streamは全て0なので無価値だろう。

設定フォーマットではそこから48bit単位が設定フィールドとなる。先頭16bitがIdentifier、後ろ32bitがValueだ。

18個の8bitフィールドが設定になるため18 * 8 = 144, 144 / 48 = 3で3つの設定が存在すると読める。

00 03 00 00 00 80

つまりこの部分になる。

Identifierは00 03なのでSETTINGS_MAX_CONCURRENT_STREAMS。最大同時ストリーム数だ。

Valueは00 00 00 80なので128

次は00 00 00 00 10なのでSETTINGS_HEADER_TABLE_SIZE。ヘッダ圧縮テーブルの最大サイズ。

04 00 01 00なので67,109,120。オクテット単位なので単位はバイトと思われる。オクテットのオクはオクトパスのOctと同じなのでタコの脚は八本と考えると覚えやすい。Octoberも旧暦の8月だからOct。

次は00 00 05なので、SETTINGS_MAX_FRAME_SIZE

00 ff ff ff 00なので4,294,967,040。これも単位なので単位はバイトと思われる。

これで設定が終わり、またフレームに戻る。

00 0004 0800
0000 0000 7fff 0000 0000 0807 0000 0000
0000 0000 0000 0000 01

これが残り。

Length: 00 00 04 → 4 * 8 = 32bit
Type: 08WINDOW_UPDATE
Unused Flags: 00
Reserved+Stream: 00 00 00 00
Reserved+Window Size Increment: 7f ff 00 002,147,418,112

次の残り

0000 0807 0000 0000
0000 0000 0000 0000 01

Length: 00 00 08 → 64bit
Type: 07帰れ
ここまでくれば尻だけ読めばいいので中間は読み飛ばす。
最後の32bitがエラーコードらしいので00 00 00 01が恐らくエラーコード。意味合い的にはPROTOCOL_ERROR

要するにHTTP 2に対してHTTP 1.1で接続していたのでプロトコルエラーが返却され、しかしnginxは理解できなかったのでHTTP 0.9として解釈していたのだろう。

今回組んだ構成

今回nginxからnginxにリバースプロキシをしたわけだが、結果として上図のような構成にした。前段にはConoHa VPSを利用している。

これをした理由としては、以前フレッツ光クロスでフルポート使えるIPv4が取れないことが判明したため、クロスに移行出来るようにするためというのと、IPv4のためにDDNSし続けるのが地味に面倒なのと、宅内からIPv4を排除したかったところによる。

ただこれはこれでVPS側にもTLS証明書が必要になるため、DDNSが消えてもIPv4保守のための呪いはまだ残る。DDNSよりはマシだが…。

また、この構成では前後でnginxのバージョンが異なり前段がv1.26.3、後段がv1.24.0となっているため、前段ではhttp2 onにしないと警告が出るなど、バージョン差異による違いが微妙にある。

結果的にできた構成

今回の施策によって自宅サーバーからIPv4が失われても、AレコードをConoHa VPSに向けることでIPv4を受けることが可能な体制の検証をすることができた。これによってフレッツ光クロスに乗り換えたときのIPv4不在問題を解消できる状態となった。

実運用に移すためにはVPS側にもTLS証明が必要になるので自宅サーバーからSCPで送るか、VPS側でも取得する必要が出てくるので、まだまだ対策が必要だ。後者は同一ドメインに二重に証明書が存在する状態になるのでなんか嫌だし、恐らく前者でやるだろう。TCP パススルーなる技術も健闘したが、真のクライアントIP転送問題など、いろいろ厄介そうなのでやめた。

SCPで送る場合はパスフレーズ付きの鍵認証を突破させ、更にnginxをリロードさせないといけないからこちらもやや頭が痛い。パスフレーズ付きのSSH鍵を無人突破させるにはどこかに平文でパスフレーズを持つ必要がある。暗号化もできるがどんどん複雑に…。

或いはDNS-PERSIST-01を使うことで証明書更新を不要にするのも一つの選択肢かもしれないので、こちらも検討していいかもしれない。

しかしVPSがあることで監視対象が増えるなど、なかなか複雑化してしまうので、これを軌道に乗せるかどうかは現時点では不透明だ。ただConoHa VPSは最低スペックで36ヶ月契約であれば月額293円のため、36ヶ月換算でも10,548円にしかならず、IPv4を保持する手段としてはお手頃である。安いISPでもv4を買おうとすると月2~3,000円はするし、まともなところだと一万円以上するので、IPv4にサーバーがついてこの価格は破格と言える。

帯域は100Mbpsだが、100Mbpsあれば十分だろう。

おまけConoHa VPSで帯域実測

自宅サーバーとConoHa VPS間でiperf3をやってみた結果。自宅サーバーへの穴あけが面倒だったので自宅サーバー→ConoHaという逆経路でしか試していない。

Interval Transfer Bitrate Retr Cwnd
0.00-1.00 sec 24.4 MBytes 204 Mbits/sec 0 1.42 MBytes
1.00-2.00 sec 10.9 MBytes 91.2 Mbits/sec 0 1.43 MBytes
2.00-3.00 sec 12.2 MBytes 103 Mbits/sec 0 1.43 MBytes
3.00-4.00 sec 11.0 MBytes 92.3 Mbits/sec 0 1.43 MBytes
4.00-5.00 sec 10.9 MBytes 91.2 Mbits/sec 0 1.43 MBytes
5.00-6.00 sec 12.2 MBytes 103 Mbits/sec 0 1.43 MBytes
6.00-7.00 sec 11.0 MBytes 92.3 Mbits/sec 11 1.26 MBytes
7.00-8.00 sec 11.0 MBytes 92.3 Mbits/sec 169 762 KBytes
8.00-9.00 sec 11.0 MBytes 92.3 Mbits/sec 0 807 KBytes
9.00-10.00 sec 12.2 MBytes 103 Mbits/sec 0 838 KBytes
  • sender: 106 Mbits/sec
  • receiver: 103 Mbits/sec

2026/07/01(水)MantisBTのステータスをカスタマイズする

更新日:
投稿日:

MantisBT標準のステータスはどうにも使いづらいので、独自のステータスを追加して置き換える方法。

確認環境

  • MantisBT 2.27.1

やりたいこと

MantisBT標準のステータスを捨てて、一人用のチケット管理システムとして使えるようにする。要するにTODO管理の様なものにする。

ステータスの再定義

まずステータスを再定義する。

ステータスは標準でnew, feedback, acknowledged, confirmed, assigned, resolved, closedと七種類あるが、これをdraft, not started, in progress, pending, testing, completedの六つに変える。意味合いとしては下書き、未着手、仕掛中、保留、試験中、完了だ。

最後に解決状態も同様にopen, fixed, reopened, unable to duplicate , not fixable, duplicate, not a bug, suspended, wont fixと九つあるのを、in completed, duplicate , wont fix, completedの四つに再定義する。

このように状態を絞ることでTODO管理ツールとして機能させることが可能になり、更に状態管理も単純になってやりやすくなる。

やり方

  1. config/config_inc.phpを開きステータスの定義を追加する

    # ステータスの定義
    # <優先度>:<キー名>として並べる。値はDBに登録される値として利用される。
    $g_status_enum_string = '10:draft,20:not started,30:in progress,40:pending,50:testing,100:completed';
    
    # 定義したキー名に対し課題一覧で表示される色を決める
    $g_status_colors['draft']       = '#fcbdbd';
    $g_status_colors['not_started'] = '#e3b7eb';
    $g_status_colors['in_progress'] = '#ffcd85';
    $g_status_colors['pending']     = '#c2dfff';
    $g_status_colors['testing']     = '#fff494';
    $g_status_colors['completed']   = '#d2f5b0';
    
    # チケットの解決状況の定義
    # <優先度>:<キー名>として並べる。値はDBに登録される値として利用される。
    $g_resolution_enum_string = '10:in completed,20:duplicate,30:wont fix,90:completed';
    
  2. config/custom_strings_inc.phpを作成し、ここまでで作った状態を和訳する。

    <?php
    
    # ステータス表示名
    # 完了はフィルタで以上が付くので一番後ろに置いておくとよいと思う
    $s_status_enum_string = '10:起案,20:未着手,30:仕掛中,40:保留,50:試験中,100:完了';
    
    # ステータス変更画面のタイトル・ボタン・通知タイトル
    $s_draft_bug_title = 'ステータスを起案に変更';
    $s_draft_bug_button = '起案にする';
    $s_email_notification_title_for_status_bug_draft = '課題が起案されました。';
    
    $s_not_started_bug_title = 'ステータスを未着手に変更';
    $s_not_started_bug_button = '未着手にする';
    $s_email_notification_title_for_status_bug_not_started = '課題が未着手になりました。';
    
    $s_in_progress_bug_title = 'ステータスを仕掛中に変更';
    $s_in_progress_bug_button = '仕掛中にする';
    $s_email_notification_title_for_status_bug_in_progress = '課題が仕掛中になりました。';
    
    $s_pending_bug_title = 'ステータスを保留に変更';
    $s_pending_bug_button = '保留にする';
    $s_email_notification_title_for_status_bug_pending = '課題が保留になりました。';
    
    $s_testing_bug_title = 'ステータスを試験中に変更';
    $s_testing_bug_button = '試験中にする';
    $s_email_notification_title_for_status_bug_testing = '課題が試験中になりました。';
    
    $s_completed_bug_title = 'ステータスを完了に変更';
    $s_completed_bug_button = '完了にする';
    $s_email_notification_title_for_status_bug_completed = '課題が完了しました。';
    
    # 解決状況表示名
    $s_resolution_enum_string = '10:未解決,20:重複,30:対応不要,90:対応完了';
    
  3. 既にMantisBTを利用している場合、既存のチケットの表示がバグるため、ステータスと解決状況が整合するようにDBを適当に整える。

    -- ステータスの置換
    UPDATE mantisbt.mantis_bug_table SET status = 20
    WHERE status != 10;
    
    -- 解決状況の置換
    UPDATE mantisbt.mantis_bug_table SET resolution = 100
    WHERE resolution = 20;
    
  4. あとは管理画面に入りワークフローを組めば終わりである。個人で使うものなので制約はだいぶ緩めにしている。

あとがき

これで管理がだいぶ楽になりそうだ。MantisBTはバグ管理ツールなのでTODO管理をしようとすると標準ではしんどいが、カスタマイズを重ねることで大分マシになる。

2026/07/01(水)nginxをinit.dからsystemdに移行してみた

Ubuntu 26からinit.dが非推奨になるが、systemdの設定が見つからなかったので試しに書いてみた。

確認環境

  • Ubuntu 24.04.4 LTS
  • nginx/1.24.0

手順

sudo mv /etc/init.d/nginx ~
sudo service stop nginx
cat <<'EOF' | sudo tee /etc/systemd/system/nginx.service
[Unit]
Description=nginx
Documentation=man:nginx(8)
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=simple
ExecStartPre=/usr/sbin/nginx -t -q
ExecStart=/usr/sbin/nginx -g 'daemon off; master_process on;'
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -QUIT $MAINPID
TimeoutStopSec=5
KillMode=mixed

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable nginx
sudo systemctl start nginx

参考記事

あとがき

nginxはマスタプロセスが上がったままなのでtype=simpleで問題なさそうだったが、正直よく分かってないので、何か問題が出たらまたその時に対処したい。

init.dにはまだ沢山転がってるので徐々に移行させていかないと…。

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。

関連記事

あとがき

今回のアクセス制御の導入にあたり、運用面で課題のある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にかかることで想定外の挙動をするから、使うならreturnrewrite ... 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でアクセスしてきたときに、ハンドシェイクをせず突き返す事ができるようだ。わずかではあるもののサーバーの負荷やネットワークのトラフィックが抑えられそうだ。

以下のようにlisten443sslをつけなくても同じように動いたが、違いはよく分かっていない。

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から読めるようにしたらいい気はするのだが、面倒なのでそれはまた今度やろうと思う。