DanLevy.net

セルフホスティング向けDockerセキュリティの必須ヒント

自社ホストサービスを安全に、防御から監視まで!

目次

🧗‍♀️ 勇敢な方へ

Dockerサービスをセルフホスティングする場合、セキュリティは雲の上の誰かに任せられない。ポートスキャンや設定ミスからあなたを守ってくれるクラウドプロバイダーは存在しない。自宅ネットワークでアプリを立ち上げる場合も、VultrやDigitalOcean、Linode、AWS、Azure、Google CloudなどのVPSを借りる場合も、すべてのセキュリティ対策を自分で固め、それが正しく機能しているかを検証する責任がある。

本ガイドではDockerセキュリティの要点を掘り下げていく。あまり知られていないテクニックから実装が難しい手法まで網羅する。カンアリトークン、リードオンリーボリューム、ファイアウォールルール、ネットワークセグメンテーションと強化、認証プロキシの追加など、実践的な対策を解説する。

このガイドでは、家庭用ネットワークとパブリッククラウド環境の比較も行い、Nginxで基本的な認証プロキシを構成する方法を示します。最後まで読み進めることで、不正アクセスを試みる者(友人や家族、場合によっては自分自身さえも)を寄せ付けないオプションをいくつか手に入れるでしょう。

相当な内容量ですが、多くの項目が関連していますので、自分の環境に最も関連性の高いものを選んで実装してください。🍀

🔄 :latestタグの落とし穴

イメージの更新を維持することはセキュリティにおいて不可欠です。ただし、:latestに依存すると、レビューなしに破壊的な変更や脆弱なビルドが導入されるリスクがあります。

安全な更新方法

pullbuildコマンドと更新コマンドを組み合わせ、意図的にイメージをリフレッシュした後、破損に気づける期間に再起動するようにしてください。

update-and-run.sh
#!/bin/bash
docker compose pull && \
docker compose up -d

バージョンピンニング vs latest

適切なバージョンを固定する選択は安定性とセキュリティのバランスを取る行為です。以下は一般的な戦略です:

docker-compose.yml
# ...
# 精確なバージョンピンニング。重要なサービス向け
image: postgres:17.2
# パッチバージョンピンニング。非重要なサービス向け
image: postgres:17.2
# メジャーバージョンピンニング。趣味プロジェクト向け
image: postgres:17
# 避けたい最優先タグ
image: postgres:latest

DependabotRenovate を使ってレビュー可能な更新PRを生成してください。深夜に再構築したくないようなサービスは特定バージョンまたはダイジェストを固定し、自動化ツールが更新を通知するようにしてください。

定期的にDockerイメージを更新するためのお気に入りツールがあれば教えてください!

🔐 シークレット管理

シークレットの管理方法は多様ですが、守るべき最も重要なルールの1つは 「Dockerイメージにシークレットをハードコードしたり、gitにコミットしたりしないこと」 です。これは最も一般的なセキュリティミスであり、長期的なリスクを伴い、修正も面倒です。

シークレットの安全な保存は、.envファイルやDocker secrets1Password/BitwardenHashiCorp VaultやAWS Secrets Managerのようなシークレットマネージャーなど、多くのオプションがあります。使用ケースに応じて、適切な「労力とセキュリティのバランス」を選びましょう。

例: Docker Compose

以下は、appサービスを127.0.0.1:8080にバインドし、両コンテナをbackendカスタムネットワークに接続する例です。

docker-compose.yml
networks:
backend:
services:
app:
networks:
- backend
ports:
# 可能であればローカルホストにバインド
- "127.0.0.1:8080:8080"
# ... 他の設定
database:
image: postgres:17.1
# ポートは不要;backendネットワーク内からアクセス可能
networks:
- backend

ネットワークのベストプラクティス

🛡️ アクセス制御

Dockerサービスを保護するためには、コンテナの権限制限やDockerソケットへのアクセス制限など、アクセス制御が不可欠です。

コンテナの権限制限

もう一つのアクセス制御の実践は、コンテナの権限を制限することです。これにより、権限昇格やトラフィックハイジャックなどの脅威の影響範囲を縮小できます。これは万能ではありませんが、多くのコンテナが本来必要なかった権限を削除します。

