ICTSC2018 本戦 問題解説: それはアクセスできないようにしたはずなのに……
本問題は、 iptables の設定を間違えたように見せかけて、運用者が Docker、ないし docker-compose に詳しくなかったためにハマってしまう、というシナリオで出題した問題です。
問題文
あなたの会社では、最近会社のブログを Docker へ移行した。
ブログには WordPress を使っており、DBは MariaDB、そしてその管理用に phpMyAdmin を 8080 番ポートで動作させている。
phpMyAdmin は外部からのアクセスを防ぐため、ローカルホストからのみアクセスを許可するようにしたつもりであったが、どうやら Docker に移行した際にうまく設定が適用されなくなってしまったらしい。
上記の問題を解決してください。
アクセスに必要な情報
IPアドレス: 192.168.20.1
ユーザー: admin
パスワード: bloom-into-docker
- WordPress の管理者ID / パスワード:
admin
/admin
- データベースの ID / パスワード:
wordpress
/wordpress
問題のゴール状態
問題文に示した意図を達成すること
解説
情報として与えられたサーバへログインすると、ホームディレクトリに docker-compose.yml が置かれており、問題文で説明されていたアプリケーションが Docker のコンテナオーケストレーションを行う docker-compose
で設定されていたことが分かります。
また、ブラウザから 192.168.20.1 にアクセスすると WordPress で構築されたブログが表示され、8080番ポートへアクセスすると phpMyAdmin が表示されるようになっていました。
Docker の動作中コンテナを確認するコマンドである docker ps
を叩くと以下のような結果が得られます。 (コンテナIDは異なる場合があります)
# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3424d4d54bae wordpress:5.0.3-php7.1-apache "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:80->80/tcp wordpress f195b424d1cf phpmyadmin/phpmyadmin:4.8 "/run.sh supervisord…" 5 minutes ago Up 5 minutes 9000/tcp, 0.0.0.0:8080->80/tcp phpmyadmin b9c12504b150 mariadb:10.2.22 "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:3306->3306/tcp database
この問題のゴール状態は「問題文に示した意図を達成すること」とありますが、意図とはなんでしょうか? それは問題文に示されています。
問題文には「外部からのアクセスを防ぐため、ローカルホストからのみアクセスを許可するようにしたつもりであったが」と書かれています。
意図をエスパーして iptables -L
で iptables のルールを確認してみると、それらしいルールが登録されていることが分かります。 (dpt:http-alt
に関連する2行がそうです)
# iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination ACCEPT tcp -- anywhere anywhere tcp dpt:http ACCEPT tcp -- localhost anywhere tcp dpt:http-alt DROP tcp -- anywhere anywhere tcp dpt:http-alt ACCEPT tcp -- 10.0.0.0/8 anywhere tcp dpt:ssh ACCEPT tcp -- 172.16.0.0/12 anywhere tcp dpt:ssh ACCEPT tcp -- 192.168.0.0/16 anywhere tcp dpt:ssh DROP tcp -- anywhere anywhere tcp dpt:ssh
つまり、TCPの8080ポートに対するアクセスは、送信元がlocalhostからのものを許可し、それ以外は拒否するようになっています。
「どうやら Docker に移行した際にうまく設定が適用されなくなってしまった」と問題文にあるとおり、おそらく普通に Apache や nginx のような HTTPサーバをローカルで実行していればこのルールは正しく適用されます。
このルールが意図の通りに動作しないのは Docker 上のコンテナで HTTPサーバが動いていることによるものです。
詳しい説明は参考で示したリンク先を参照して頂きたいのですが、 Docker でコンテナのポートを外部に公開した場合、外部からのアクセスは iptables のDOCKER
チェインへジャンプするルールがPREROUTING
チェインに追加されます。
そのため、上記のINPUT
チェインに追加された 8080ポート (http-alt) に対するルールは処理されません。
さて、問題の背景が分かったところでこの問題に対処する必要がありますが、実は本問題の解答にあたり、 iptables と格闘する必要はありません。この問題のジャンルが軽量コンテナであることもヒントになっています。
そもそもなぜ外部から TCPの8080ポートへアクセスできるのかといえば、それは外部のインタフェースからのアクセスを受け付けているからに他なりません。
docker ps
の結果における PORTS
の列に表示されているように、 0.0.0.0:8080->80/tcp
というのは、ホストが全てのIPからTCPの8080ポートでアクセスを受け付けることを意味しています。
これを拒否するようにするためには、コンテナが受け付けるIPアドレスを制限すれば良いのです。
docker-compose のコンフィグファイルのドキュメントを参照すれば、 docker-compose.yml
の "8080:80"
を "127.0.0.1:8080:80"
に変更することで問題が解決できることが分かります。
反映は docker-compose up -d
で可能ですが、とりあえずホストを再起動しても反映されます。困ったら再起動しましょう。
また、WordPressやデータベースの認証情報はただの罠です。特に意味はありません。
別解
これは意表を突かれたのですが、 phpMyAdmin の Docker イメージは内部で nginx を使っているようで、その nginx のコンフィグを書き換えることで対処してきたチームが1チームいました。
Docker のコンテナのライフタイムや設定の柔軟性を考えるとあまり望ましくはありませんが、大会における条件は満たしていたため満点としました。
また、解決できることは分かっていましたが望ましくない解答として、 iptables と格闘することによる解答方法があります。このときは、以下のDNATのルールに着目する必要があります。
iptables -L DOCKER -t nat Chain DOCKER (2 references) target prot opt source destination RETURN all -- anywhere anywhere RETURN all -- anywhere anywhere DNAT tcp -- anywhere anywhere tcp dpt:mysql to:172.18.0.2:3306 DNAT tcp -- anywhere anywhere tcp dpt:http-alt to:172.18.0.3:80 DNAT tcp -- anywhere anywhere tcp dpt:http to:172.18.0.4:80
内部的には、このルールが Docker 内のコンテナへの NAT を実現しているため、このルールが適用される送信元IPをローカルホストへ絞るようにすることで本来望んでいた動作を得ることができます。
具体的な操作は述べませんが、上記の設定を修正した上で、これを永続化するようにすることができれば目的を達成できます。
しかし、 Dockerチェインのiptables のルールの操作は Docker が責任を追っており、これをユーザが弄ることは望ましい操作ではありません。事実、このルールを変更しても再起動後にはDockerのデーモンによりルールが上書きされてしまいます。
大会中の解答では dockerd が iptables を操作しないよう修正し、なんとか再起動後もルールが維持されるようにしたチームが2チームいました。
しかし、これは他のコンテナ等が存在する場合には管理が大変煩雑になるためあまり望ましくありません。
本来であればこのような解答を行うことが望ましくないと分かるよう誘導するべきであり、これは作問を行った私のミスだと考えています。
講評
採点基準として、解法の良し悪しに関係なく、ホストの再起動後も期待した動作を維持したチームには満点の200点、再起動後は期待した動作を維持できなかったチームには100点を与えました。
本戦においては、12チームが想定解法で満点となりました。また、2チームが別解により満点、1チームが別解により100点となりました。
本問題は想定していたよりも正答率がかなり高く、最近の学生は Docker が使えるのだな、と個人的には少し感動しました。
まだまだ Docker や Kubernetes は登場したてではありますが、アンケート等の結果も見ると興味を持って触っている方が多いように思いました。
この講評を書きながら、そういえば私は ICTSC5において FreeBSD における軽量コンテナシステムである Jail の問題を出題したことを思い出しました。Dockerがここまで流行るとは思ってなかった……
皆さんも FreeBSD、そしてJail を使ってみてください。
参考
パブリックIPを持つサーバでDockerを起動するとportが全開放される問題の対処法 – grep Tips *
記事中の問題とは関係がありませんが、 Docker が iptables へ設定するルール等が詳しく説明されています。
また、出題した環境で用いられていた docker-compose.yml
および永続化されていた iptables コンフィグ (/etc/iptables/rules.v4
) はこちらです。 https://gist.github.com/kyontan/d96a6afbaae615b1b156844a1be261a1
問題環境を再現するためには Ubuntu Server 18.04 において netfilter-persistent
および iptables-persistent
を導入し、上記の iptables のルールを読み込んだ上で docker-compose up -d
でコンテナを起動してもらえればと思います。