🙆

NGINX で想定外の永久リダイレクトが発生するパターン

2023/04/09に公開

概要

NGINX で想定外の永久リダイレクト (HTTP レスポンスステータスコード 301) が発生してしまうパターンにハマり、原因を調査した結果わかったことを記しておく

この記事のゴール

NGINX で想定外に永久リダイレクトが発生するパターンをノウハウとしてまとめておくことで、同じ事象にハマる人 (=未来の自分を含む) の助けになることを期待する

状況

前段のロードバランサーで TLS 終端を行い、後ろに NGINX を構えて HTTP で通信するシステム構成はよくあると思う
さらにこの状況で、セキュリティの関係でロードバランサーには HTTP リクエストを受け付けないように設定している状況もあり得ると思う

このシステム構成で気付かぬうちに NGINX で永久リダイレクトが派生してしまうと、通信が届かない状況が発生してしまう

ブラウザによっては永久リダイレクトを手動で削除しない限りブラウザキャッシュに持ち続けてしまう場合もあり厄介である

発生パターン

location /hoge/ + xxx_pass

公式情報

公式ページのこの言及
https://nginx.org/en/docs/http/ngx_http_core_module.html#location

If a location is defined by a prefix string that ends with the slash character, and requests are processed by one of proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass, memcached_pass, or grpc_pass, then the special processing is performed. In response to a request with URI equal to this string, but without the trailing slash, a permanent redirect with the code 301 will be returned to the requested URI with the slash appended. If this is not desired, an exact match of the URI and location could be defined like this:

location /user/ {
    proxy_pass http://user.example.com;
}

location = /user {
    proxy_pass http://login.example.com;
}

実際にやってみる

NGINX のバージョンは 1.23.4

location は前方一致なので、この書き方の場合、http://localhost/proxy/ の方に入ってしまう

nginx.conf
location /proxy/ {
    add_header Content-Type text/plain;
    return 200 "/proxy/ dayo";
}

location / {
    add_header Content-Type text/plain;
    return 200 "root dayo";
}
$ curl -s -L -D - http://localhost/proxy
HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 11:51:32 GMT
Content-Type: application/octet-stream
Content-Length: 9
Connection: keep-alive
Content-Type: text/plain

root dayo%

次に proxy_pass を指定してみる

nginx.conf
location /proxy/ {
    proxy_pass http://localhost/proxied/;
}

location /proxied {
    add_header Content-Type text/plain;
    return 200 "proxied dayo";
}

location / {
    add_header Content-Type text/plain;
    return 200 "root dayo";
}
$ curl -s -L -D - http://localhost/proxy
HTTP/1.1 301 Moved Permanently
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 11:58:01 GMT
Content-Type: text/html
Content-Length: 169
Location: http://localhost/proxy/
Connection: keep-alive

HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 11:58:01 GMT
Content-Type: application/octet-stream
Content-Length: 17
Connection: keep-alive

proxied dayo%

明示的に設定していないのに 301 Moved Permanently が発生した!

ちなみにこの状態で URI の最後にスラッシュをつけるとリダイレクトは発生しない

$ curl -s -L -D - http://localhost/proxy/
HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:00:41 GMT
Content-Type: application/octet-stream
Content-Length: 17
Connection: keep-alive

proxied dayo%

また、公式ページの言う通りに以下のように設定してもリダイレクトは発生しなくなる

nginx.conf
location /proxy/ {
    proxy_pass http://localhost/proxied/;
}

location /proxy {
    proxy_pass http://localhost/proxied/;
}

location /proxied {
    add_header Content-Type text/plain;
    return 200 "proxied dayo";
}

location / {
    add_header Content-Type text/plain;
    return 200 "root dayo";
}
$ curl -s -L -D - http://localhost/proxy 
HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:03:32 GMT
Content-Type: application/octet-stream
Content-Length: 17
Connection: keep-alive