権限とは何ですか? Linuxカーネルで定義された名前付きの権限や能力です。( capabilities マニュアルページにフルリストがあります。) 例としてCAP_CHOWN(ファイル所有権の変更)、CAP_NET_ADMIN(ネットワークインターフェースの設定)、CAP_KILL(任意のプロセスの終了)などがあります。

必要な権限を特定する2つの方法は:

  1. 試行錯誤 この遅いが効果的な方法では、最初にすべての権限を削除し、アプリが動作するまで1つずつ追加します。
  2. 過去の作業を検索project-name cap_drop Dockerfile”や”project-name cap_drop docker-compose.yml”を検索し、他者が既に作業を終えているか確認します。LLMが起点を提案できますが、コンテナをテストし、イメージドキュメントを読むまで推測と扱ってください。

権限のベストプラクティス

例: 権限の削除・制限
services:
database:
image: postgres:17.1
networks: [ db-network ]
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
- SETGID
- SETUID
db-admin:
image: dpage/pgadmin4:4.1
networks: [ db-network ]
ports:
- "8081:80"
# ... 他の設定
networks:
db-network:

これでサービスはdb-networkネットワークを通じて相互に通信できます。Docker Composeはそのネットワークを自動的に作成します。

--external/external:オプションを使用して事前に作成されたネットワークに参加します。省略すると新しいネットワークが作成されます。

Dockerソケットのアクセス

⚠️ 警告: docker.sockは基本的にホストの管理者権限に相当します

⚠️ :roオプションはソケット経由で送信されるI/Oには影響しません!

これは単にソケットパス自体を読み取り専用でマウントすることを保証するだけであり、そのソケットを通じて送信されるAPI呼び出しは依然としてコンテナを作成したりホストパスをマウントしたり、おそらく意図せずに委任された他の非常に興味深い操作を行うことができるままです。

ソケットのベストプラクティス

国のブロック!

時折役立つかもしれませんが、実際のセキュリティ境界ではありません。

ここでは音楽ではなく、地政学的実体について話しています…

自宅や友人向けにアプリをホスティングしている場合、予期しない国からのトラフィックをブロックしたり、予期される国からのトラフィックのみを許可したりできます。これはノイズを減らしますが、VPNやプロキシ、ボットネット、忍耐力のある攻撃者を止めることはできません。

中国からのすべてのトラフィックをブロックするためのこのスクリプトを確認してください:

block-china.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \
while read line; do ufw deny from $line to any; done
block-china.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \
while read line; do ufw deny from $line to any; done

同様に、米国からのトラフィックのみを許可することもできます:

allow-usa.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/us.zone | \
while read line; do ufw allow from $line to any; done

CloudFlareプロキシホストのセキュリティ強化

自宅サーバーがCloudFlare IP(プロキシ)で保護されている場合、アクセスをCloudFlare IPとローカルネットワークのみに制限できます。

これは先ほどの国別ブロッキングと似ていますが、はるかに細かい制御が可能です。

whitelist-ingress-from-cloudflare.sh
ufw default deny incoming # 入力トラフィックをすべてブロック!!!
ufw default allow outgoing # 出力トラフィックをすべて許可
ufw allow ssh # SSHを許可
# ホストされたサービス用に専用のDMZ/VLANを設定したローカルサブネットへのアクセスを許可
ufw allow from 10.0.0.0/8 to any port 443

CloudFlare IPの許可

curl -fsSL https://www.cloudflare.com/ips-v4 |
while read line; do ufw allow from $line to any port 443; done

IPv6サポートの追加

curl -fsSL https://www.cloudflare.com/ips-v6 | \

while read line; do ufw allow from $line to any port 443; done

