Systemd Hardening: ProtectSystem、ProtectHome、PrivateTmpでサービスを保護する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

午前2時に起きたこと

以前、SSHへのブルートフォース攻撃を受けて、深夜に急いで対処したことがある。見知らぬIPから何百回ものログイン試行がログに残っているのを見たときの気持ち——決して気持ちいいものじゃない。あの日以来、サーバーのセットアップ方法を根本から変えた。問題が起きてから対処するのではなく、最初からセキュリティを固める方針に。

あの事件の後に取り入れたのがsystemd hardening——具体的にはProtectSystemProtectHomePrivateTmpというディレクティブだ。多くのsysadminが知らないか、複雑に思えて使っていない。実際のところ、設定は10分で終わるし、効果はすぐに目に見える。

問題の本質はこうだ。あるサービスが侵害されると、攻撃者は/etc/passwdを読んだり、ホームディレクトリに書き込んだり、/tmp経由で他のサービスにデータをリークしたりできてしまう。systemd hardeningはそれをカーネルレベルで防ぐ——追加のソフトウェアは一切不要だ。

設定前に押さえるべき3つのディレクティブ

ProtectSystem — システムファイルシステムをロックする

ファイルシステムへの書き込みを制限する。3段階のレベルがある:

  • true/usr/bootを読み取り専用でマウントする
  • full/etcも読み取り専用リストに追加する
  • strictReadWritePathsでホワイトリストされたパス以外、すべてのファイルシステムが読み取り専用になる

専用のデータディレクトリ以外への書き込みが不要なサービスには、通常strictを使っている。

ProtectHome — ホームディレクトリを隠す

すべてのユーザーのホームディレクトリを隠すか空にする。3段階のレベルがある:

  • true/home/root/run/userにアクセス不可になる
  • read-only — 読み取り専用でマウントする
  • tmpfs — 空のtmpfsをマウントし、サービスからはホームディレクトリが完全に空に見える

PrivateTmp — 独立した/tmp名前空間

各サービスに独立した/tmp/var/tmpの名前空間を提供する。これは思っている以上に重要だ。多くのアプリケーションが/tmpを一時データの受け渡しに使っている。あるサービスが侵害され/tmpにファイルを書き込んだとき、PrivateTmpがなければ同じサーバー上の別のサービスがそれを読める。

インストールと設定

追加でインストールするものはない——systemdはUbuntu、Debian、RHELに標準搭載されている。サービスファイルを修正するだけだ。

オーバーライドファイルで既存のサービスを設定する

パッケージのアップデートで上書きされるため、元のサービスファイルを直接編集すべきではない。代わりにsystemctl editを使おう:

sudo systemctl edit nginx

このコマンドはエディタを開き、/etc/systemd/system/nginx.service.d/override.confにファイルを作成する。以下の内容を追加する:

[Service]
# ファイルシステム保護
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

# Nginxはこれらのパスへの書き込みが必要なため、ホワイトリストに追加する
ReadWritePaths=/var/log/nginx /var/lib/nginx /run/nginx

保存後、リロードして再起動する:

sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl status nginx

最初からhardeningを組み込んだサービスファイルを作成する

自分で開発したアプリケーション(Python、Node.jsなど)のサービスファイルを作成する場合は、[Service]セクションに直接追加する:

[Unit]
Description=My Web App
After=network.target

[Service]
Type=simple
User=webapp
Group=webapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python app.py

# --- Hardening ---
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true

# アプリのデータディレクトリへの書き込みのみ許可
ReadWritePaths=/opt/myapp/data /var/log/myapp

[Install]
WantedBy=multi-user.target

追加で使えるディレクティブ

3つのメインディレクティブに加えて、すべてのサービスに追加するようにしているものがある:

  • NoNewPrivileges=true — サービスはsetuid/setgidで権限を昇格できない
  • ProtectKernelTunables=true/proc/sys/sysへの書き込みをブロックする
  • ProtectKernelModules=true — 任意のカーネルモジュールのロードをブロックする
  • ProtectControlGroups=true/sys/fs/cgroupが読み取り専用になる
  • RestrictSUIDSGID=true — 新しいSUID/SGIDファイルの作成をブロックする
  • RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 — 使用できるソケットの種類を制限する

