ECS Fargate 上でフォワードプロキシサーバを構築する (前編)
こんにちは。エンジニアチームの犬束です。
今回は、ECS Fargate 上でフォワードプロキシサーバを構築する方法を紹介します。
構成としては、NLB を置き、ECS (Fargate) 上で動いている Squid コンテナを NLB のターゲットとする形です。
NLB のドメインでフォワードプロキシサーバを指定して使うことを想定しています。
想定しているユースケース
プライベートサブネット内の Lambda や ECS で任意の Web サイトのクローラを作るケースを想定します。
プライベートサブネットからインターネットへアクセスするので、目的の Web サイトにアクセスする際のソース IP アドレスは、NAT Gateway に割り当てた Elastic IP に固定されます。可能な限りソース IP アドレスを分散させたいと考えましたが、無料のプロキシサーバは不安定かつセキュリティ面で不安が残り、一方で有料のプロキシサーバはトラフィックが増えると比較的良いお値段がします。
それ故、AWS 上でフォワードプロキシサーバを構築できないかという発想に至りました。AWS 上で構築するということは、AWS が保有している IP アドレスしか利用できないという制約はあるものの、その制約内である程度のソース IP アドレスの分散はできると考えられます。
想定している構成を図にまとめると、以下のようになります。
この構成を選定した理由
ECS
Google で検索すると、EC2 でフォワードプロキシサーバを構築している記事はいくつか見かけました。
ただ、EC2 で構築する場合サーバのメンテナンスが面倒だなと感じるのが正直なところです。ECS Fargate で構築すれば、同じ構成のコンテナを何台立てるのも楽ですし、定期的にコンテナを落として IP ローテーションをするといったことも難なくできそうです。というわけで、ECS Fargate を前提に検討しました。
なお、コンテナで Squid を動かす、ECS で Squid を動かすといったテーマを取り上げた記事もいくつか見かけたものの、今回のように NLB -> ECS Fargate (Squid)
という構成に言及した記事は私が調べた限りでは見つけられませんでした。
NLB
ECS で IP ローテーションしようと思うと、パブリックサブネットに配置して IP 自動割当を有効化するのが手っ取り早いと思われます。ただし、IP が自動割当されるということは、接続するフォワードプロキシサーバの IP アドレスが毎回変わることになります。
それだとクローラを開発する際にプロキシの設定が難しいので、何かしらのドメインでアクセスができるようになるのが望ましいです。したがって、ECS の前にロードバランサを挟もうという考えになるのですが、ALB と NLB どちらを使うかが問題になります。
普段 ALB を使うのに慣れているので、今回も ALB でと思っていたのですが、実験してみるとうまくいきませんでした。ALB 自体がリバースプロキシの仕組みなので、フォワードプロキシサーバを作りたいという今回の要件に合わないようです。
そのため、NLB を利用することにしました。
Squid
次に、フォワードプロキシサーバを立てるのに使うソフトウェアを選定する必要があります。
今回、候補としては Nginx と Squid を検討していました。Squid は使ったことがなく、まず Nginx を検討したのですが、調べてみると 2 点不便な点がありました。
- Nginx はデフォルトでは HTTPS 通信をフォワードプロキシできないため、ソースコードをダウンロードした上で別途パッチを適用する必要がある[1]
- Nginx はデフォルトでは Digest 認証に対応していないため、ソースコードをダウンロードした上で別途 Digest 認証モジュールを追加してコンパイルする必要がある[2]
2 点目に関しては、上に書いたように ECS の IP 自動割当を有効化するためにパブリックサブネットに配置しようと思うと、サーバが全世界に公開されてしまうため、何かしらの認証を設けたいと考えていました。Basic 認証よりはまだ Digest 認証の方がマシかなということで Digest 認証を使おうと思っていたのですが、Nginx はデフォルトだと対応していないと知って意外に思いました。
Squid は上記 2 点いずれもデフォルトで対応しています。
パッチ適用や外部モジュール追加をしても良いのですが、管理がやや煩雑になりそうな気がしたため、Squid を利用することにしました。
実装
Squid の設定ファイル
まずは Squid の設定ファイルから見ていきます。
長いので、全体をご覧になりたい方はアコーディオンを開いてください。
squid.conf
auth_param digest program /usr/lib/squid/digest_file_auth -c /etc/squid/password
auth_param digest children 20 startup=0 idle=1
auth_param digest realm MyRealm
auth_param digest nonce_garbage_interval 5 minutes
auth_param digest nonce_max_duration 30 minutes
auth_param digest nonce_max_count 50
acl digest_user proxy_auth REQUIRED
http_access allow digest_user
# Squid normally listens to port 3128
http_port 35201 require-proxy-header
# Add NLB IP addresses to ACL
acl nlb_ip src xxx.xxx.xxx.xxx/xx
# Add client IP addresses to ACL
acl client_ip src xxx.xxx.xxx.xxx/32
# Permit using proxy protocol
proxy_protocol_access allow nlb_ip
proxy_protocol_access allow client_ip
# Permit http access
http_access allow nlb_ip
http_access allow client_ip
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http
acl CONNECT method CONNECT
#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports
# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports
# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager
# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
http_access deny to_localhost
# And finally deny all other access to this proxy
http_access deny all
# Set PID file to a place the default squid user can write to
pid_filename /var/run/squid/${service_name}.pid
# Change log format
logformat timefm %{%Y/%m/%d %H:%M:%S}tl %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %[un %Sh/%<a %mt
# Disable cache
cache deny all
# Hide hostname
visible_hostname unknown
# Hide source IP address
forwarded_for off
# Prevent the fact that accessing via proxy from being known to target
request_header_access X-Forwarded-For deny all
request_header_access Via deny all
request_header_access Cache-Control deny all
# Don't display the version on the error page.
httpd_suppress_version_string on
# Use stdio and redirect access log to stdout and daemon log to stderr
access_log stdio:/proc/self/fd/1 timefm
# Leave coredumps in the first cache dir
coredump_dir /var/cache/squid
デフォルトの設定と異なる箇所のみ解説します。
Digest認証の設定
auth_param digest program /usr/lib/squid/digest_file_auth -c /etc/squid/password
auth_param digest children 20 startup=0 idle=1
auth_param digest realm MyRealm
auth_param digest nonce_garbage_interval 5 minutes
auth_param digest nonce_max_duration 30 minutes
auth_param digest nonce_max_count 50
acl digest_user proxy_auth REQUIRED
http_access allow digest_user
-
/usr/lib/squid/digest_file_auth
は認証用のプログラムですが、OS により場所が異なるようです。後ほど触れますが、今回はdebian:bullseye-slim
を使っており、その場合はこの場所に配置されるようです。 -
/etc/squid/password
はパスワードファイルの保管場所です。保管場所を変更する場合は適宜変えてください。 -
MyRealm
は認証領域の指定です。任意の文字列を指定できます。 -
digest_user
は ACL の名前です。任意の文字列を指定できます。 - その他、各種パラメータの意味は公式ドキュメントに詳しいです。
Listen するポートの指定
# Squid normally listens to port 3128
http_port 35201 require-proxy-header
- Squid はデフォルトでは 3128 番ポートで Listen しますが、パブリックサブネットに配置する都合上、デフォルトとは異なるポートで Listen するのが望ましいと思います。
- NLB を通った通信のアクセス元は、通常は ECS は知ることができません[3]。ECS がアクセス元を知ることができるようにするためには NLB において Proxy Protocol を有効化する必要があり、Squid 側でも Proxy Protocol を必須にするために
require-proxy-header
という指定を付ける必要があります[4]。
許可するアクセス元 IP アドレスの設定
# Add NLB IP addresses to ACL
acl nlb_ip src xxx.xxx.xxx.xxx/xx
# Add client IP addresses to ACL
acl client_ip src xxx.xxx.xxx.xxx/xx
# Permit using proxy protocol
proxy_protocol_access allow nlb_ip
proxy_protocol_access allow client_ip
# Permit http access
http_access allow nlb_ip
http_access allow client_ip
- ECS をパブリックサブネットに配置するため、Squid 側でアクセス元 IP アドレスの制限をかけておくのが非常に大事です。NLB が配置されている VPC の CIDR (
nlb_ip
) と、NLB にアクセスするアクセス元の IP アドレス (client_ip
) を設定し、いずれも Proxy Protocol を利用許可する必要があります。xxx.xxx.xxx.xxx/xx
としている箇所は適切な IP アドレスを記述してください[5]。 - 複数のアクセス元 IP アドレスを設定したい場合、
acl client_ip src xxx.xxx.xxx.xxx/xx
を必要な行数だけ増やします。
その他細かな設定
# Change log format
logformat timefm %{%Y/%m/%d %H:%M:%S}tl %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %[un %Sh/%<a %mt
# Use stdio and redirect access log to stdout and daemon log to stderr
access_log stdio:/proc/self/fd/1 timefm
# Disable cache
cache deny all
# Hide hostname
visible_hostname unknown
# Hide source IP address
forwarded_for off
# Prevent the fact that accessing via proxy from being known to target
request_header_access X-Forwarded-For deny all
request_header_access Via deny all
request_header_access Cache-Control deny all
# Don't display the version on the error page.
httpd_suppress_version_string on
- デフォルトのログのフォーマットには時刻やユーザエージェントなどがないため、少し設定を変更します。
- ECS では標準出力に出力された内容が CloudWatch Logs にログとして残るため、ログの出力先を標準出力に変更します[6]。
- 今回はキャッシュしたいという要件はないので、キャッシュは無効にします。
- アクセス元を隠しておきたいため、ホスト名やソース IP アドレスを隠匿する設定を入れておきます。
- デフォルトではエラーページに Squid のバージョンが表示されます。悪意のある攻撃者に利用される恐れがあるため、これも表示されないようにします[7]。
Squid の起動スクリプト
start_squid.sh
#!/bin/sh
set -e
SQUID=$(/usr/bin/which squid)
# Prepare the cache using Squid.
echo "Initializing cache..."
"$SQUID" -z
# Give the Squid cache some time to rebuild.
sleep 5
# Launch squid
echo "Starting Squid..."
exec "$SQUID" -NYCd 1
Squid の起動スクリプトは上記のようにしました。使っているオプションは以下の通りです[8]。
-
-z
: キャッシュディレクトリを作成するオプションで、最初の起動時には実行します。 -
-N
: デーモンモードで起動しないオプションです。Docker でデーモンモードで起動すると即終了してしまうため、このオプションを付けます。 -
-C
: fatal signal をキャッチしない設定をするオプションです。 -
-Y
: ファストリロード時にUDP_HIT
かUDP_MISS_NOFETCH
のみ返すようにする設定をするオプションです。 -
-d
: デバッグも標準エラー出力に流すようにするオプションです。
Dockerfile
Dockerfile
FROM debian:bullseye-slim
ARG PROXY_USERNAME
ARG PROXY_PASSWORD
ENV TZ=Asia/Tokyo
RUN apt-get update && apt-get install -y --no-install-recommends squid tzdata
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
RUN groupadd squid
RUN useradd -g squid -d /home/squid squid
RUN mkdir /var/cache/squid
RUN mkdir /var/run/squid
RUN echo | awk -v username=${PROXY_USERNAME} -v realm=MyRealm -v hash="$( echo -n "${PROXY_USERNAME}:MyRealm:${PROXY_PASSWORD}" | md5sum | cut -d ' ' -f 1 )" '{print ""username":"realm":"hash""}' > /etc/squid/password
RUN chown -R squid:squid /var/log/squid
RUN chown -R squid:squid /var/cache/squid
COPY start-squid.sh /usr/local/bin/
COPY squid.conf /etc/squid/
RUN chmod 755 /usr/local/bin/start-squid.sh
RUN chmod 755 /etc/squid/squid.conf
RUN chown -R squid:squid /etc/squid/password
RUN chown -R squid:squid /var/run/squid
USER squid
CMD ["/usr/local/bin/start-squid.sh"]
Dockerfile は上記のようにしました。ポイントは以下のとおりです。
- セキュリティの観点から、Squid の Digest 認証のユーザ名とパスワードは build-arg としてビルド時のみ保持するように渡しています (
build-arg
として渡された値はビルドされたコンテナ内には残らないため、環境変数などで渡すより安全性が高い) - セキュリティの観点から、
squid
ユーザを作成して Squid 起動に必要なファイルやディレクトリのみにアクセス権を渡し、コンテナ起動時はroot
ではなくsquid
ユーザとして起動しています - 日本時間でログが出るようにするため、タイムゾーンを日本標準時にする設定をしています
- MD5 で暗号化されたパスワードファイルを生成するのには htdigest を使うのが一般的ですが、対話型プロンプトでパスワードを与えるのが Dockerfile では面倒なため、awk、md5sum、cut コマンドを使って生成しています
このあたりまで準備できれば、あとは ECS Fargate にデプロイするだけというところですが、長くなってきたので続きは次回の記事に書きます。
次回は、ECS Fargate へのデプロイおよび関連するリソースの設定周りを解説し、実際に起動したフォワードプロキシサーバにアクセスしてフォワードプロキシとして機能していることの確認まで行います。
-
詳細は https://fujiu.hatenablog.com/entry/2020/03/15/010353 をご参照ください ↩︎
-
詳細は https://qiita.com/heiwa_pinf/items/72bd8569320f9362a5b3 をご参照ください ↩︎
-
Proxy Protocol に関しては https://dev.classmethod.jp/articles/nlb-meets-proxy-protocol-v2/ をご参照ください ↩︎
-
公式ドキュメントでは http://www.squid-cache.org/Doc/config/http_port/ に
require-proxy-header
のパラメータの説明があります ↩︎ -
NLB 経由で Squid サーバにアクセスする場合の設定のアクセス制限の方法については https://blog.serverworks.co.jp/tech/2018/04/13/clb-proxyprotocol/ を参考にしました ↩︎
-
ログのフォーマット、出力先については https://blog.mmmcorp.co.jp/blog/2018/02/17/squid_ecs/#outline__4 を参考にしました ↩︎
-
アクセス元情報の隠匿、エラーページでの Squid バージョンの非表示などは https://dev.classmethod.jp/articles/redundant-proxy-servers-using-squid-and-nlb-and-efs/#toc-9 を参考にしました ↩︎
-
https://blog.mmmcorp.co.jp/blog/2018/02/17/squid_ecs/#outline__7 の内容を拝借しました。オプションの説明は https://linux.die.net/man/8/squid を参考にしました ↩︎
Discussion