nginxからApache2のバーチャルホストにいい感じにリバプロする方法の続きで、nginxからApache2にリバースプロキシされた時にクライアントのIPを拾う設定の方法について。
確認環境
| Env | Ver |
|---|---|
| OS | Ubuntu 24.04.3 LTS |
| nginx | 1.26.1 |
| Apache2 | 2.4.58 |
やりたいこと
Apache2で動いているCGIのREMOTE_ADDRからクライアントIPを取れるようにする。
nginxからApache2のバーチャルホストにいい感じにリバプロする方法ではApache2側でREMOTE_ADDRを取得しようとするとnginxのIPになってしまうため、これを回避する方法。
前提条件
- nginxからApache2へのリバースプロキシをする時に、真のクライアントIPを
X-Real-IPヘッダーに入れていること。proxy_set_header X-Real-IP $remote_addr; - nginxとApacheが動いているマシンが同じ
- IPv6を使っている
やること
# remoteipモジュールの有効化
sudo a2enmod remoteip
# ::1からのHTTPリクエストを信頼しX-Real-IPをREMOTE_ADDRとして解釈する
cat <<'EOF' | sudo tee /etc/apache2/conf-available/remoteip.conf
RemoteIPHeader X-Real-IP
RemoteIPInternalProxy ::1
EOF
# 設定の有効化
sudo a2enconf remoteip
# 設定の反映
sudo systemctl restart apache2
意味合いの解説
sudo a2enmod remoteip
remoteipモジュールの有効化をしている。
逆はa2dismodで無効化出来るっぽい。
RemoteIPHeader <header-field>
ここで指定したHTTPヘッダーをクライアントのIPアドレス(useragent IP address)として扱うようにする。
例えばRemoteIPHeader X-Forwarded-ForとすればX-Forwarded-ForをクライアントIPとして扱える。
この設定だけだと問答無用で信頼してしまうため、本来と異なる経路で攻撃を受けた時に偽装されたヘッダをクライアントIPとして解釈するリスクがある。そのため、次項の設定も行う。
参考:mod_remoteip - Apache HTTP Server Version 2.4
RemoteIPInternalProxy <proxy-ip|proxy-ip/subnet|hostname ...>
RemoteIPHeader単品では前段のnginxを無視してApache2へ直にアクセスされたときに偽のX-Real-IPなどが来ていると、偽のIPが取れてしまうので、これを回避する仕組み。
RemoteIPInternalProxy ::1のように指定することで、そのIPから来たRemoteIPHeaderを信頼する。異なるIPからリクエストが来た場合は、RemoteIPHeaderを無視して、そのクライアントのIPが設定される。
RemoteIPTrustedProxyでも同じことができるのだが、違いはよくわかっていない。一応内部用と外部用で分かれているらしいが、RemoteIPTrustedProxy ::1でも期待通り機能したので謎。
公式ドキュメントではIPv4アドレスが指定されているので、恐らくIPv4でも行けると思うが確認していない。
参考:mod_remoteip - Apache HTTP Server Version 2.4
sudo a2enconf remoteip
作成した設定の有効化を行う。これを行っていない場合、設定は反映されない。
やっていることは/etc/apache2/conf-available/にある設定ファイルのシンボリックリンクを/etc/apache2/conf-enabled/に張っているだけだと思われる。
標準では/etc/apache2/apache2.confでIncludeOptional conf-enabled/*.confを指定しているため、これで設定が読み込まれるようになる。
逆はa2disconfで無効化できる。
あとがき
ここまで書いたところで/etc/apache2/sites-available/と/etc/apache2/sites-enabled/の違いに気が付いた。これも多分有効無効を切り替えるもので、sites-enabled側に設定を直書きするのは誤っているのだろう、きっと。
確認時のApache2のバージョンはApache/2.4.58 (Ubuntu)
Apache2のインストール
sudo apt -y install apache2
公開ディレクトリを触りやすくする
公開ディレクトリの所有者はデフォルトだとroot:rootで、そのままでは扱いづらいため自分自身でも手軽に扱えるように権限を調整する。
# 公開ディレクトリを見たりいじる為に自分をwww-dataに入れる
sudo usermod -aG www-data <自分のユーザー名>
# 公開ディレクトリを扱いやすくするためwww-data:www-dataに変える
sudo chown -R www-data:www-data /var/www
# 公開ディレクトリを扱いやすくするためにグループに全権限を付与する
sudo chmod -R g+rwx /var/www
mod_rewriteやmod_cgiを使えるようにする
CGIの実行に必要なPerlなどのランタイムは必要に応じて別途入れておく。PHPは一般的にCGIとして実行しないため、次項の方法で動かせるようにする。
sudo a2enmod rewrite
sudo a2enmod cgi
sudo service apache2 restart
PHPを使えるようにする
バージョンの部分は適宜書き換える。PHP本体をインストールしていない場合は別途インストールしておく(依存解決で勝手にインストールされるかもしれないが)
sudo apt install -y libapache2-mod-php8.3
sudo a2enmod php8.3
sudo service apache2 restart
投稿日:
nginxでhttps://example.comを受けて、https非対応のApache2にリバプロし、Apache2側でもhttps://example.comでアクセスされているように振舞わせる方法。取り敢えず最低限こんだけあると無難だろうという内容。
確認環境
| Env | Ver |
|---|---|
| OS | Ubuntu 24.04.3 LTS |
| nginx | 1.26.1 |
| Apache2 | 2.4.58 |
Apache2(後段)の設定
/etc/apache2/ports.conf
80や443を開けるとnginxと衝突するため、適当にずらしておく。
Listen 8080
/etc/apache2/sites-enabled/001-example.conf
ServerNameは外側と同じものにする。
<VirtualHost *:8080>
ServerName example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/example-error.log
CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>
nginx(前段)の設定
/etc/nginx/nginx.conf
httpセクションにupstream apacheを追記することで、conf.d/配下の設定ファイルから共通的に参照できるようにする。同じことを複数の設定ファイルに書くと構文エラーで起動しなくなる。
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream apache {
server [::1]:8080;
}
include /etc/nginx/conf.d/*.conf;
}
/etc/nginx/conf.d/example.conf
ここでは/etc/nginx/nginx.confで設定したupstreamにプロキシする設定を作っている。
proxy_set_header Host $host;はnginxが受けたホストヘッダをそのままApacheに飛ばすことで、Apache側がバーチャルホストでルーティングできるようにしている。Apacheとnginxで別々のホスト名にすることもできるが、そうした場合、Apache側はApache側で定義したホスト名で動作するため、CGIなどのスクリプトを動かすときに支障があるうえ、Apache側のホスト名をhostsに書かないと疎通できないこともあり、手間なので基本的に前段のドメインに合わせておくのが無難だ。
proxy_set_header X-Real-IP $remote_addr;はnginxにアクセスしてきたクライアントのIPをApacheに渡すための設定。そのままだとnginxのIPが渡ってしまう。
proxy_set_header X-Forwarded-Proto $scheme;もホストヘッダの転送と似ていて、nginxが受けたプロトコルスキーマをApacheに飛ばすための設定。こうしておくとApache側はhttpで受けているのに、httpsでアクセスされたものとして認識させることが出来るため、プロトコルスキーマ付きでURLを生成するCGIがいるときに都合がいい。単にスキーマを誤魔化しているだけなので実際は暗号化されていない。
server {
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
location / {
proxy_pass http://apache/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
余談:HTTP通信に用いられるHOSTヘッダーについて
HTTP通信で利用されるドメインは単にHOSTヘッダーにドメインを書いているだけなので、以下のようなことをするとhostsを書かなくとも疎通できる。
curl -H "HOST: lycolia.info" http://"[2403:3a00:101:13:133:167:8:98]"
これは一般的なHTTPクライアントの仕組みとして、まずドメインをDNSに問い合わせ、IPを取得したのち、ドメインをホストヘッダーに載せてIPアドレス宛にHTTP要求を出しているからだと思われる。
hostsの書き換えができない環境で、任意のドメインに対してHTTP要求を出したい時にも重宝するので、覚えておくと何かと役に立つ。
関連記事
WSL上のUbuntuで動作するnginxでHTTPSに対応したサーバーを作る方法。実機でも同様の手順でいける
確認環境
| Env | Ver |
|---|---|
| nginx | nginx/1.18.0 (Ubuntu) |
| mkcert | v1.4.4 |
| Ubuntu | 20.04.6 LTS |
| Windows 11 | 22621.3880 |
手順
- Windows側にmkcertを入れる
- mkcertで証明書を作る
mkcert sandbox.test
- 以下のpemファイルが生成される
sandbox.test.pemsandbox.test-key.pem
- 生成されたpemファイルを
/etc/nginx/conf.d/ssl/に移動する /etc/nginx/conf.d/sandbox.test.confを作成し、以下のような記述をするserver { listen 443 ssl; client_max_body_size 100m; server_name sandbox.test; ssl_certificate conf.d/ssl/sandbox.test+1.pem; ssl_certificate_key conf.d/ssl/sandbox.test+1-key.pem; access_log /var/log/nginx/sandbox.access.log; error_log /var/log/nginx/sandbox.error.log; # ファイルホスト用 # location / { # root /usr/share/nginx/html/sandbox; # index index.html; # try_files $uri /index.html =404; # } # APサーバーへのリバプロ用 location ~ ^/.*$ { rewrite ^/.*$ / break; proxy_set_header X-Request-Path $request_uri; proxy_set_header X-Host $host; proxy_pass http://127.0.0.1:9999; } # fastcgi用 location ~ \.php$ { root /usr/share/nginx/html/sandbox; fastcgi_pass unix:/run/php/php8.0-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }- nginxを再起動する
sudo service nginx restart
他の端末に証明書を撒く方法
mkcertのRoot CAをエクスポートして他の端末に突っ込めば、他の端末でもpemが流用できる
HTTPヘッダを持たないHTTPリクエストはあり得るのか?というのを検証しているときに気づいた話。
RFC 7230ではHostヘッダを持たないHTTPリクエストは禁止されており、これを受けたサーバーは400応答を返すことを必須としている。
RFC 7230:Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and RoutingのHostより
A client MUST send a Host header field in an HTTP/1.1 request even if
the request-target is in the absolute-form, since this allows the
Host information to be forwarded through ancient HTTP/1.0 proxies
that might not have implemented Host.
A server MUST respond with a 400 (Bad Request) status code to any
HTTP/1.1 request message that lacks a Host header field and to any
request message that contains more than one Host header field or a
Host header field with an invalid field-value.
なお、Node.jsのHTTPサーバー機能ではHostヘッダーのない要求を受け入れることができる。
http.createServer([options][, requestListener])を見ると、以下のようにrequireHostHeader: falseを渡すことで実現可能だ。規定値はtrueであるため、基本的にはHostヘッダーなしの要求は400応答が返される。
import http from 'node:http';
http
.createServer({requireHostHeader: false}, (req, res) => {
console.log(req.headers);
res.statusCode = 200;
res.end();
})
.listen(3000);
nginxにおいてもHostヘッダーなしの要求は以下の応答が返されたため同様と思われる。
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.26.0</center>
</body>
</html>
但し、nginxにおいてHostなしの要求を許容する方法は見つからなかった。Server namesによるとserver_name "";とすることで出来そうに見えたが、これは機能させることができず、400応答が返された。