こんにちは、インフラチームテックリードの櫻井です。
今回はDockerとfirewalldを使って内部ネットワークへのアクセスを制限し、SSRF攻撃を防ぐ方法について紹介します。
SSRF攻撃とは
SSRF(Server Side Request Forgery)攻撃はWebアプリケーションに対する攻撃の一種で、公開されたサーバーを経由して公開されていない内部ネットワークのサーバーにアクセスする手法です。

具体例
例えば以下のように外部から指定されたURLにcurlでリクエストを行い、その結果を出力するプログラムがあるとします(このプログラムにはXSS脆弱性も含まれていますが今回は割愛します)。
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'http://' . $_REQUEST['url'],
CURLOPT_RETURNTRANSFER => true
]);
echo curl_exec($ch);
このプログラムは何も対策がされていないため example.com のようなドメインだけではなく、 192.168.1.10
のようなプライベートIPにもリクエストを送信することができてしまいます。
例えばEC2インスタンスには 169.254.169.254
にリンクローカルアドレスが割り当てられており、先ほどのプログラムを通じてこのIPアドレスにリクエストを送ることでインスタンスのメタデータや一時的な認証情報などを盗むことができてしまいます。
実際はここまで分かりやすいパターンはあまりないと思いますが、OSSが内部ネットワークにも通信できることを知らずに使ってしまっていたというパターンは十分ありうると思います。
対策パターン
このようなSSRF攻撃を防ぐための対策には大きく分けて3種類あります。
- 外部からの入力をリクエストのURLに使用しない
- 正規表現などを使ってリクエスト送信先をバリデーションする
- ファイアウォールを使って内部ネットワークへのアウトバウンド通信を制限する
1. 外部からの入力をリクエストのURLに使用しない
この方法は外部からの入力をリクエストのURLに使用しないように、そもそも仕様を考え直したり機能を削除するという方法です。
SSRFの対策は難しいことが多いので、可能であればこのようにするのが一番安全かつシンプルな対策です。
しかしアプリケーションの要件によってはそのようにできない場合もあるため、その場合は2または3の対策を行う必要があります。
2. 正規表現などを使ってリクエスト送信先をバリデーションする
この方法は正規表現などのパターンマッチングを使って外部からの入力をチェックし、内部ネットワークへのリクエストをブロックするという対策です。
この方法は導入は比較的容易である反面、漏れ無く正確にパターンを記述するのは難しく、正規表現が複雑になるとデバッグやメンテナンスも難しくなります。
また自分で正規表現を作成せずWAFのマネージドルールを使って外部からの入力値をチェックするという方法もあります。
3. ファイアウォールを使って内部ネットワークへのアウトバウンド通信を制限する
この方法はLinuxのfirewalldやAWSのセキュリティグループなどの機能を利用して、指定した範囲のIPアドレスへのアウトバウンド通信を制限する方法です。
この方法はポートやCIDRを指定して柔軟かつ分かりやすく記述することができますが、サーバー単位で設定が適用されてしまうため、特定の機能でのみ内部ネットワークへの通信を制限するということができません。
しかしDocker上でその機能を動かし、Dockerの仮想ネットワークに対してファイアウォールの設定を適用することで、特定の機能のみ内部ネットワークへのアウトバウンド通信を制限することができるようになります。
また、Docker上で動かすことでファイルシステムへのアクセスも必要最小限に制限することができ、ディレクトリトラバーサル脆弱性の対策にもなります。
今回はこのDockerコンテナからのアウトバウンド通信をfirewalldで制限する方法について紹介したいと思います。
Dockerとfirewalldの設定手順
次に実際にDockerとfirewalldの具体的な設定手順について説明します。
今回のホストサーバーはAlmaLinux8+Apache+PHPの構成ですが、Nginxや他のプログラミング言語を使った構成にも応用できると思います。
Dockerとfirewalldをインストールする
まず初めにDockerとfirewalldをインストールする必要があります。
OSによってインストール方法は異なりますが、例えばRHEL8系のOSなら以下のコマンドでインストールできます。
$ sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
$ sudo dnf install docker-ce firewalld
Dockerのiptablesを無効化する
Dockerはデフォルトでiptablesを上書きする機能があるため、これを無効化します。
具体的には /etc/docker/daemon.json
に以下の設定を追加します。
{
"iptables": false
}
Dockerとfirewalldを起動する
次にインストールしたDockerとfirewalldを起動し、自動起動設定もしておきます。
$ sudo systemctl start docker
$ sudo systemctl enable docker
$ sudo systemctl start firewalld
$ sudo systemctl enable firewalld
firewalldのインバウンド通信を許可する
次に起動したfirewalldに対してインバウンド通信の許可設定を追加します。
今回はTCP通信のための80番ポートを許可します。
$ sudo firewall-cmd --permanent --add-port 80/tcp
firewalldのIPマスカレードを設定する
次にDockerからインターネットにアクセスするためにfirewalldにIPマスカレードを設定します。
$ sudo firewall-cmd --permanent --add-masquerade
Dockerコンテナからインターネットへ通信するためのルールを設定する
次にDockerコンテナのネットワーク からインターネットへ通信するためのルールを設定します。
Dockerはデフォルトだと docker0
インターフェースの bridge
ネットワークを経由して外部と通信するため、これに対してfirewalldのルールを設定します。
bridge
ネットワークのCIDRは以下のコマンドから 172.17.0.0/16
であることが確認できます。
$ sudo docker network inspect bridge
[
{
"Name": "bridge",
...
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
...
}
]
以下のコマンドで 172.17.0.0/16
からインターネットへの通信を行うためのNATルールを設定します。
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 1 ! -o docker0 -s 172.17.0.0/16 -j MASQUERADE
DockerコンテナからプライベートIPへの通信を拒否する
次にDockerコンテナが経由するdocker0インターフェースからプライベートIPへの通信を拒否するルールを設定します。
プライベートIPの一覧は
にあるものを使用しました。
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 0.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 10.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 100.64.0.0/10 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 127.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 169.254.0.0/16 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 172.16.0.0/12 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.0.0.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.0.2.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.88.99.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.168.0.0/16 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 198.18.0.0/15 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 198.51.100.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 203.0.113.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 224.0.0.0/4 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 240.0.0.0/4 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 255.255.255.255/32 -j REJECT
firewalldをリロードして設定を反映する
次にここまで設定してきたfirewalldの設定を反映します。
$ sudo firewall-cmd --reload
apacheユーザーをdockerグループに追加する
今回はapacheユーザーで実行されているPHP経由でDockerを呼び出すため、apacheユーザーをdockerグループに追加します。
追加した後はApacheを再起動して変更を反映する必要があります。
$ sudo gpasswd -a apache docker
$ sudo systemctl restart httpd
Dockerコンテナ経由で実行する形にプログラムを変更する
最後に対象のプログラムをDockerから実行する形に変更します。
使用するDockerイメージは軽量で使いやすいものなら何でも良いですが、今回はcurlがインストールされている curlimages/curl を使用してみました。
また今回のようにシェルのコマンドの一部に外部からの入力を使う場合は、OSコマンドインジェクションを防ぐために必ず escapeshellarg() を使ってエスケープしておく必要があります。
OSコマンドインジェクションの対策としてはこのように escapeshellarg() でエスケープする方法の他にも proc_open() を使って標準入力から渡す方法や、PHP7.4以降なら proc_open() の command パラメータにコマンドの引数を含めたarrayを渡す方法などがあります。
<?php
$command = "docker run --rm curlimages/curl curl -s http://" . escapeshellarg($_REQUEST['url']);
passthru($command);
firewalldとDockerの設定を完了した状態でこのように変更したプログラムに ?url=169.254.169.254
をつけてアクセスしてみると、具体例 のときのプログラムとは違ってプライベートIPへのリクエストが拒否されていることが分かると思います。
まとめ
今回はDocker+firewalldを使ってプライベートIPへのアクセスを制限し、SSRF攻撃を防ぐ方法について紹介しました。
SSRF攻撃は他の脆弱性に比べて完全な対策が難しいことが多いですが、今回紹介した方法も一つの手段として参考にしていただけると幸いです。
参考
