物理サーバーのサービスで書き込み先を制限し拒否動作を検証

物理サーバーのサービスで書き込み先を制限し拒否動作を検証

1台の物理サーバーで複数のバックエンドサービスを動かす場合、プログラムが書き込む必要のある場所は通常、状態保存用のディレクトリに限られます。しかし、ディレクトリの所有権を広く与えすぎると、デプロイ済みのファイルまで変更できてしまいます。サービス専用アカウントを作るだけでは、最初の境界を設けたにすぎません。サービスから見えるファイルシステムを読み取り専用にし、必要なパスだけを書き込み可能にすることで、誤った書き込みの影響範囲を抑えられます。検証も「サービスが起動した」で終わらせず、書き込めてはいけない場所に実際に書き込めないことまで確認する必要があります。

1. 書き込み先を洗い出してからサンドボックスを設定する

本記事では Ubuntu Server 24.04 LTS を例に、systemd でシステムレベルのサービスを管理します。作業には管理者権限と Python 3 が必要です。使用するパスはすべて独立した実験用ディレクトリで、既存のサービスユニットは変更しません。AmdMetal の物理サーバーのログイン情報は、支払い後にコンソールで確認できます。Linux には SSH で接続します。

まず、プログラムコード、永続的な状態データ、ランタイムファイル、一時ファイルを区別します。コードの更新はデプロイ用アカウントが行い、サービスアカウントには読み取りだけを許可します。状態ディレクトリには、再起動後も保持すべきデータを保存します。ソケットやプロセス ID ファイルなどのランタイムファイルと一時ファイルも、保存先を別途一覧にまとめます。ログを標準出力に出す構成なら、ログシステムに収集を任せられるため、ログディレクトリ全体への書き込みを許可する必要はありません。

中心となる設定は ProtectSystem=strict です。サービスのマウント名前空間内で、ファイルシステム階層を読み取り専用にします。ただし、一部の API ファイルシステムなどは例外です。そのうえで ReadWritePaths により、指定したディレクトリへの書き込みを可能にします。この設定はホスト全体のマウント状態を変えるものではなく、プロセスを完全に隔離する仕組みでもありません。

読み取り専用の制限は、機密性を保証するものではありません。読み取り可能なファイルが自動的に隠されるわけでも、ネットワークアクセスが自動的に制限されるわけでもありません。また、書き込み許可リストはディレクトリのアクセス権限の代わりにはなりません。

2. 拒否の原因を切り分けられる権限の基準を作る

実験では2つのディレクトリを用意します。一方は書き込みを許可する場所、もう一方は通常のアクセス権限では書き込めるものの、サンドボックスでは拒否されるべき場所です。これにより、「もともと権限がなかっただけ」の失敗を、制限が機能したと誤判定するのを防げます。以下のアカウント名とパスが既存のサービスで使われていないことを、先に確認してください。いずれも実験用であり、サーバーの初期ログインアカウントではありません。

sudo useradd --system --no-create-home \
  --shell /usr/sbin/nologin sandbox-check

sudo install -d -o root -g root -m 0755 \
  /srv/sandbox-check

sudo install -d -o sandbox-check -g sandbox-check -m 0750 \
  /srv/sandbox-check/blocked \
  /var/lib/sandbox-check

sudo runuser -u sandbox-check -- /usr/bin/python3 -c \
'import tempfile, os; f, p = tempfile.mkstemp(dir="/srv/sandbox-check/blocked"); os.close(f); os.unlink(p)'

最後のコマンドは正常終了する必要があります。書き込み禁止にする予定のディレクトリ内にランダムな名前の一時ファイルを作り、すぐに削除する処理です。失敗した場合は、まずパス上のディレクトリの通過権限、ACL、基盤となるファイルシステムのマウント属性を確認します。この問題を解消しないまま、以降の失敗をサンドボックスが原因だと判断してはいけません。

配置するスクリプト自体は管理者所有にし、サービスアカウントが検査ロジックを書き換えられないようにします。実験用ディレクトリを、所有者が書き込める構成にしているのは、制限の原因を証明するための意図的な設定です。本番アプリケーションのコードディレクトリは、サービスアカウントが書き込めない状態を維持し、この実験上の例外をそのまま持ち込まないでください。

3. 書き込み許可リストと独立した検証スクリプトを用意する

次のスクリプトを /srv/sandbox-check/probe.py として保存し、所有者を管理者、パーミッションを 0644 に設定します。許可したパスでは書き込みが成功し、禁止したパスでは読み取り専用ファイルシステムのエラーが返ることを必須とします。それ以外のエラーはすべて検証失敗として扱います。

import errno
import os
import tempfile

def create_and_remove(directory):
    fd, path = tempfile.mkstemp(prefix="probe-", dir=directory)
    try:
        with os.fdopen(fd, "w") as stream:
            stream.write("sandbox-check\n")
            stream.flush()
            os.fsync(stream.fileno())
    finally:
        os.unlink(path)

create_and_remove("/var/lib/sandbox-check")

try:
    create_and_remove("/srv/sandbox-check/blocked")
except OSError as exc:
    if exc.errno != errno.EROFS:
        raise
else:
    raise RuntimeError("Write boundary was not enforced")

print("Allowed write passed; blocked write returned EROFS")

既存データの上書きを避けるため、ファイル名はランダムに生成します。fsync は書き込み処理の一連の呼び出しを確認するためのものであり、性能や電源断に対するデータの永続性をテストしたことにはなりません。検証スクリプトの出力は検証結果のみで、認証情報や業務データは含みません。