確認とモニタリング

適用中の設定を確認する

再起動後、ディレクティブが有効になっていることを確認する:

systemctl show nginx | grep -E "ProtectSystem|ProtectHome|PrivateTmp"

期待される出力:

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

systemd-analyzeでセキュリティスコアを評価する

これはあまり知られていないビルトインツールだ。各サービスのセキュリティをスコアリングする(0が最高、10が最悪):

systemd-analyze security nginx

各ディレクティブの詳細と総合スコアが表形式で返される:

  NAME                              DESCRIPTION                          EXPOSURE
✓ PrivateTmp=yes                   Service has a private /tmp           0.0
✓ ProtectSystem=strict             Protected system files               0.0
✓ NoNewPrivileges=yes              Cannot gain new privileges           0.0
✗ User=/DynamicUser=               Service runs as root user            0.4
...
→ Overall exposure level for nginx.service: 3.8 OK 🙂

目標はスコアを4.0以下に下げることだ。3つのメインディレクティブだけで、スコアは通常7〜8から4〜5に下がる。

実際にテストしてprotectionが機能することを確認する

ProtectHomeが機能していることを確認する最速の方法は、プロセスの名前空間に入ってホームディレクトリを確認することだ:

# nginxのPIDを取得
PID=$(systemctl show nginx --property MainPID --value)

# nginxの名前空間内の/homeを確認
sudo nsenter -t $PID --mount -- ls -la /home/
# ProtectHome=trueなら → ディレクトリは完全に空

# 独立した/tmpを確認
sudo nsenter -t $PID --mount -- ls /tmp/
# PrivateTmp=trueなら → /tmpはクリーンで、他のプロセスのファイルは見えない

hardening有効化後のエラーを追跡する

このステップが最も重要だ——ReadWritePathsの設定漏れは、ProtectSystem=strictを有効にした後にサービスがクラッシュする最も一般的な原因だ:

sudo journalctl -u nginx -f

典型的なエラー:

nginx: [emerg] open() "/var/log/nginx/error.log" failed (30: Read-only file system)

オーバーライドファイルのホワイトリストにパスを追加して修正する:

sudo systemctl edit nginx
# 以下の行を追加する: ReadWritePaths=/var/log/nginx
sudo systemctl daemon-reload && sudo systemctl restart nginx

全サービスを素早く監査するスクリプト

サーバー上でhardeningが適用されていないサービスを確認するには:

#!/bin/bash
echo "セキュリティスコアが6.0以上のサービス(hardening要):"
for svc in $(systemctl list-units --type=service --state=running --no-legend | awk '{print $1}'); do
  score=$(systemd-analyze security "$svc" 2>/dev/null | grep "Overall exposure" | grep -oP '[0-9]+\.[0-9]+')
  if [[ -n "$score" ]] && (( $(echo "$score > 6.0" | bc -l) )); then
    echo "  $svc → score: $score"
  fi
done

適用する際の優先順位

すべてのサービスが同じレベルを必要とするわけではない。以下の順番で優先度を分けている:

  1. インターネットに公開されているサービス(nginx、apache、Node.js web)——すぐにstrictレベルで適用する
  2. 機密データを扱うサービス(データベース、認証サービス)——strictに加え、localhostのみで使う場合はPrivateNetworkも追加する
  3. バックグラウンドジョブ、cronタスク——最低でもProtectSystem=fullPrivateTmp=trueを設定する
  4. コアシステムサービス(sshd、systemd-resolved)——慎重に、必ずステージング環境で十分にテストしてから適用する

SSHへのブルートフォース攻撃から学んだこと:攻撃者は最初からrootを必要としない。あるサービスに侵入し、データベースパスワードが含まれた設定ファイルを読み取り、そこから別のターゲットへ横移動するだけでいい。ProtectSystem=strictはまさにその一歩をブロックする——サービスが侵害されても、隣のサービスの/etc/otherapp/db.confは読めない。

設定は10分、systemd-analyze securityでの監査がさらに5分。その見返りは、午前2時に安心して眠れることだ。

Share: