サーバーへの接続を絞り、アプリの前に WAF を置く

erpc worker は、最初のデプロイから、SSH を自分のアドレスからだけ受け入れ、ほかの受信接続をすべて落とします。[waf] テーブルを書くと、API の前にフィルタリングするプロキシが置かれます。
このページは、プレリリースの ERPC CLI v0.1.0-rc.4 について説明しています。

ファイアウォールで接続を絞る

デプロイは、アプリが初めて起動する前に、サーバーのファイアウォールを設定します。
  • SSH は firewall.ssh_allow と、デプロイを実行したアドレスからだけ受け入れます。
  • ほかの受信接続はすべて落とします。例外は firewall.app_allow からの run.port です。WAF の後ろにあるアプリでは、代わりに WAF のポートが例外になります。ただしそれは、サーバーの WAF が、[waf] が今作るルールをすでに動かしている場合だけです(下)。
変更はそれぞれ 120 秒のロールバックのタイマーの下で行われ、新しい接続が入れたときにだけ確定します。そのため、間違った変更で締め出されることはありません。
ただし、許可リストは、それを作ったときのアドレスが変わっても残ります。自宅のアドレスが変わった場合、戻る手段は、ERPC のサーバーなら ERPC のサポート、自分のサーバーならプロバイダーのコンソールです。自宅のアドレスを頼りにする前に、サーバーに届く 2 つ目のアドレス(オフィス、VPN の出口)を firewall.ssh_allow に入れておいてください。
アドレスが変わるなら、管理者が使う固定のアドレスをすべて加えます。
bash
erpc worker firewall allow --to ssh 203.0.113.10 198.51.100.7
コマンドすること
erpc worker firewall listサーバーに届いてよい相手を、erpc.toml とサーバーのテーブルを並べて表示します
erpc worker firewall allow --to ssh|app <addresses>アドレスを受け入れ、それを適用します。SSH は単一のアドレスからだけ許可でき、ネットワークを受け付けるのは --to app のときだけです
erpc worker firewall deny --to ssh|app <addresses>アドレスの受け入れをやめます
erpc worker firewall syncサーバーのファイアウォールを、erpc.toml に書かれたとおりに戻します

アプリの前に WAF を置く

erpc.toml にテーブルを 1 つ加えます。
toml
[waf]
port = 8080
すると erpc worker deploy は、サーバーで HTTP のリバースプロキシを、専用の systemd ユニット(erpc-waf-<app>-<env>)で動かします。このプロキシは特権のない動的ユーザーとして動き、鍵もシークレットも、ワーカーのものは何も持たず、メモリは 1 GiB に抑えられます。waf.port で待ち受け、各リクエストをアプリより先に読み、ルールが名指すものを拒否し、残りを 127.0.0.1:<run.port> のアプリへ転送します。
  • アプリはループバックに置いたままです。 アプリ自身のポートが外に開くことはありません。erpc worker api とヘルスチェックは、WAF を通らずにループバックでアプリに届きます。
  • 接続してよい相手は、今もファイアウォールのリストが決めます。 そのリストに「全員」と書けるのは、WAF のポートだけです。
    sh
    erpc worker firewall allow --to app 0.0.0.0/0     # and ::/0
    誰も入っていなければ、WAF は待ち受けますが誰も入れず、デプロイがそのことを伝えます。
  • ポートが開くのは、WAF がそこで待ち受けてからで、それより前には開きません。WAF が立ち上がらなかったデプロイは、理由を示して失敗し、アプリは動いたままです。
  • [waf] を変えたとき、新しいルールをデプロイが置き終えるまで、ポートは新しい相手には開きません。
  • erpc.toml から [waf] を外してデプロイすると、サーバーから WAF が取り除かれます。

WAF の設定