/etc/systemd/system/sandbox-check.service を作成します。

[Unit]
Description=Filesystem sandbox verification

[Service]
Type=oneshot
User=sandbox-check
Group=sandbox-check
ExecStart=/usr/bin/python3 /srv/sandbox-check/probe.py
ProtectSystem=strict
ReadWritePaths=/var/lib/sandbox-check
PrivateTmp=true
NoNewPrivileges=true

PrivateTmp=true は、サービスに隔離された一時ディレクトリのビューを提供します。したがって、「永続ディレクトリ1か所だけに書き込みを許可する」ことを、「プロセス全体で書き込める場所が1か所しかない」と解釈してはいけません。NoNewPrivileges=true は、プログラムの実行によって新たな特権を得ることを制限しますが、すでに付与されているファイルアクセス権限を取り消すものではありません。

4. 書き込みの成功と拒否を両方検証する

まずユニットを検査し、定義を再読み込みしてから実行します。

sudo systemd-analyze verify /etc/systemd/system/sandbox-check.service
sudo systemctl daemon-reload
sudo systemctl start sandbox-check.service

sudo systemctl show sandbox-check.service \
  -p Result -p ExecMainStatus -p ProtectSystem -p ReadWritePaths

sudo journalctl -u sandbox-check.service -b --no-pager -n 30

期待する結果は Result=success、ExecMainStatus=0 で、ログには検証スクリプトの成功メッセージが表示されます。一度実行して終了するサービスなので、実行後に稼働中ではない状態と表示されても異常ではありません。ここでは、継続実行やアクティブ状態の維持は設定していません。

設定値を見るだけでは、書き込みの境界が機能している証明にはなりません。有効な証拠には、サンドボックス外では同じアカウントで書き込みが成功すること、サンドボックス内では許可ディレクトリへの書き込みが成功すること、禁止ディレクトリへの書き込みでは EROFS が返ることのすべてが必要です。EACCES が返った場合に確認できるのは、通常のアクセス権限または別のセキュリティポリシーによってアクセスが拒否されたことだけであり、この検証に合格したとは判断できません。

本番サービスでは実際の業務処理のパスもテストする

検証スクリプトが成功したら、実際のサービス用にセキュリティ強化設定を別途作成します。アップロード、キャッシュ生成、状態保存、一時ファイルの削除を含む業務テストを実施してください。アプリケーションが「一時ファイルを作成してから名前を変更する」方式でデータを更新する場合は、保存先のディレクトリ自体にも書き込み権限が必要です。ファイル1つだけを例外にしても、十分でないことがよくあります。

シンボリックリンクの実際の参照先も確認します。設定上は状態ディレクトリ内にあるように見えるリンクでも、最終的には別の読み取り専用パスを指している可能性があります。親ディレクトリ全体の権限を広げて問題を覆い隠してはいけません。

5. トラブルシューティング、取り消し、検証結果の記録

サービスの起動前に失敗する場合は、まずログを読み、マウント名前空間を作成できないのか、パスが存在しないのか、設定の記述に誤りがあるのかを確認します。起動後に失敗する場合は、ファイル操作が行われる場所に着目します。プログラムが意図せずコードディレクトリにキャッシュを書いていないか、共有の一時ファイルに依存していないか、追加のランタイムディレクトリを必要としていないかを確認してください。

本番サービスにランタイムディレクトリが必要なら、RuntimeDirectory でそのライフサイクルを systemd に管理させたうえで、実際のアクセスをテストできます。/var や /srv 全体を許可リストに追加してはいけません。確認できた書き込み要件に従い、必要なパスを個別に許可します。

設定変更後に daemon-reload を実行しても、ユニット定義が再読み込みされるだけで、すでに動作しているプロセスに新しいサンドボックスが適用されるわけではありません。本番サービスでは、管理された手順で再起動して新しいプロセス環境を作り、書き込み成功と拒否の両方を改めてテストする必要があります。

実験を取り消すときは、ユニットを停止し、今回作成したユニットファイルを削除してから、定義を再読み込みします。実験用データディレクトリとアカウントは、使用しているプロセスがないことを確認したうえで個別に削除します。業務ディレクトリを再帰的に削除する汎用コマンドは実行しません。本番サービスの場合は、変更前に保存しておいた設定へ戻し、再起動して検証します。

最後に、ユニットの実効設定、サービスアカウント、書き込み許可リスト、検証スクリプトの終了ステータス、実際の業務テストの結果を記録します。検証の目的は、セキュリティスコアを高くすることではありません。必要な書き込みがすべて成功し、禁止する予定の書き込みがすべて拒否され、その理由を説明でき、同じ結果を再現できる証拠がそろっていることが目標です。

よくある質問

ReadWritePathsで通常の書き込み権限も付与できますか?

できません。読み取り専用のサンドボックス内に書き込み可能な例外を設ける設定であり、所有者、モード、ACLは変更しません。サービスアカウント自体の権限も必要です。

ProtectSystem=strictで秘密情報の読み取りも防げますか?

防げません。主に書き込みを制限する設定で、読み取り可能なファイルを自動的に隠したり通信を遮断したりはしません。機密ファイルの権限、パスの非公開化、ネットワーク制限は別途設計します。

AMD EPYC 7543P 専用物理サーバー

月額$458.7〜

AmdMetal 物理サーバーを構成 AmdMetal の料金を見る

物理サーバーの構成をひとつずつ確認

AMD EPYC 7543Pを搭載した専有サーバーです。機種とオプションの料金は構成ページでまとめてご確認いただけます。