はじめに:root権限に頼りすぎない!
Linuxを扱うDevOpsの皆さんなら、「手軽だからroot権限で実行する」という経験は少なくないでしょう。sudo python app.py や sudo ./start_service.sh の一言で済む。しかし、ここでの「手軽さ」は「危険」と表裏一体です。アプリケーションに完全なroot権限を与えることは諸刃の剣。特にアプリケーションにセキュリティ上の脆弱性がある場合、これは非常に危険です。では、アプリケーションがroot権限なしに特権的な操作(例えばポート80/443でリッスンするなど)を行うにはどうすればよいでしょうか?
その答えがLinux Capabilitiesにあります。これは詳細な権限管理メカニズムであり、rootアカウントの特権をより小さな「能力」に分割することを可能にします。そして、それらを個々のプロセスや実行ファイルに個別に割り当てることができます。これは、配管工に家のすべての鍵を渡すのではなく、浴室の鍵だけを渡すようなものです。
クイックスタート:Pythonスクリプトにポート80のリスニング権限を5分で付与する
Capabilitiesの「力」をすぐにご理解いただくために、実際の例でご説明します。
問題: 単純なPythonスクリプトが、ポート80(特権ポート、root権限が必要)でHTTPサーバーを実行したいが、スクリプト全体をroot権限で実行したくない。
ステップ1:Pythonスクリプトの準備
以下の内容で simple_server.py ファイルを作成します。
import http.server
import socketserver
PORT = 80
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"ポート {PORT} でサービスを提供中")
httpd.serve_forever()
ステップ2:実行を試みる(そして失敗する)
このスクリプトを通常ユーザー(rootではない)で実行してみます。
python3 simple_server.py
Permission denied または socket.error: [Errno 13] Permission denied のようなエラーが表示されるでしょう。予想通り、ポート80は「大人」のものです。
ステップ3:Capabilityを割り当てる
ここでCapabilitiesの出番です。setcap コマンドを使用して、特権ポートでリッスンする能力 (CAP_NET_BIND_SERVICE) を割り当てます。Pythonインタープリタに割り当てるのが最善です。スクリプトに直接割り当てることも可能ですが、小規模なスクリプトにはこの方法が便利です。
sudo setcap 'cap_net_bind_service=+ep' $(eval readlink -f $(which python3))
上記のコマンドの説明:
cap_net_bind_service: プロセスが1024未満のポートにバインドすることを許可するcapability。+ep: 「e」(effective) はこのcapabilityが有効化されることを意味し、「p」(permitted) はプロセスがこのcapabilityを使用することを許可されることを意味します。$(eval readlink -f $(which python3)): 使用中のpython3インタープリタの絶対パスを見つけます。
ステップ4:再実行する(そして成功!)
今度は、スクリプトを通常ユーザーで再実行してみます。
python3 simple_server.py
そして「やった!」 Serving at port 80 という出力が表示されるでしょう。ブラウザを開いて http://localhost (またはサーバーのIPアドレス)にアクセスすると、サーバーが稼働し、ファイルを提供しているのがわかります。これで私たちのPythonスクリプトは、完全なroot権限で実行することなく、ポート80でリッスンできるようになりました。
ステップ5:クリーンアップ(重要!)
テスト後、セキュリティを確保するためにcapabilityを削除することを忘れないでください。Pythonインタープリタにcapabilityを割り当てることは常に良い考えとは限りません。なぜなら、あらゆるPythonスクリプトがそれを悪用できる可能性があるからです。
sudo setcap -r $(eval readlink -f $(which python3))
詳細な説明:なぜLinux Capabilitiesが重要なのか?
以前のLinuxでは、プロセスはroot(「Godモード」権限)であるか、または通常ユーザー(非常に制限された)であるかのどちらかでした。中間はありませんでした。これにより、2つの大きな問題が生じました。
- セキュリティリスク: Nginx、Apache、またはカスタムアプリケーションのようなサービスがポート80にバインドする必要がある場合、root権限で起動することを余儀なくされました。その後、アプリケーションは通常、より権限の少ないユーザーに「権限を降格(drop privileges)」します。しかし、起動プロセスまたは「権限降格」でエラーが発生した場合、アプリケーション全体がroot権限で実行され続けてしまいます。これは、悪用された場合に非常に大きなセキュリティ脆弱性を生み出します。
- 柔軟性の制限: 多くのアプリケーションはrootの小さな特権を1つか2つしか必要としません。しかし以前は、それらすべてを持つことを強制されていました。例えば、ファイルの所有権を変更する (
chown) あるいはネットワーク情報 (net_admin) を読み取るには、すべてのroot権限を持つ必要がありました。
Linux Capabilitiesは、この問題を解決するために生まれました。これはrootアカウントの特権を約40〜50の別々の「能力」に分割します(例:CAP_CHOWN、CAP_DAC_OVERRIDE、CAP_NET_ADMIN、CAP_SYS_ADMINなど)。各能力は、以前はrootだけが行うことができた特定の操作を許可します。これにより、プロセスに必要なcapabilityのみを正確に割り当てることができ、rootの「王冠」全体を渡す必要がなくなります。
私がよく目にする一般的なcapabilitiesをいくつか紹介します。
CAP_NET_BIND_SERVICE: 1024未満のポートにバインドすることを許可します。CAP_CHOWN: ファイルの所有者 (owner) を変更することを許可します。CAP_DAC_OVERRIDE: ファイルの読み取り/書き込み/実行のアクセス権チェックをバイパスします。CAP_NET_ADMIN: ネットワーク管理操作 (ルートの追加/削除、インターフェース設定など) を実行することを許可します。CAP_SYS_ADMIN: 最も「強力な」capabilitiesの1つで、多くのシステム管理特権を含みます。これを付与する際には注意が必要です。
各プロセスはいくつかのcapabilityセットを持っています。
- Permitted (P): プロセスが使用を許可されているcapabilityのセット。
- Effective (E): Permittedのサブセットで、現在プロセスによって有効化され使用されているcapabilityです。
- Inheritable (I):
execve呼び出し時に子プロセスに継承されるcapability。 - Bounding (B): プロセスが持ち得るすべてのcapabilityの「上限」を制限します。
- Ambient (A): SUIDを使用せずにrootから非rootユーザーに切り替えたときに保持されるcapabilityのセット。これはやや高度な概念ですが、コンテナ内で非常に役立ちます。
高度な内容:getcap、setcap、systemdによるCapabilitiesの管理
1. 現在のCapabilitiesの確認
実行ファイルに割り当てられているcapabilitiesを確認するには、getcap を使用します。
getcap /usr/bin/ping
結果は /usr/bin/ping = cap_net_raw+ep のようになるかもしれません。これは、ping がeffectiveおよびpermittedモードで CAP_NET_RAW capability(生ICMPパケットを作成するため)を持っていることを意味します。これが通常ユーザーでも ping を使用できる理由です。
実行中のプロセスのcapabilitiesを確認するには、/proc/<pid>/status ファイルを読み取ることができます。
cat /proc/self/status | grep Cap
CapPrm (Permitted)、CapEff (Effective)、CapInh (Inheritable)、CapBnd (Bounding)、CapAmb (Ambient) のような行が16進数で表示されます。これらの数値が何を意味するのか理解するには、16進数からcapability名へのマッピングテーブルを参照するか、capsh --decode=<hex_value> のようなツールを使用する必要があります。
2. Capabilitiesの割り当てと削除
setcap の基本的な構文:
sudo setcap 'cap_NAME1+ep cap_NAME2+ep' /path/to/executable
sudo setcap -r /path/to/executable # すべてのcapabilitiesを削除
注意:信頼できる実行ファイルにのみcapabilitiesを割り当てるべきです。そうしないと、攻撃者がそのファイルを悪用して権限を昇格させる可能性があります。
3. systemdにおけるCapabilities
DevOpsの皆さんにとって、systemd を使ったサービス管理は当然のことでしょう。systemdは、サービスのcapabilitiesを制御するための強力なディレクティブを提供します。
AmbientCapabilities: サービスにAmbient capabilitiesを割り当てます。これは、非rootで実行されるが特定の特権を必要とするアプリケーションにとって非常に役立ちます。CapabilityBoundingSet: サービスが持ち得るcapabilitiesのセットを制限します。これは強力な防御メカニズムであり、アプリケーションがさらに多くの権限を取得しようとしても、この制限を超えることはできないようにします。デフォルトでは、すべてのcapabilitiesが含まれます。これを最小限に抑えることをお勧めします。NoNewPrivileges=true: プロセスが(SUID/SGIDビットなどを介して)権限を昇格できないようにします。サービスでは常にこれを有効にすべきです!
Nginxのサービスファイルは(Nginxはすでにかなりセキュリティが最適化されていますが)次のように見えるかもしれません。
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
PrivateTmp=true
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
上記の例では、CapabilityBoundingSet を CAP_NET_BIND_SERVICE と CAP_DAC_OVERRIDE のみに制限しています(これは単なる例であり、むやみに適用すべきではありません)。これは、Nginxが要求しようとしても、これら2つ以外のcapabilityを持つことはできないことを意味します。同時に、NoNewPrivileges=true は、新しい権限を取得できないように保証します。
実践的なヒント:Capabilitiesに関する貴重な経験
1. 最小特権の原則 (Principle of Least Privilege)
これは指針となる原則です。アプリケーションの動作に必要な最小限のcapabilityのみを割り当ててください。結果を本当に理解していない限り、CAP_SYS_ADMIN を決して付与してはいけません。これはroot権限とほぼ同等です。
2. デバッグは芸術
sysadminを始めたばかりの頃、ログを注意深く読まず、コードやファイアウォールの設定ミスだと思い込んで、この問題のデバッグに丸一日を費やしたことがあります。まさか、CAP_NET_BIND_SERVICE が足りなかっただけとは! アプリケーションが期待通りに動作せず、権限について疑問がある場合は、strace を使用してください。
strace -e capability python3 simple_server.py
このコマンドは、プロセスが実行するcapability関連のすべてのシステムコールを表示します。これにより、アプリケーションが何をしようとしているのか、どのcapabilityが拒否されているのかを確認できます。特定のシステムコール(例えば bind())に関連する EPERM エラーが表示された場合、対応するcapabilityが不足している可能性が非常に高いです。
3. 共通ファイルに対する setcap の注意点
インタープリタ(Pythonなど)や共通のシステムユーティリティ(/usr/bin/curlなど)にcapabilityを割り当てる場合は、細心の注意を払う必要があります。なぜなら、そのファイルに過剰な権限が付与されている場合、誰でもそれを悪用できる可能性があるからです。バイナリまたはスクリプトのコピーを作成し、そのコピーにcapabilityを割り当てて、そのコピーのみを実行するのが最善です。
4. Capabilitiesとコンテナ
コンテナ環境(Docker、Kubernetes)では、Capabilitiesはますます重要になります。デフォルトでは、Dockerは不要なほとんどのcapabilitiesを削除し(ドロップし)、安全なサブセットのみを保持します。コンテナ実行時に --cap-add と --cap-drop を使用して、このセットをカスタマイズできます。これにより、コンテナの攻撃対象領域を大幅に削減できます。
docker run --cap-add=NET_ADMIN --cap-drop=CHOWN my_image command
常に、コンテナランタイムがデフォルトで保持するcapabilitiesを確認し、アプリケーションのニーズに合わせて調整してください。
結論
Linux Capabilitiesは、システムのセキュリティと制御を強化するための非常に強力なツールです。このメカニズムを適切に理解し、適用することで、より安全なアプリケーションを構築し、セキュリティ脆弱性によるリスクを軽減し、「最小特権の原則」を遵守することができます。今日からCapabilitiesの探求を始め、DevOpsプロセスに統合していきましょう!
