docker run --rm -it centos curl https://example.com
を弾きたい。それだけなら--net=none
で簡単なのだけれど、
docker run --rm -it -p 8080:80 nginx
で外部からのアクセスは受け付けるようにしたい。
一時的に弾く。
sudo iptables -F DOCKER-USER
sudo iptables -A DOCKER-USER -m state --state RELATED,ESTABLISHED -j RETURN
sudo iptables -A DOCKER-USER -s 172.17.0.0/12 -j REJECT
sudo iptables -A DOCKER-USER -j RETURN
元に戻す。
sudo iptables -F DOCKER-USER
sudo iptables -A DOCKER-USER -j RETURN
永続化する。
sudo systemctl enable nftables
で、
[Service]
ExecStartPost=iptables -F DOCKER-USER
ExecStartPost=iptables -A DOCKER-USER -m state --state RELATED,ESTABLISHED -j RETURN
ExecStartPost=iptables -A DOCKER-USER -s 172.17.0.0/12 -j REJECT
ExecStartPost=iptables -A DOCKER-USER -j RETURN
を書き込む。
iptablesで何かするときは、ミスるとその瞬間にSSHも繋がらなくなるので、ネットワーク以外のバックアップ手段を確認しておくべき。
詳細
ホストはCentOS Stream 8。CentOS 8はサポートが1年になってしまったので、おとなしくStreamを使います。
$ cat /etc/os-release
NAME="CentOS Stream"
VERSION="8"
:
Dockerは20.10.3。
$ docker -v
Docker version 20.10.3, build 48d30b5
なぜこんなことをしたいのかというと、CTFの問題サーバーを立てるためである。当然外から接続を受け付ける必要がある。攻撃に成功した人は(たいていは)任意のコードが実行できるようになるので、それでどこかの掲示板に犯罪予告を投稿するとかされるのを避けたい。
まあ、どこかのCTFでconnect back shellを使った覚えがあるし、webのXSSに関する問題だと自分のサーバーにリクエストを飛ばすこともあるし、そこまで気にする必要は無く、記録を残しておいて何かがあったときに「犯人はこいつです」と言えれば良いのかもしれないが。
外向きのアクセスだけを弾く
単にIPレベルで外向きのものを弾くと、内向きのリクエストに対するレスポンスも弾いてしまう。RELATED,ESTABLISHED
な外向きのパケットは通し、それ以外の外向きのパケットを弾けば良い。
コンテナからcurlとか全部消しておけば?
bashがあればTCPアクセスはできてしまう。
$ exec 3<>/dev/tcp/example.com/80; printf "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" >&3; cat <&3
HTTP/1.1 200 OK
Accept-Ranges: bytes
:
そもそも、任意のコードが実行できるなら、TCPアクセスするようなコードも動かせる。
コンテナの中で何とかする
コンテナ内のiptablesでそういう設定をしたり、あるいは攻撃対象のプロセスをseccompで制限したりという手はあるかもしれない。でも、コンテナ内で複雑なことはしたくないし、コンテナ内のrootが取られたときにもアクセスを防ぎたい。最近もsudoに権限昇格の脆弱性があった。
Dockerのネットワークで何とかする
-p 8080:80
で指定したポートだけ不思議な力で繋がってくれれば良かったのだけど、-net=none -p 8080:80
は繋がらない。docker network create --internal internal_net
して、-net=internal_net -p 8080:80
もダメ。
--internal
なネットワークAと、外に繋がるネットワークBを作り、AとB両方に繋がるコンテナにプロキシさせるという手はあるかもしれない。コンテナ立ち上げ時に複数のネットワークを指定することはできないものの、後付けはできるらしい。
docker - Start container with multiple network interfaces - Stack Overflow
ホストの外のファイアウォールで防ぐ
これが一番安心。でも、使っている環境にそういうファイアウォールがあるかという話があり、別にファイアウォール代わりのマシンを借りるとお金が掛かってしまう。また、ホストマシンからもインターネットアクセスができなくなってしまうのは不便。
ホストのiptables
ということでホストのiptablesで防ぐ。
Dockerもiptablesを使って色々やっているが、ユーザーが処理を挟めるように、DOCKER-USER
が用意されている。
Docker and iptables | Docker Documentation
sudo iptables -F DOCKER-USER
sudo iptables -A DOCKER-USER -m state --state RELATED,ESTABLISHED -j RETURN
sudo iptables -A DOCKER-USER -s 172.17.0.0/12 -j REJECT
sudo iptables -A DOCKER-USER -j RETURN
-
-F DOCKER-USER
- とりあえず中身を全部消す
-
-A DOCKER-USER -m state --state RELATED,ESTABLISHED -j RETURN
- 確立済みTCPセッションならば処理をDockerに戻す
-
-A DOCKER-USER -s 172.17.0.0/12 -j REJECT
- それ以外で送信元がDocker内部のパケットは破棄
- サブネットを変えれば、特定のDockerネットワークは弾き、他のものは通すこともできそう
- それ以外で送信元がDocker内部のパケットは破棄
-
-A DOCKER-USER -j RETURN
- それ以外は処理をDockerに戻す
コマンドの実行順序がそのままフィルタの適用順序になるので、順番が重要。
永続化
iptablesの設定は再起動すると消えてしまう。
iptablesと言っているけれど、CentOS 8ではnftablesに換わっている。iptablesコマンドで設定ができているのは互換インタフェースが提供されているかららしい。正攻法で永続化するには、/etc/sysconfig/nftables.confに
table ip filter {
chain DOCKER-USER {
ct state related,established return
ip saddr 172.16.0.0/12 counter reject
return
}
}
を書き込み、systemctl enable --now nftables
でnftablesを有効化する(パケットのフィルタリングにnftablesを使っていることと、nftablesデーモンが立ち上がっていることはイコールではない……?)。
しかし、これが良いのかが分からない。Dockerはnftablesの設定を永続化しないので、systemctl reload nftables
とかでDockerが設定した分が全部飛んで繋がらなくなる。
設定をnftablesにさせるのではなく、Dockerの起動直後に設定をするほうが良いのかもしれない。sudo systemctl edit docker
をして、
[Service]
ExecStartPost=iptables -F DOCKER-USER
ExecStartPost=iptables -A DOCKER-USER -m state --state RELATED,ESTABLISHED -j RETURN
ExecStartPost=iptables -A DOCKER-USER -s 172.17.0.0/12 -j REJECT
ExecStartPost=iptables -A DOCKER-USER -j RETURN
を書き込んでおけば起動直後に設定が有効になる。これならば、nftablesからまた別の何かに換わってもiptablesの互換機能さえ提供してくれていればそのまま動くはず。