🐘

ログは「見せない・渡さない」!Docker環境のDB権限分離と通信制御の鉄則!

に公開

シリーズ一覧

  1. 全体構成編:元インフラ屋がWeb開発初挑戦!構成図で見るセキュアなチャット基盤の全貌!
  2. ホストOS防衛編:Dockerコンテナからホストを守れ!元インフラ屋が施したSSHとFWの鉄壁設定!
  3. DB権限分離とネットワーク隔離編:本記事
  4. 以降執筆中...

はじめに

こんにちは。私は、Web開発初挑戦で「yonimal」というチャットサービスを作りました。
https://yonimal.com

まじの初めてです。HTMLは<a>タグや<br>タグ等、基礎しか知りません。
flask/pythonで開発しましたが、pythonも初めてというか開発業務自体したことがありません。
本サービスは、Dockerでアプリを動かしていますが、なんとDockerも初めてです。

前回の記事では、「ホストOSとコンテナの境界線」を守りましたが、今回はコンテナ同士がどうデータを扱い、どう外と繋がるかという、より内側の設計についてです。

本サービスには、表側の「チャットアプリ(App)」の他に、私が管理・運用するための「管理パネル(Admin)」という別のコンテナが存在します。
Adminコンテナの別に立てている目的は、チャットログ、問い合わせログ、アクセスログといった流出したらまずい機密情報を安全に閲覧することです。

今回は、これらの「データ」を守るために施した、DB権限の分離と外部通信(Egress)制御のこだわりをお話しします。

使用している技術スタックや全体構成図については、第1回の記事をご確認ください。


【DB権限の分離】Appは「書くだけ」、Adminは「読める」

最前線で攻撃に晒されるAppコンテナ。
万が一、乗っ取られても、DB内のデータを根こそぎ盗られることは絶対に回避したいです。
そこで私は、PostgreSQLのユーザー権限をコンテナごとに厳格に分けました。

DBの権限
DBの権限
※全体構成図はこちら

  • Appコンテナ(INSERTのみ): ユーザーがアクセスするサービスコンテナ。現在のチャットや問い合わせをDBに「書き込む」ことはできますが、過去のデータを「見る(SELECT)」ことは一切できません。
  • Adminコンテナ(全権限): 管理者である私だけがアクセスできるコンテナ。ログの調査やメンテナンスのために「見る(SELECT)」ことができます。

これにより、AppコンテナにSQLインジェクション等の脆弱性があっても、物理的に過去のログを盗み出すことが不可能な構造にしています。

このSELECT権限の有無は、機能実装にも影響します。
例えば『入室通知』を送るにはDBから通知先リストを読み出す必要がありますが、Appコンテナにはその権限がありません。
そのため、通知処理はSELECT権限を持つAdminコンテナが代行する仕組みにしています。


【通信の分離】コンテナ間の通信を制御

各コンテナ間で自由に通信ができると、万が一、一部のコンテナが乗っ取られた際に、より強い権限を持つコンテナへ攻撃がされてしまいます。
それを回避するために、通信の制御も厳密にしています。

コンテナNW
コンテナNW
※全体構成図はこちら

各コンテナごとに、NW層を分離しています。
NW層をまたぐ通信は、色付きの実線の矢印で接続されているコンテナのみアクセス可能となっています。
その他のコンテナには一切アクセスできません。

これにより、例えば、Appコンテナが乗っ取られても、Adminコンテナへの通信は不可能です。

この方針に則り、yonimalのDocker環境のNWリストは以下のように細分化しています。
各NWに必要最低限のコンテナを追加することにより、セキュリティを確保しています。
2列目のInternalについては、次章で触れます。

$ docker network inspect $(docker network ls -q) --format '{{printf "%-30s" .Name}} | Internal: {{printf "%-5t" .Internal}} | Containers: {{range .Containers}}{{.Name}} 
{{end}}'
bridge                         | Internal: false | Containers: 
host                           | Internal: false | Containers: 
none                           | Internal: false | Containers: autoheal 
yonimal_network_admin_db       | Internal: true  | Containers: yonimal-admin-1 yonimal-db-1 
yonimal_network_admin_proxy    | Internal: true  | Containers: yonimal-proxy-1 yonimal-admin-1 
yonimal_network_admin_redis    | Internal: true  | Containers: yonimal-admin-1 yonimal-redis-1 
yonimal_network_admin_web      | Internal: true  | Containers: yonimal-admin-1 cloudflared 
yonimal_network_app_db         | Internal: true  | Containers: yonimal-app_a-1 yonimal-app_b-1 yonimal-db-1 
yonimal_network_app_proxy      | Internal: true  | Containers: yonimal-proxy-1 yonimal-app_a-1 yonimal-app_b-1 
yonimal_network_app_redis      | Internal: true  | Containers: yonimal-app_a-1 yonimal-app_b-1 yonimal-redis-1 
yonimal_network_app_web        | Internal: true  | Containers: yonimal-app_a-1 yonimal-app_b-1 yonimal-nginx-1 
yonimal_network_nginx          | Internal: true  | Containers: cloudflared yonimal-nginx-1 
yonimal_network_proxy_public   | Internal: false | Containers: yonimal-proxy-1 
yonimal_network_tunnel         | Internal: false | Containers: cloudflared 

【出口の防衛】コンテナから外部への通信抑制およびTinyproxyによるホワイトリスト

コンテナから外への通信も、放置できません。
勝手な通信を許せば、乗っ取られた後にデータを外部のサーバーに置かれてしまうかもしれません。
私のサーバーが攻撃サーバーとなり、外部へ攻撃するかもしれません。

これを防ぐために、外部に直接通信できるコンテナを制限しています。
それが、前章のInternalであり、Internal: trueに設定した場合、外部に直接出られません。
原則、Internal: trueでコンテナを囲い込む方針ですが、2つ例外があります。

1つ目は、cloudflaredコンテナです。
外部のcloudflareのサーバーとトンネルを張るためにInternal: falseとなっています。
※この部分につきましては、また別の記事に書きます。

2つ目は、Tinyproxyコンテナです。
外部接続用のプロキシとなるためにInternal: falseとなっています。

AppコンテナとAdminコンテナは、Turnstile認証、Discordへの通知、WebPush、LINE通知で外部への通信が必要です。
これらの通信については、Tinyproxyのホワイトリストの機能を使用して、必要な接続先のみに限定することでセキュリティを確保しています。

外部通信
外部通信
※全体構成図はこちら


おわりに

これで、今回の記事は以上です。まとめると以下の通りです。

  • 外部ユーザーからの通信を受けるアプリコンテナは、DB権限を最小限にする。
  • 各コンテナ間の通信経路は、必要なもののみに限定する。
  • コンテナから外部への通信は原則禁止し、必要な場合はプロキシで接続先を限定する。

なぜ「yonimal」を作ろうと思ったか、開発の裏側をもっと知りたい!と思ってくださった方は、ぜひnoteの方も覗いてみてください。
https://note.com/yonimal_dev/n/n532757b2358f

Discussion