地理ベースの変更をテストするには、目的の国に位置する場所のVPNが役立ちます。詳しくは[監視と検証](#-monitoring--verification)セクションを参照してください。
### アプリケーション層セキュリティ
[network and host are security hardened,](#-network-hazard) が完了した後でも、さらにやるべきことがあるかもしれません。
今度は、サービス自体の「アプリケーション」層について考えなければなりません。

そのデータベースは有効なパスワードを持っていますか?このコンテナはHTTPS/証明書の自動化を実行していますか?アプリケーションには組み込みの認証機能がありますか?どのメールアドレスがサインアップできるかに制限がありますか?デフォルトの資格情報や変更可能な環境変数がありますか?

実際に_把握_する唯一の方法は確認することです。このケースではまずREADMEdocker-compose.ymlDockerfile.env.*などの主要なファイルから始めましょう。プロジェクト内だけでなく、サポートサービス(例: Postgres、Redisなど)についても同様に確認するのが理想です。

リバースプロキシ

もう1つの防御レイヤーは基本認証です。HTTPSなしで使用しないでください。レガシーサービスの場合、管理者ルートの前に基本認証を設定するだけで、ランダムなリクエストや認証なしのクローラーによる直接アクセスを防ぐことがよくあります。

/etc/nginx/conf.d/secure-admin.conf
location /admin {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://internal_admin:80;
proxy_set_header X-Real-IP $remote_addr;
}

資格情報を生成するには:

Terminal window
htpasswd -c /etc/nginx/.htpasswd admin

基本認証プロキシを使うと、内部サービスに到達する前に攻撃者がユーザー名とパスワードという追加の障害に直面します。

もう1つのオプションとして、TraefikCaddyといったHTTPSと基本認証を自動化できるサービスを利用する方法もあります。

多くのドメインやサービスをGUIで管理したい場合は、Nginx Proxy Managerの利用をおすすめします。

🔍 監視と検証

これは最も重要でありながら最も見過ごされがちなステップです。最高のファイアウォールやネットワーク、ベストプラクティスを持っていても、検証を行わなければそれが機能しているかどうかまったく把握できません。

さらに、数少ないコマンドの知識や、どこで調べるべきかを知っているか否かが、侵害を防ぐかどうかの差になります。ハッカー気分を味わえることは加点です。(詳細と例については、監視と検証セクションに進んでください。)

信頼せず、二重に確認する

ポートの確認

⚠️ 重要: 自分が所有していないホストのスキャンを行わないでください。

ホームネットワークやVPSどちらにいても、世界に公開されているポートを把握する必要があります。

これを行う方法は2つあります:

外部ネットワークでのテスト

現在の(パブリック)IPアドレスを取得するには、ifconfig.meなどのサービスが便利です:curl https://ifconfig.me。ホスティングプロバイダーのダッシュボードで確認する方法もあります。

Get Public IP
curl -fsSL https://ifconfig.me
# --> CURRENT PUBLIC IP

パブリックIPを取得したら、外部ネットワークに接続する必要があります。友人のコンピュータ、スマートフォン/5Gホットスポット、または専用サーバホストを使用できます。

nmap External Scan
target_host="$(curl -fsSL https://ifconfig.me)"
# 注: `target_host` が目的のIPであることを確認してください
# 特定のポートをスキャン:
nmap -A -p 80,443,8080 --open --reason $target_host
# 上位100ポート:
nmap -A --top-ports 100 --open --reason $target_host
# 全ポート:
nmap -A -p1-65535 --open --reason $target_host

ネットワーク内でのテスト

nmap の使用に慣れるために、ローカルネットワークや自分のサーバー、ルーター、プリンタ、スマート冷蔵庫などをスキャンしてみましょう。

例: スキャンコマンド

Terminal window
# ローカルホストのすべてのオープンポートをスキャン
nmap -sT localhost
# 自分のマシンのプライベートIPでサービスをスキャン
nmap -sV 192.168.1.10
# ネットワーク内のサービス詳細を検出
nmap -sn 192.168.0.0/24
nmap -sn 10.0.0.0/24
# またはDockerの 172.18.0.1/16
nmap -sn 172.18.0.1/16
nmap スキャン
% nmap -A --open --reason 192.168.0.87
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-06 13:51 MST
Nmap scan report for dev02.local (192.168.0.87)
Host is up, received syn-ack (0.0067s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|_ 256 {FINGERPRINT} (ED25519)
80/tcp open http syn-ack Caddy httpd
|_http-server-header: Caddy
|_http-title: Dev02.DanLevy.net
443/tcp open ssl/https syn-ack
|_http-title: Dev02.DanLevy.net
1234/tcp open http syn-ack Node.js Express framework
|_http-cors: GET POST PUT DELETE PATCH
|_http-title: Dev02.DanLevy.net (application/json; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.36 seconds

オープンポートの確認

lsof に慣れましょう - macOS と Linux で利用可能です。これはネットワーク状態やディスクアクティビティを詳細に表示します。

lsof コマンド
# 特定ポートのモニタリング
sudo lsof -i:80 -Pn
Terminal window
# 確立済み接続の監視
sudo lsof -i -Pn | grep ESTABLISHED
# リスン状態の表示
sudo lsof -i -Pn | grep LISTEN
# IPアドレスではなくネットワーク名を表示(逆引きDNS検索が非常に遅くなる可能性あり)
sudo lsof -i -P | grep LISTEN
# すべてのネットワーク接続を監視
sudo watch -n1 "lsof -i -Pn"

サンプル出力

nmapによるリスナーのスキャン

ファイル監視

どのプロセスが最もハードドライブ帯域幅を使用しているかを特定するには iotop を使用できます:

Terminal window
sudo iotop

個別のファイル変更を確認するには、Linuxでは inotifywait、MacOSでは fswatch を使用できます。これはフォルダ単位またはシステム全体で不正な動作や異常な挙動を検出するのに役立ちます。

Terminal window
# ディレクトリ内のすべてのファイル変更を監視
sudo inotifywait -m /path/to/directory

MacOSでは fswatch を使用できます:
インストールはbrew install fswatchで行う

Terminal window
fswatch -r /path/to/directory

⏰ よく見落とされるヒント

  1. 認証試行や重要なエンドポイントへのレート制限
    Nginxのlimit_reqモジュールやSSHアクセス用のfail2banを通じて、ブルートフォース攻撃へのスロットリングはおそらく良い考えです。私は「おそらく」と言います。IPv6や安価なボットネットの時代において、それはかつての状態とは異なっているからです。

  2. 可能な限り読み取り専用ボリュームを使用する

    services:
    webapp:
    volumes:
    - ./config:/config:ro

    他のベストプラクティス(非rootユーザー、最小限のフォルダ権限)と組み合わせることで、:roボリュームマウントオプションはコンテナ内からの誤った変更や一部の書き込み試行を防ぐ追加の保護を提供します。これは、すでに広範な権限を持つプロセスがホストを攻撃するのを防ぐことはできません。

  3. コンテナアクセスの定期的な監査
    コンテナがシークレット、ポート、マウントを必要としない場合、それらを削除しましょう!

  4. WiFiの不審者に注意
    WiFiパスワードを変な奴らに教えてはいないでしょう?特に。しかし、友人…いや、家族にも教えていたかもしれません。彼らがどのアプリを持っているか、それがあなたのSSIDやパスワードを世界に共有していないか、あなたは決して知りません。

ホームネットワーク vs. 公共プロバイダー vs. チューニング

  1. バーチャル分離/DMZ
    ホームサーバーを別のVLANやDMZに配置できる場合は、それを行いましょう。これにより、サーバー側からの潜在的な侵害で内部デバイスがアクセス不能になることを防ぎます。

    • ホームサーバー専用のルーターまたはVLANを使用する。
    • ホームサーバー専用のWiFiネットワークを使用する。
    • ホームサーバー専用のサブネットを使用する。
  2. クラウドプロバイダー: Hetzner、Vultr、DigitalOcean、Linode、AWS、Azure、Google Cloudはすべて異なるファイアウォール機能を提供しています。

    • 一部のプロバイダーやサービスはデフォルトでポートをブロックしています。一部はオプトインや追加料金のオプションを提供しています。サービスプロバイダーのドキュメントを確認してください。
    • 多くのプロバイダーは高度な監視と脅威検出サービスを提供しています。
  3. VPN & トンネリング: 公開インターネットにサービスを露出せずにインターネット上でセキュアに接続するため、VPNに類似したオプションやトンネリングサービスの使用を検討してください。

    • TailScale、ngrok、ZeroTier。
    • WireGuard、OpenVPN。

🚀 本番環境チェックリスト

📚 補足資料

ありがとう

熱心なRedditユーザーに感謝します:

読んでもらってありがとう!このガイドがお役に立てたなら大変嬉しく思います。ご質問や提案があれば、以下のSNSからご連絡ください。あるいは「GitHubで編集」リンクをクリックしてPRを作成していただければ幸いです!❤️