猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

AWS ALB のヘルスチェックと Django の ALLOWED_HOSTS

f:id:yoshiki_utakata:20210307171243p:plain

はじめに

一般的に、 AWS で Web アプリケーションを動かすときは、 Application Load Balancer *1 と EC2 を利用します。

Application Load Balancer を使う場合、ヘルスチェックの設定をする必要がります。

例えば、 urls.py で、 Django のエンドポイントを追加し、

urlpatterns = [
    ...
    # LB からのヘルスチェックに答えるためのURLで、アプリが起動していれば常に 200 OK を返す
    url(r'^status$', lambda request: HttpResponse()),
]

LB からは /status に対してリクエストし、ステータスコード 200 が返ってきたらヘルスチェックOKとします。

ALLOWED_HOSTS との兼ね合い

しかし、 Django には ALLOWED_HOSTS という設定があります。

例えば、 http://localhost/status のように Django にアクセスする場合は、 ALLOWED_HOSTSlocalhost が追加されていないとアクセスできません。

ALLOWED_HOSTS = [
    "localhost",
]

ALLOWED_HOSTS に追加されていないホスト名でアクセスすると、ステータスコード 403 のエラーになります。

AWS Application Load Balancer は、http://アプリがデプロイされているEC2のプライベートIP にアクセスして、ヘルスチェックを行います。

アプリがデプロイされているEC2のプライベートIP を ALLOWED_HOSTS に追加していないと、403 エラーになってしまい、ヘルスチェックに失敗してしまいます。

対策

対策は色々考えられます。

  • Status Code が 403 ならヘルスチェック OK とする
  • アプリケーションが動いている EC2 のプライベートIPを動的に取得して許可するようにする
  • プライベートIPなら無条件でアクセス許可するようにする
  • どんなホストでアクセスされてもアクセスを許可するようにする

全アクセスを許可するのが一番ラクだが、ALLOWED_HOSTS という設定項目が存在するのには意味があるはずです。 Django のドキュメントを読んでみます。

ALLOWED_HOSTS の意味

Django の公式ドキュメントの ALLOWED_HOSTS について書いてある部分を読むと、

https://docs.djangoproject.com/ja/3.1/ref/settings/#allowed-hosts

Django サイトを配信できるホスト/ドメイン名を表す文字列のリストです。これはセキュリティ対策の手段の1つで、一見安全な設定の Web サーバでも晒される可能性が高い、 HTTP Host header 攻撃 を防ぐことができます。

さらに、「Hostヘッダーの検証」について書いてある部分を読むと、

https://docs.djangoproject.com/ja/3.1/topics/security/#host-header-validation

Djangoはいくつかのケースで、URLを組み立てるためにクライアントから送られてきた Host ヘッダーを使用します。 ...

とあります。

つまり、

  • Django は、ページのURLを組み立てるときに、リクエストに含まれる Host ヘッダーを利用することがあります。
  • 通常 Host ヘッダーは、 DNS 解決のときに使われたヘッダーが利用されます。しかし、単なるリクエストヘッダーのため書き換えは容易です。そのため、変な URL を生成することが可能になってしまいます。

リクエストヘッダを利用して、URL生成のような処理を行うのは、一般的には良くないです。

Django は、便宜上、 Host ヘッダを利用しているようで、そのかわり、 ALLOWED_HOSTS で、Hostヘッダに指定できる値を制限しているのです。

つまり、結論としては、「ALLOWED_HOSTS ですべてのホストを許可するのは危ない」となります。

ホストヘッダインジェクションの例

Host ヘッダーに限らず、ユーザーが入力可能な値を信頼して利用するのは危険です。

ヘッダーは、 JavaScript で書き換えができないため、そんなに危険ではないという話もあります。

http://hasegawa.hatenablog.com/entry/20151110/p1

しかし、 Django は Host ヘッダーを信用する実装になっている(と思われる)ため、無闇に全許可するのはやめるべきです。

結局どうすればいいのか

選択肢はこのいずれかになります。

  • Status Code が 403 ならヘルスチェック OK とする
  • アプリケーションが動いている EC2 のプライベートIPを動的に取得して許可するようにする
  • プライベートIPなら無条件でアクセス許可するようにする

事例を調査する

動的にEC2のIPを取得して許可する 例が多いように思いました。

progl.hatenablog.com

デメリットとして、IPアドレスが取得できなかった場合にアプリを起動できなくなってしまいます。

「Status Code が 403 ならヘルスチェック OK とする」はどうでしょうか。

若干気持ち悪さはありますが*2、 Django アプリケーションが動いていることは確認できるので、問題ないです。

私は実際この方法を採用おり、問題なく動いています。ヘルスチェックのためのエンドポイントを用意しなくていいので楽です。難しい問題から開放されます。

「プライベートIPなら無条件でアクセス許可するようにする」 ですが、Django の ALLOWED_HOSTS は CIDR (IPレンジ)が記述できないので、非常に面倒です。

stackoverflow.com

nginx で 200 を返す という案もありましたが、Django が正常に動いてなくてもヘルスチェックがOKになってしまうのがデメリットです。

結論

結論は

  • Status Code が 403 ならヘルスチェック OK とする
  • アプリケーションが動いている EC2 のプライベートIPを動的に取得して許可するようにする

がいいかなと思いました。

また、広くみんなに使われている手段は後者でした。

*1:AWS には Elastic Load Banancer (ELB) という、ロードバランサーを立てられるサービスがありますが、立てられるロードバランサーの種類に Classic Load Banancer や Application Load Balancer 、 Network Load Balancer などがあります。

*2:Status Code が正常系ではないのに、ヘルスチェックがOKとなっている点が