必須なのは port だけです。すべてのキーと、その既定値は次のとおりです。
toml
[waf]
port = 8080          # the public port, 1024 or above; the app keeps [run] port
methods = ["GET", "POST", "PUT", "DELETE"]
allow_paths = []     # every path
deny_paths = []
max_body_bytes = 1048576
max_header_bytes = 16384
rate = { per_second = 50, burst = 100 }
max_connections_per_address = 32
ban = { after = 100, window_secs = 60, for_secs = 600 }
block_probes = true
header_timeout_secs = 10
body_timeout_secs = 30
upstream_timeout_secs = 30
new_connections = { per_second = 50, burst = 100 }
キーすること拒否されたものが受け取るもの
methods転送するメソッド。大文字で書きます(最大 16 個。CONNECT は不可)405 と Allow
allow_paths転送するパス。何も並べなければ、すべてのパスです。パターンは 1 つのパスか、* で終わればその接頭辞の下のすべてです。/api/* は /api とその下のすべてを指します。パーセントデコードを 1 回したパスと、大文字・小文字を区別して比べます404
deny_paths拒否するパス。書き方は同じで、先に調べます403
max_body_bytesリクエストのボディの上限。最大 64 MiB413
max_header_bytesリクエスト行とヘッダーを合わせた上限。1024 から 65536431
rate1 つのアドレスからの 1 秒あたりのリクエスト数と、その率が掛かる前に同時に受ける数429 と Retry-After
max_connections_per_address1 つのアドレスが同時に持てる接続数応答せずに接続を閉じる
banwindow_secs の間に after 件のリクエストを止めたら、そのアドレスを for_secs の間(最大 30 日)拒否します応答せずにその接続を閉じる
block_probesスキャナーが求めるもの(/.env、/.git、/wp-login.php など)を拒否します403
header_timeout_secsクライアントがリクエストのヘッダー部分を送り終えるまでの時間。1 から 120。ボディの途中で止まったクライアントや、応答を受け取らなくなったクライアントを待つ時間でもありますヘッダー部分は 408。止まったボディは 400。受け取られない応答は接続を閉じる
body_timeout_secsリクエストのボディ全体にかけてよい時間。1 から 600。WAF がアプリを待つ時間は数えません408
upstream_timeout_secsアプリが接続を受けて応答を始めるまでの時間。1 から 600504。アプリが待ち受けていなければ 502
new_connectionsファイアウォール自身の上限。1 つのアドレスから WAF のポートへの 1 秒あたりの新しい TCP 接続数で、上限を超えた分は、プロキシが見る前に nftables が落とします。nftables のアドレスの集合がいっぱいの間は、そこに入っていないアドレスは数えられないので、集合に空きが出るまで、この上限はそのアドレスに掛かりません。プロキシ自身の上限(rate、max_connections_per_address、ban)は引き続き掛かりますパケットを落とす
rate、max_connections_per_address、ban、new_connections は false で無効にでき(ban = false)、書かなかったキーは既定値になります。erpc worker check は、どのデプロイよりも前に、[waf] テーブルが何をするか、どこが間違っているかを伝えます。

設定がなくても WAF が行うこと

  • 曖昧さのない 1 つの HTTP/1 リクエストとして読めないリクエストには 400 を返します。正規形でないパスにも 400 を返します。
  • プロトコルの切り替え(Upgrade)を求めるリクエストには 501 を返します。
  • 拒否はどれも ban の件数に数えられ、拒否するたびにその接続を終えます。
  • アドレスとは、IPv4 アドレス 1 つ、または IPv6 の /64 1 つです。
  • WAF は、クライアントからプロキシへの接続に属するヘッダー(Connection とそれが名指すヘッダー、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、Proxy-Connection、TE、Trailer)、Expect、名前に下線を含むヘッダー、クライアント自身が付けた Forwarded、X-Real-IP、X-Forwarded-Proto を転送しません。X-Real-IP には接続元のアドレスを設定し、そのアドレスを X-Forwarded-For の末尾に加えます。X-Forwarded-For のうち、プロキシ自身が書いたのは最後の項目だけです。

WAF の動きを見る

コマンド表示するもの
erpc worker statusWAF のポート、転送先、いつからか、そのとき以降の件数
erpc worker firewall listWAF のポートに届いてよい相手と、ルールごとに止めたもの
erpc worker logs --waf止めたリクエスト 1 件ずつ。ルール、ステータス、クライアントのアドレス、メソッド、ターゲットの先頭 200 バイト
転送したリクエストはログに残らず、ヘッダーやボディがログに残ることはありません。止めたリクエストのクエリは、200 バイトに入る範囲でログに残ります。クライアントが送ったものがターミナルに書き込まれることはありません。WAF はジャーナルを表示可能な ASCII で書きます。

WAF がしないこと

  • TLS はありません。 ポートでは素の HTTP/1.1 で通信します。
  • HTTP/2 はありません。 HTTP/2 には 400 を返します。
  • WebSocket も、ほかのどの Upgrade もありません。 501 を返します。
  • ボディの中は見ません。 ボディは大きさを測るだけで、読みません。スキャナーの一覧は短く、雑音を減らすフィルターであって保証ではありません。入力の検査は、引き続きアプリが行います。
  • 認証はありません。 アプリを呼んでよい相手は、アプリと、ファイアウォールのリストが決めます。
  • 1 つのアドレスを 1 つのクライアントとして扱います。 1 つの NAT、プロキシ、CDN の後ろにいるクライアントは、率も ban も共有します。多くのアドレスを持つクライアントは、アドレスの数だけ率を持ちます。
  • ban と件数は、WAF が再起動すると忘れられます。
  • 覚えておけるアドレスは最大 8192 個で、同時に持てる接続は最大 1024 です。
  • 待ち受けるのは IPv4 だけです。 そのため、app_allow の IPv6 の項目は、今はどこにも届きません。
  • パスは、デコードした後のバイト列として比べます。 パスをもっと緩く読むアプリ(大文字・小文字を区別しない、; のパラメーターを扱う、2 回デコードする)には、deny_paths が覆わない名前のまま届くことがあります。allow_paths にはこの隙間はありません。並べていないものは転送しません。
  • 読むのが遅いクライアントにも最後まで応答します。 どれほどゆっくりでも応答を受け取り続けるクライアントには、最後まで応答を返します。
  • 大量のリクエストで大きなアップロードが途切れることがあります。 WAF は 1 つのスレッドで動くので、大量のリクエストが来ると、大きなアップロードが 408 で途切れることがあります。大きなボディを受けるアプリは、max_body_bytes と一緒に body_timeout_secs も上げてください。
ワーカーのガイドには、app_allow の変更が次のデプロイまで保留される場合を含め、すべてのルールが載っています。