動かざることバグの如し

近づきたいよ 君の理想に

Nginxだけで外部リソースをプロキシ配信する(動的proxy_pass)

環境

  • nginx/1.28.0

やりたいこと

画像などのファイルを外部リソースをURL指定したらプロキシ配信するのをNginxだけで実現したい。

例えば

http://test.turai.work/proxy?url=https://img.konami.com/games/shinepost/s/img/package.webp

https://img.konami.com/games/shinepost/s/img/package.webp を返すエンドポイントを作りたい。

設定

server {
  listen 80;
  listen 443 ssl;
  server_name ssl.example.com;

  ssl_certificate   path/to/your/certificate.crt;
  ssl_certificate_key path/to/your/certificate.key;

  # 名前解決用のDNSリゾルバを指定する 動的プロキシには必須
  resolver 8.8.8.8 1.1.1.1 valid=30s;
  resolver_timeout 5s;

  location /proxy {
    set $target_url $arg_url;
    if ($target_url = "") { return 400 "Bad Request: Missing 'url' parameter.\n"; }

    if ($target_url ~* ^https?://([^/]+)) {
      set $target_host $1;
    }

    proxy_set_header Host $target_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_ssl_server_name on;
    proxy_ssl_name $target_host;

    client_max_body_size 1g;

    proxy_pass $target_url;
  }
}

各ディレクティブのポイントを整理しておく。

resolver 8.8.8.8 1.1.1.1 valid=30s;

動的な proxy_pass を使う場合、nginxは起動時ではなくリクエスト時にホスト名を解決する必要がある。 そのためDNSリゾルバを明示的に指定しないと動かない。valid=30s でDNSキャッシュのTTLを上書きしている

resolver_timeout 5s;

DNS解決のタイムアウト。外部DNSに依存するので詰まった場合に無限待ちしないよう設定する

set $target_url $arg_url;

クエリパラメータ url の値を変数に入れる。$arg_XXX はnginxのビルトイン変数でクエリパラメータを簡単に取れる

1つ目のif

if ($target_url = "") { return 400 "Bad Request: Missing 'url' parameter.\n"; }

url パラメータが空の場合に400を返す。最低限のバリデーション

2つ目のif

if ($target_url ~* ^https?://([^/]+)) {
  set $target_host $1;
}

正規表現でURLからホスト名部分を取り出して $target_host にセット。proxy_set_header Host に渡すために必要

proxy_ssl_server_name on; proxy_ssl_name $target_host;

HTTPSのターゲットに対してSNI(Server Name Indication)を有効化する。これがないと証明書の検証で失敗するケースがある

proxy_pass $target_url;

変数を使った動的なproxy_pass。静的なホスト名ではなく変数を渡す場合はリゾルバ設定が必須になる

注意点

  • オープンプロキシになるリスクがある
    • この設定をそのまま公開すると、誰でも任意のURLへのリクエストを中継できるSSRF踏み台として悪用される可能性がある。
    • valid_referers による参照元制限や、許可するドメインをホワイトリスト管理するなどの対策が必要
  • 内部ネットワークへのアクセスに注意
    • url=http://169.254.169.254/ などのメタデータエンドポイントや社内リソースへのリクエストを中継してしまう恐れがある。クラウド環境では特に危険
  • url パラメータはURLエンコードが必要
    • クエリパラメータとして渡す都合上、URLに &= が含まれていると正しくパースされない。
    • クライアント側でエンコードして渡すことが前提になる
  • レスポンスのキャッシュは考慮していない
    • 同じ画像を何度もプロキシするならnginxのproxy_cacheを組み合わせると外部へのリクエスト数を減らせる

参考リンク