proxied dayo%

try_files + $uri/

公式情報

公式ページのこの言及
https://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

It is possible to check directory’s existence by specifying a slash at the end of a name, e.g. “$uri/”.

実際にやってみる

NGINX のバージョンは 1.23.4

nginx.conf
error_page 404 /404.html;
location = /404.html {
    root /usr/share/nginx/html;
}

location /tryfiles {
    try_files $uri $uri/ =404;
}

${root}/tryfiles/index.html を配置済み

$ curl -s -L -D - http://localhost/tryfiles
HTTP/1.1 301 Moved Permanently
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:23:34 GMT
Content-Type: text/html
Content-Length: 169
Location: http://localhost/tryfiles/
Connection: keep-alive

HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:23:34 GMT
Content-Type: text/html
Content-Length: 265
Last-Modified: Sun, 02 Apr 2023 00:51:52 GMT
Connection: keep-alive
ETag: "6428d1a8-109"
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>/tryfiles/index.html</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>/tryfiles/index.html</h1>
</body>
</html>%

明示的に設定していないのに 301 Moved Permanently が発生した!

ちなみに、ディレクトリが存在しない場合はちゃんとスルーされる

$ curl -s -L -D - http://localhost/tryfiles
HTTP/1.1 404 Not Found
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:25:32 GMT
Content-Type: text/html
Content-Length: 298
Connection: keep-alive
ETag: "6428d146-12a"

<!DOCTYPE html>
<html>
<head>
<title>404 NotFound</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>NotFound</h1>
<p>The request URL was not found on this server.</p>
</body>
</html>%

location + if + try_files

公式情報

https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/

さまざまなおかしなことが発生するようだが、try_files と組み合わさった場合の言及は以下

# try_files wont work due to if

location /if-try-files {
     try_files  /file  @fallback;

    set $true 1;
    
    if ($true) {
         # nothing
     }
}

実際にやってみる

NGINX のバージョンは 1.23.4
全く意味のなさそうな if ディレクティブを入れてみる

nginx.conf
location /tryfiles {
    try_files $uri $uri/index.html =404;

    set $true 1;

    if ($true) {
	# nothing
    }

}
$ curl -s -L -D - http://localhost/tryfiles
HTTP/1.1 301 Moved Permanently
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:47:57 GMT
Content-Type: text/html
Content-Length: 169
Location: http://localhost/tryfiles/
Connection: keep-alive

HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:47:57 GMT
Content-Type: text/html
Content-Length: 265
Last-Modified: Sun, 02 Apr 2023 00:51:52 GMT
Connection: keep-alive
ETag: "6428d1a8-109"
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>/tryfiles/index.html</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>/tryfiles/index.html</h1>
</body>
</html>%

明示的に設定していないのに 301 Moved Permanently が発生した!
というかもはや意味がわからない挙動をしている

if ディレクティブをコメントアウトするとこの挙動はなくなる

nginx.conf
location /tryfiles {
    try_files $uri $uri/index.html =404;

    # set $true 1;

    # if ($true) {
    #     # nothing
    # }

}
$ curl -s -L -D - http://localhost/tryfiles
HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 09 Apr 2023 12:50:35 GMT
Content-Type: text/html
Content-Length: 265
Last-Modified: Sun, 02 Apr 2023 00:51:52 GMT
Connection: keep-alive
ETag: "6428d1a8-109"
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>/tryfiles/index.html</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>/tryfiles/index.html</h1>
</body>
</html>%

本当に意味がわからないが、location の中に if を使うのは基本的にやってはいけないようだ

まとめ

NGINX の設定で想定していない永久リダイレクトが発生するパターンを列挙した
特に NGINX 公式の情報はいまいち具体的にどういう挙動をするのかが読み取りにくく、実際に動かしてみないとわからない部分があるため注意が必要
うっかりこれらの罠を踏んでしまわないようにしたい

paiza

Discussion