今後この記事は Qiita 上では編集されません。
変更があれば、私のブログにて更新されます。
https://isso.cc/blog/docker_postgres_open_port_issue
こちらは 「本番環境などでやらかしちゃった人 Advent Calendar 2023」 22 日目の記事になります。
はじめに
Happy Coding!🤶
みなさん年末いかがお過ごしでしょうか。
私は卒論の抄録執筆が終わらないし、今年体調崩しまくってるしで泣きそうです😭
この記事では昔々あるところで起こった「Docker が俺の Postgres を勝手に全世界に公開しやがって色々怒られた話」について述べていきたいと思います。
※所属団体の関係で、技術的な話以外のところの一部で詳しく話せないところはぼかしたり、デタラメなことで置き換えたりしています。ご了承ください。
簡単な自己紹介
私は大学 4 年で、働いているという訳でもなく、
個人やちょっとした団体で Web アプリや API サーバなどを作って、みんなが使えるようにしています。
背景
今回は数年前に起こった、とある団体でのお話です。
こちらの記事は数年前に起こったことを題材にしています。(ここ重要)
サービス構成
そこではオンプレミスに近い環境で、以下のような構成のアプリをデプロイしています。
(小さめの個人開発とかだとよくある構成だと思います)
- RHEL 系 OS (今は 9.x)
- Nginx Proxy を使用してプロキシを組んでいる
- 443/tcp -> 3000/tcp のフロントエンドサーバに
- 8443/tcp -> 8080/tcp のバックエンドサーバに
- Docker によって 3 つのサービスが立ち上げられている
- フロントエンドサーバ (3000/tcp -> 3000/tcp)
- バックエンドサーバ (8080/tcp -> 8080/tcp)
- PostgreSQL (5432/tcp -> 5432/tcp)
ファイアウォール
もちろん外部公開するサーバなので、セキュリティもしっかりとしなければなりません。
私は Linux に搭載されたパケットフィルタリング型のファイアウォール機能 iptables を用いて、22/tcp, 443/tcp, 8443/tcp 以外のリクエストを許可しないような設定をしていました。
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:SUBNET - [0:0]
# lo
-A INPUT -i lo -j ACCEPT
# Drops
-A INPUT -p tcp --tcp-flags ALL NONE -j DROP
-A INPUT -p tcp ! --syn -m state --state NEW -j DROP
-A INPUT -p tcp --tcp-flags ALL ALL -j DROP
# Accept Rules
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 8443 -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
COMMIT
問題発覚
サービスを動かしてしばらくした後、団体のサーバ群を脆弱性診断にかけたっぽいです。
その結果...
ホスト診断結果: レベル High, 早急に対応が必要です
内容:
PostgreSQL に総当たり攻撃を仕掛けることでログインが可能。
攻撃者にデータを書き換えられたり、OS コマンドを実行される危険性あり。
ユーザ名: postgres
パスワード: hogehoge
対策:
必要最低限の IP アドレスのみからアクセスできるように制限してください。
オワタ😇 (認証情報まで当ててくるんや、すごい...)
ってことで、すごく怒られてしまいました...。
これがメールで転送されてきた時、すごく顔が青ざめたことを今でも思い出します...。
原因
原因は 2 つ考えられます。
パスワードが簡単なものになっていた
これは本当に私が悪いです。
振り返ると短めで推測されやすいものになっていたからです。
また、ユーザ名も postgres
になっていました...。
この出来事から、Postgres のユーザ、パスワード、データベース名などは、
以下のような形でランダム生成し、
信頼できるパスワードマネージャーにおいて管理するようになりました。
% openssl rand -base64 12
NjkuJWWwf9MmN5j4
ファイアウォールの設定が変わってた? [本命]
問題はここです。
確かに iptables では 22/tcp, 443/tcp, 8443/tcp ポートしか空いていなかったはず。
Postgres のポートは 5432/tcp で iptables の設定を見ても開いているような感じはしませんでした。
Docker と iptables
原因は Docker でした。
Docker のドキュメントを見るととんでもないことが書いてありました。
デフォルトでは、全ての外部ソース IP アドレスから Docker ホストに接続できます。
な、なんだとっ !!!!!
なんと Docker が俺の Postgres を勝手に全世界に公開しやがっていたらしいです。
それだけでなく、フロントエンドサーバもバックエンドサーバも、
全てプロキシを通じずにアクセスできるようになっていました。
手元の環境で試したら、
確かに Docker のデーモン起動前と起動後で iptables の設定が大きく変わっており、
Docker で建てたコンテナが全て外部から閲覧できるようになっていました。
-
Docker Daemon 起動前
Docker Daemon 起動前の iptables の設定# iptables -L Chain INPUT (policy DROP) target prot opt source destination ACCEPT all -- anywhere anywhere DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/NONE DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN state NEW DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN,RST,PSH,ACK,URG ACCEPT tcp -- anywhere anywhere tcp dpt:ssh ACCEPT tcp -- anywhere anywhere tcp dpt:https ACCEPT tcp -- anywhere anywhere tcp dpt:pcsync-https ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED REJECT all -- anywhere anywhere reject-with icmp-host-prohibited Chain FORWARD (policy DROP) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain SUBNET (0 references) target prot opt source destination
-
Docker Daemon 起動後
Docker Daemon 起動後の iptables の設定# systemctl start docker # iptables -L Chain INPUT (policy DROP) target prot opt source destination ACCEPT all -- anywhere anywhere DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/NONE DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN state NEW DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN,RST,PSH,ACK,URG ACCEPT tcp -- anywhere anywhere tcp dpt:ssh ACCEPT tcp -- anywhere anywhere tcp dpt:https ACCEPT tcp -- anywhere anywhere tcp dpt:pcsync-https ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED REJECT all -- anywhere anywhere reject-with icmp-host-prohibited Chain FORWARD (policy DROP) target prot opt source destination DOCKER-USER all -- anywhere anywhere DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED DOCKER all -- anywhere anywhere ACCEPT all -- anywhere anywhere ACCEPT all -- anywhere anywhere ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED DOCKER all -- anywhere anywhere ACCEPT all -- anywhere anywhere ACCEPT all -- anywhere anywhere Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain DOCKER (2 references) target prot opt source destination Chain DOCKER-ISOLATION-STAGE-1 (1 references) target prot opt source destination DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere RETURN all -- anywhere anywhere Chain DOCKER-ISOLATION-STAGE-2 (2 references) target prot opt source destination DROP all -- anywhere anywhere DROP all -- anywhere anywhere RETURN all -- anywhere anywhere Chain DOCKER-USER (1 references) target prot opt source destination RETURN all -- anywhere anywhere Chain SUBNET (0 references) target prot opt source destination
-
ポートスキャン (イメージ)
ポートスキャン結果 (イメージ)% nmap hogehoge.example.com … PORT STATE SERVICE 22/tcp open ssh 443/tcp open https 3000/tcp open front 5432/tcp open postgresql 8080/tcp open api 8443/tcp open http-proxy …
こんな落とし穴があるなんて...とほほ...🥺
この現象は ufw など他のファイアウォールでも発生するそうです。
(iptables じゃないからって安心してるそこのあなたも注意!)
問題の解決
Docker のドキュメントを見ると、このようなことも書いてありました。
コンテナに対して特定の IP アドレスもしくはネットワークからのみ許可するには、
DOCKER-USER フィルタ チェインの一番上に
無効化ルールを追加します。
たとえば、以下のルールは 192.168.1.1 を除く全ての外部アクセスを制限します。
$ iptables -I DOCKER-USER -i ext_if ! -s 192.168.1.1 -j DROP
どうやら iptables の設定に DOCKER-USER フィルタチェインのルールを追加すれば解決しそうです。
Docker はプライベート IP アドレスのクラス B (172.16.0.0/12) の中で IP を割り振るため、
外からは 172.16.0.0/12 からのアクセスのみを許可するようにして、
それ以外は許可しないようにします。
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:SUBNET - [0:0]
# DOCKER-USER Filter を追加
:DOCKER-USER - [0:0]
# lo
-A INPUT -i lo -j ACCEPT
# Drops
-A INPUT -p tcp --tcp-flags ALL NONE -j DROP
-A INPUT -p tcp ! --syn -m state --state NEW -j DROP
-A INPUT -p tcp --tcp-flags ALL ALL -j DROP
# Accept Rules
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 8443 -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
#
# ここから DOCKER-USER Chain 設定
#
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -s 172.16.0.0/12 -j ACCEPT
-A DOCKER-USER -j REJECT
COMMIT
設定を確認すると、
Docker Daemon 起動後の iptables の設定
と比較して、
設定できているような気がします。
Chain INPUT (policy DROP)
target prot opt source destination
ACCEPT all -- anywhere anywhere
DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/NONE
DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN state NEW
DROP tcp -- anywhere anywhere tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN,RST,PSH,ACK,URG
ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
ACCEPT tcp -- anywhere anywhere tcp dpt:https
ACCEPT tcp -- anywhere anywhere tcp dpt:pcsync-https
ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED
REJECT all -- anywhere anywhere reject-with icmp-host-prohibited
Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain DOCKER (2 references)
target prot opt source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
RETURN all -- anywhere anywhere
Chain DOCKER-ISOLATION-STAGE-2 (2 references)
target prot opt source destination
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
RETURN all -- anywhere anywhere
Chain DOCKER-USER (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
ACCEPT all -- 172.16.0.0/12 anywhere
REJECT all -- anywhere anywhere reject-with icmp-port-unreachable
RETURN all -- anywhere anywhere
Chain SUBNET (0 references)
target prot opt source destination
外部からポートスキャンをすると、22/tcp, 443/tcp, 8443/tcp 以外は見れていないことがわかりました!
% nmap hogehoge.example.com
…
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
8443/tcp open http-proxy
…
まとめ
今回は Docker によって iptables の設定が勝手に変えられ、
隠すべき Postgres が公開されてしまった話を述べてきました!
Docker によって iptables や ufw に穴を開けられる話は、
調べた感じかなり有名な話っぽいですが、
僕と同じことをやらかしてしまっている人は多いのではないかと思います。
公開サーバで Docker を動かしている人は、
今一度 iptables の設定を見直すことをおすすめします。
以上、良いお年を...🎅