SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

systemdサービスをProtectSystemでサンドボックス化

ProtectSystem、PrivateTmp、DynamicUser、NoNewPrivilegesの違いを解説します。各設定が防ぐ操作や壊れる機能、起動時の障害をデバッグする方法を確認できます。

systemd サンドボックス化の役割

systemd サンドボックス化は、unit file を作成する作業の後半です。Type= はサービスの起動方法を決め、ProtectSystem= や PrivateTmp= などのディレクティブは、起動後にサービスがアクセスできる対象を決めます。これらはカーネルの機能であり、mount namespace や seccomp filter を systemd がプロセスに制御を渡す前に適用します。アプリケーションからはこれらが見えず、コードの変更も必要ありません。

デフォルトでは、サンドボックス化は一切行われません。サンドボックス化されていない unit は root として実行され、どこにでも書き込み、システム上のすべてのファイルを読み取り、kernel module をロードできます。その unit がインターネットからアクセス可能な Web アプリケーションであれば、ファイルアップロードの脆弱性1つでサーバー全体が侵害されます。以下の unit では、同じアプリケーションに読み取り専用のファイルシステム、空の /home、他のプロセスから見えない /tmp が与えられます。また、setuid binary を見つけた場合でも root へ昇格する経路はありません。

これらは一度に1つの unit に対して適用されます。あるサービスを強化しても、隣接するサービスには影響しません。そのため、まず公開ポートで待ち受けているサービスから着手します。

unit ファイル全体

notes は小規模な Web サービスです。localhost で待ち受け、/var/lib/notes の下に SQLite データベースを保持し、nginx の背後で動作します。バイナリがフォアグラウンドで動作し続けるため、Type=exec が適しています。Type=simple、exec、forking、notify の違いによって、systemd が起動を追跡する方法が決まります。以下の各ディレクティブについては、後半で、それによって防げる問題と、よく発生する障害を含めて説明します。

[Unit]
Description=Notes web service
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
ExecStart=/opt/notes/bin/notes --listen 127.0.0.1:8080
DynamicUser=yes
StateDirectory=notes
Environment=NOTES_DB=/var/lib/notes/notes.db

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectProc=invisible

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target

/etc/systemd/system/notes.service に書き込み、sudo systemctl daemon-reload を実行してから、sudo systemctl restart notes.service を実行します。パッケージに含まれる unit の場合は、vendor ファイルを編集しないでください。sudo systemctl edit notes.service を実行すると、/etc/systemd/system/notes.service.d/override.conf に drop-in が開かれます。ここには [Service] の追加設定だけを記述し、パッケージのアップグレード後も維持できます。systemctl cat notes.service は、vendor ファイルを先頭に配置したマージ済みの結果を表示します。

サービスの実行ユーザー

User= がない場合、サービスは root として実行されます。この場合、ここにある他のすべてのディレクティブは被害を抑えるための設定になります。この状態を避ける方法は2つあります。

静的な system ユーザー。 sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes で作成し、unit に User=notes を設定します。再起動や再ブート後も UID は変わりません。サービス自身の state directory 以外にあるファイルをサービスが所有する場合や、バックアップジョブがそれらを読み取る必要がある場合に重要です。各サービスに専用の非特権アカウントを割り当てる理由が、そのまま当てはまります。

DynamicUser=yes。 systemd はサービスの起動時に予約済みの範囲から UID を割り当て、サービスの停止時に解放します。ディスク上にアカウントが存在しないため、サービスを削除しても不要なものが残りません。サービスの実行中は、getent passwd notes が systemd の NSS(name service switch)モジュールを通じて名前を解決します。停止後は、その名前もなくなります。

DynamicUser=yes を設定すると、いくつかの設定も自動的に有効になります。RemoveIPC=yes、NoNewPrivileges=yes、RestrictSUIDSGID=yes、ProtectSystem=strict、ProtectHome=read-only、およびプライベートな /tmp です。現在の systemd では、そのプライベートな /tmp は PrivateTmp=disconnected です。これは新しい tmpfs ですが、例の unit のように PrivateTmp=yes を自分で設定した場合は異なります。1行でサンドボックスの大部分を構成できるため、例の unit は短いままです。

壊れるもの。 動的 UID は、任意の場所にあるファイルを所有できません。サービス間で番号が再利用されるためです。永続データは StateDirectory=、CacheDirectory=、または LogsDirectory= を通す必要があります。systemd は起動のたびにこれらを作成し、現在の UID に chown します。DynamicUser=yes を使用した場合、実体のディレクトリは /var/lib/private/notes で、/var/lib/notes はそこを指すシンボリックリンクです。/var/lib/private のモードは 0700 で、root が所有します。そのため、通常のユーザーとして実行されるバックアップは、root からは完全に読み取り可能に見えるパスで Permission denied になります。固定された所有者を必要とするもの、SSH key、NFS export、別のサービスが読み取るファイルなどでは、代わりに静的なユーザーを使用してください。

ファイルシステムの見え方: ProtectSystem、ProtectHome、PrivateTmp

ProtectSystem= には 3 つの値があります。yes は /usr とブートディレクトリを読み取り専用でマウントします。full は /etc を追加します。strict は、/dev、/proc、/sys というカーネル API ディレクトリを除き、階層全体を読み取り専用でマウントします。これらのディレクトリは別のディレクティブで扱います。strict から始めて、必要な箇所だけ書き込み可能にしてください。最初に緩く設定して、後から厳しくする運用は定着しません。

書き込み可能にする箇所は ReadWritePaths=/srv/notes/uploads です。必要な数は想定より少なくなります。StateDirectory=、LogsDirectory=、CacheDirectory=、RuntimeDirectory= は ProtectSystem=strict の下で自動的に書き込み可能なままになるため、例の unit には ReadWritePaths= の行がありません。strict では /tmp も読み取り専用になります。ただし、PrivateTmp=yes により unit 専用の書き込み可能なものが渡される場合を除きます。

壊れるもの。 これらのパス以外への書き込みは Read-only file system で失敗します。/etc に自身の設定を保存し直すアプリケーション、/run に直接 PID ファイルを作成するアプリケーション、/opt にプラグインを展開するアプリケーションは、いずれも影響を受けます。エラーからパスを確認し、親ディレクトリではなく、そのパスだけを追加してください。ReadWritePaths= に記載したパスが存在しない場合、警告ではなく起動失敗になります。そのため、任意のパスにはハイフンを付けて ReadWritePaths=-/srv/notes/uploads のようにします。

読み取り専用であることと、見えないことは同じではありません。ProtectSystem=strict の下では、サービスは引き続き /etc/passwd を読み取れます。また、別のアプリケーションに属する、全ユーザーから読み取り可能な Secret も読み取れます。InaccessiblePaths=/etc/ssh /srv/otherapp はサブツリーを unit のビューから完全に取り除きます。サービス自身の Secret については、LoadCredential=dbpass:/etc/notes/dbpass がファイルを unit 専用のディレクトリにコピーします。このディレクトリを読み取れるのは、そのサービスだけです。アプリケーションは $CREDENTIALS_DIRECTORY からファイルを読み取ります。

ProtectHome=yes は /home、/root、/run/user を空に見せます。Web サービスが home ディレクトリを使う必要はありません。これにより、パストラバーサルの脆弱性から /root/.ssh に到達されることも防げます。read-only と tmpfs は、より緩やかな値です。データが実際に home ディレクトリにあるアプリケーションは動作しなくなります。/home/app の下に手動でインストールしたアプリケーションの多くが該当します。データを /var/lib に移すか、ProtectHome=read-only を設定して保護効果が小さくなることを受け入れてください。

PrivateTmp=yes はサービス専用の /tmp と /var/tmp を用意します。これらはサービスの起動時に作成され、停止時に削除されます。これにより、サービス間の一時ファイル競合に関する問題をすべて解消できます。また、クラッシュしても、システム上のすべてのユーザーが一覧表示できるディレクトリに Secret が残ることはありません。

壊れるもの。 /tmp を共有ポイントとして扱うものです。MySQL へ /tmp/mysql.sock 経由で接続するよう設定したサービスは、Can't connect to local MySQL server through socket '/tmp/mysql.sock' と報告するようになります。データベースはホストの /tmp にソケットを作成しますが、サービスは自身の /tmp の中を探すためです。127.0.0.1 を指定するか、/run の下にある実際のソケットパスを指定してください。同じ問題はデバッグ時にも発生します。サービスが /tmp に書き込んだファイルは、シェルの /tmp には表示されません。中を確認するには、サービスの mount namespace に入ってください。

pid=$(systemctl show --property=MainPID --value notes.service)
sudo nsenter --target "$pid" --mount ls -l /tmp

PrivateDevices=yes は /dev を、/dev/null、/dev/zero、/dev/urandom など少数の疑似デバイスに置き換えます。物理デバイスは存在しない状態になります。ディスクデバイスノード、/dev/kvm、/dev/net/tun、サウンドカード、GPU はすべて見えなくなります。そのため、ハードウェアトランスコードを行うメディアサーバーは /dev/dri/renderD128 を開けず、ソフトウェア処理にフォールバックするか終了します。サービスが実際に 1 つのデバイスノードを必要とする場合は、その unit で PrivateDevices= を無効にし、DeviceAllow=/dev/dri/renderD128 rw でノードを指定してください。これは、システム上のすべてのデバイスを許可するデフォルトよりも、はるかに限定的です。

サービスが取り得る権限: NoNewPrivileges と capabilities

NoNewPrivileges=yes は、kernel が決して解除しないプロセスフラグを設定します。その時点以降、そのプロセスと、そこから起動されるすべての子プロセスは、setuid binary や file capability を介して権限を取得できません。unit 内で最も価値の高い単一行です。ローカル権限昇格の多くの攻撃経路を、最初の段階で止められるためです。

壊れるもの。 サービス内部から sudo を呼び出す処理は動作しません。現在の sudo は他の処理に先立ってこのフラグを確認し、The "no new privileges" flag is set, which prevents sudo from running as root. で終了します。別の場所で引用されている古い effective uid is not 0 ... 'nosuid' option エラーは、mount または install の問題を示すものであり、このディレクティブが生成するものではありません。unix_chkpwd を別プロセスとして実行する PAM (pluggable authentication modules) の password チェックも、同じ理由で失敗します。newuidmap を必要とする rootless container tool も同様です。サービスがいずれかに依存している場合は、ディレクティブを削除するのではなく、その依存関係をなくしてください。

CapabilityBoundingSet= は、unit 内の各プロセスが保持できる capabilities を制限します。空の assignment を指定すると、すべての capabilities が削除されます。すでに non-root user で実行するサービスでは、これは最初のロックに加わる第 2 のロックです。NoNewPrivileges=yes が capability を取得する通常の方法を阻止するためです。両方を維持してください。異なる形で失敗するため、失敗を 2 回検出できます。

Web service が必要とすることの多い capability は、1024 未満の port 用の CAP_NET_BIND_SERVICE です。non-root process には、単に許可するだけでなく付与する必要があるため、両方の行が必要です。

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

systemd は privileges を削除する前に ambient capabilities を適用するため、これは NoNewPrivileges=yes と併用しても動作します。これらの行がないと、サービスは起動した後、listen tcp :443: bind: permission denied のように port を示すエラーで終了します。多くの VPS 環境では、127.0.0.1:8080 に bind し、nginx または Caddy に 443 を管理させる方法が適切です。この方法なら、unit から capability を完全に除外できます。先頭の ~ はリストを反転させるため、CapabilityBoundingSet=~CAP_SYS_ADMIN だけをブロックし、残りを許可します。allow 形式を優先してください。新しい capabilities は追加され続けるため、deny list は時間の経過とともに管理しにくくなります。

カーネルが公開する機能

ProtectKernelTunables=yes は、この unit に対して /proc/sys、/sys およびその配下の書き込み可能なファイルを読み取り専用にします。起動時に sysctl を設定するサービスは動作しなくなります。起動スクリプトは sysctl: setting key "vm.max_map_count": Read-only file system を出力して終了します。値は /etc/sysctl.d/ に設定してください。そこが適切な場所であり、再起動後も維持されます。そのうえで、このディレクティブは有効なままにします。

ProtectKernelModules=yes はモジュールの読み込みを禁止します。modprobe を実行する unit は modprobe: ERROR: could not insert 'nf_conntrack': Operation not permitted になります。サービスからではなく、/etc/modules-load.d/ を使用してブート時にモジュールを読み込んでください。

ProtectKernelLogs=yes は dmesg を取り上げます。ProtectControlGroups=yes は /sys/fs/cgroup を読み取り専用にするため、container runtime や cgroup を自身で管理するソフトウェアは直ちに影響を受けます。ProtectProc=invisible は /proc にある他のユーザーのプロセスを隠します。これにより、侵害されたサービスから別の daemon のコマンドラインや、そこで渡されたパスワードを読み取れなくなります。/proc を走査する monitoring agent では、この設定を無効にする必要があります。

RestrictNamespaces=yes は、サービスによる新しい namespace の作成を停止します。これは container runtime に必要な操作であり、攻撃者が脱出経路を構築する際にも使用します。LockPersonality=yes は kernel の実行ドメインの変更を禁止し、RestrictSUIDSGID=yes はサービスによる setuid ファイルの作成を停止します。いずれも適用コストが低く、通常のアプリケーションで問題になることはほとんどありません。

サービスが通信できる対象

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 はローカルソケット、IPv4、IPv6 を許可し、それ以外のすべてに対して socket() を EAFNOSUPPORT で失敗させます。これは seccomp フィルターなので、x86-64 と arm64 で動作します。現在の VPS であれば、どちらのアーキテクチャも利用できます。

破損する機能。 AF_NETLINK は、予想以上に頻繁に該当します。glibc の getifaddrs() は netlink ソケットを開き、代替手段を持ちません。Go の net.Interfaces() は同じ netlink ダンプを直接読み取ります。.NET は getifaddrs() を通じてインターフェースを列挙します。そのため、自分の IP アドレスを知りたいだけのサービスでも、OSError: [Errno 97] Address family not supported by protocol または使用言語に相当するエラーで停止します。例外は Java です。Linux 上の OpenJDK の java.net.NetworkInterface は、IPv4 には ioctl を使用し、IPv6 には /proc/net/if_inet6 を読み取るため、AF_NETLINK がなくても動作します。失敗した場合、一覧は AF_UNIX AF_INET AF_INET6 AF_NETLINK になります。これはデフォルトよりはるかに限定された状態です。AF_PACKET は raw packet capture に必要な機能であり、それ以外で付与すべき場面はほとんどありません。

IPAddressAllow=localhost を使う IPAddressDeny=any は、別の仕組みです。unit の cgroup に BPF フィルターをアタッチします。unit 単位で適用され、nft list ruleset からは見えません。そのため、同じホスト上のデータベースにだけ接続すべきサービスには適しています。一方、次にそのホストを調査する担当者にとっては、原因を把握しにくくなります。cgroup BPF をサポートしないカーネルでは、systemd は unit が IP firewall を設定しているが、ローカルシステムは BPF/cgroup firewalling をサポートしていないという内容をログに記録します。この場合、ルールは何もせずに無効になります。思い込みで判断せず、journal を確認してください。

強化した unit が起動しなくなるのはなぜか

上記の各ディレクティブによって、動作していたサービスが起動に失敗することがあります。しかも、その失敗は原因となったディレクティブとは無関係に見えることが少なくありません。手順は常に同じです。journal を読み、1 つのディレクティブだけを緩和して、再テストします。

sudo systemctl restart notes.service
systemctl status notes.service
sudo journalctl -u notes.service -n 50 --no-pager

sandbox を定義する行は次のようになります。

notes.service: Failed to set up mount namespacing: No such file or directory
notes.service: Failed at step NAMESPACE spawning /opt/notes/bin/notes: No such file or directory
notes.service: Main process exited, code=exited, status=226/NAMESPACE

226/NAMESPACE は、systemd がファイルシステムのビューを構築できなかったことを示します。そのため、バイナリはまったく実行されていません。通常の原因は、ReadWritePaths=、BindPaths=、または InaccessiblePaths= に指定したパスが存在しないことです。228/SECCOMP は、SystemCallFilter= または SystemCallArchitectures= の適用に失敗したことを示します。code=killed, status=31/SYS は別のケースです。プロセスは起動しましたが、フィルターが拒否したシステムコールを実行し、kernel に終了させられています。この切り分けでは、起動しない unit の systemd 終了コードリファレンスを開いておいてください。番号を見ると、namespace の問題かアプリケーションの問題かをすぐに判断できます。

drop-in で、1 回に 1 つのディレクティブだけを緩和してテストします。sudo systemctl edit notes.service を実行し、そこに次の 1 行だけを追加します。

[Service]
ProtectSystem=full

再起動します。サービスが起動した場合は、削除ではなく、どのディレクティブを絞り込むべきかが分かります。元に戻して strict にし、アプリケーションが実際に必要とする 1 つのパスについて ReadWritePaths= の行を追加し、再度起動します。サービスが起動しないという理由でブロック全体を削除すると、unit の保護がなくなり、誰も書いた覚えのないコメントだけが残ることになります。

サービスを使わずに sandbox をテストするには、その中で shell を実行します。

sudo systemd-run --pty -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /bin/bash

その shell 内では、touch /etc/test は Read-only file system を返し、ls /home には何も表示されません。アプリケーションから見える環境を確認する最も早い方法です。コマンドを手動で実行し、どのコマンドが失敗するかを確認できます。

エラーがまったく表示されない失敗もあります。スペルを誤ったディレクティブは警告になるだけで、サービスはそのディレクティブなしで起動します。

/etc/systemd/system/notes.service:14: Unknown key name 'ProtectSytem' in section 'Service', ignoring.

unit は実行されますが、sandbox は適用されません。その後もエラーは発生しません。これを検出するコマンドは 2 つあります。sudo systemd-analyze verify /etc/systemd/system/notes.service は同じ警告を明示的に表示し、systemctl show は実行中のサービスが実際に受け取った内容を表示します。

systemctl show notes.service -p User -p ProtectSystem -p PrivateTmp -p NoNewPrivileges

strict と記述したのに ProtectSystem=no が返る場合、その unit は想定した内容を読み込んでいません。

systemd-analyze security をチェックリストとして使う

sudo systemd-analyze security notes.service は、unit で使用できるすべてのサンドボックス設定、現在の unit が各設定をどのように扱っているか、各行の判定を出力します。引数なしで実行すると、そのサーバーで読み込まれているすべてのサービスを一覧表示します。インストール前の unit ファイルを確認する場合は、パスを指定して --offline=true を追加します。

出力は、作業項目の一覧として読みます。警告が付いた行を順に確認し、それぞれについて「このサービスにそのアクセスは必要か」と判断します。多くの場合、答えは「不要」です。その場合は該当する行を追加します。必要な場合もあります。メディアサーバーにはデバイスノードが必要です。バックアップエージェントには /home の読み取りが必要です。こうした行には常に警告が残ります。これは失敗ではなく、正しい結果です。

最後に表示される概要の数値は、その上にある行の集計です。このツールは、サービスの動作、保持しているデータ、アプリケーション自体にバグがあるかどうかを把握していません。ツールが確認するすべての行を満たしていても、その unit がサーバー上で最も弱い部分になることがあります。測定しているのはソフトウェアの露出ではなく、unit ファイルの露出だからです。自己ホスト型のパスワードマネージャーを例にすると、この違いが明確になります。Vaultwarden の unit がすべての行を満たしていても、管理者トークンとバックアップファイル が、vault の安全性を左右します。数値を追いかけると、内容を理解していないディレクティブを貼り付けることになります。そのような設定は、パッケージのアップグレード後に壊れやすく、なぜその行が必要だったのか説明できる人もいなくなります。

サンドボックスの範囲

これらのディレクティブは、サービスがアクセスできる対象を制御します。消費できるリソース量は制限しないため、完全にサンドボックス化した unit でも、ホストのすべての CPU コアとメモリを使い切る可能性があります。これは別の設定群であり、systemd サービスの CPU とメモリの制限に関するガイドで説明しています。

また、強制アクセス制御の代わりにはなりません。namespace は unit ごとに設定され、unit ファイルを記述する担当者が定義します。一方、SELinux はシステム全体に 1 つのポリシーを適用します。両者は組み合わせて使用します。どちらか一方で、もう一方が不要になるわけではありません。

最後に、対象となるのは systemd がこの unit 内で起動するプロセスだけです。サービスが socket 経由で helper daemon に処理を渡す場合、その helper はサンドボックス化されていません。同じブロックをその unit にも記述し、ファイルをそのまま信頼せず systemctl show で結果を確認してください。

FAQ

ProtectSystem=strict にすると、実際にどこが読み取り専用になりますか?

/dev、/proc、/sysを除くファイルシステム階層全体が読み取り専用になります。これらは代わりにPrivateDevices=、ProtectKernelTunables=、ProtectControlGroups=で管理されます。対象には/etc、/var、/srv、/opt、/tmpも含まれます。systemd が自動的に書き込み可能に戻す例外は、管理対象のディレクトリであるStateDirectory=、CacheDirectory=、LogsDirectory=、RuntimeDirectory=です。サービスがそれ以外の場所に書き込む場合は、明示的なReadWritePaths=エントリが必要です。読み取り専用でも読み取り自体が禁止されるわけではありません。そのため、システム上の別の場所にある Secret ファイルは、InaccessiblePaths=に記載しない限りサービスから読み取れます。

サービスが status=226/NAMESPACE で失敗するのはなぜですか?

systemd が mount namespace を構築できなかったため、実行ファイルは起動していません。その直前の journal には通常、Failed to set up mount namespacing: No such file or directoryと記録されます。ほとんどの場合、ReadWritePaths=、BindPaths=、InaccessiblePaths=のいずれかに指定したパスがディスク上に存在しません。ディレクトリを作成するか、エントリの先頭にハイフン(ReadWritePaths=-/srv/notes/uploads)を付けて、存在しない場合に systemd がスキップするようにします。パスが存在する場合は、unit 内の誤記を確認し、systemctl cat notes.serviceで実際の設定を確認してください。確認していない drop-in が行を追加している可能性があります。

PrivateTmp を使うサービスは、/tmp 経由でファイルを共有できますか?

いいえ。それが PrivateTmp の目的です。サービスには、実行中だけ存在する新しい/tmpと/var/tmpが割り当てられます。そのため、ホストの/tmpで別のプロセスが作成したソケットやファイルは、サービスから見えません。よく問題になるのは/tmp/mysql.sockのデータベースソケットです。127.0.0.1経由で接続するか、クライアントの接続先を/runにある実際のソケットへ変更してください。サービス固有の一時ファイルを確認するには、systemctl show --property=MainPID --valueからメイン PID を取得し、sudo nsenter --target <pid> --mountでその mount namespace に入ります。

sandbox 化したサービスが root なしで 443 番ポートを待ち受けるにはどうすればよいですか?

root アカウント全体ではなく、1 つの capability だけを付与します。AmbientCapabilities=CAP_NET_BIND_SERVICEをCapabilityBoundingSet=CAP_NET_BIND_SERVICEと併用して設定し、User=またはDynamicUser=yesを維持します。これにより、プロセスは低い番号のポートに bind できますが、それ以外の権限は取得しません。bounding set だけを設定するのがよくある誤りです。その場合、capability は許可されていても付与されないため、サービスはポート名を含む permission error で終了します。VPS ですでに reverse proxy を実行している場合は、サービスで127.0.0.1:8080に bind し、nginx に 443 を保持させる方法がより適切です。これなら unit に capability は不要です。

system user を作成する代わりに DynamicUser を使うべきですか?

サービスがすべてのデータをStateDirectory=、CacheDirectory=、LogsDirectory=のいずれかに保存する場合に使用してください。これは、小規模な self-hosted Web アプリケーションの多くに当てはまります。サービスの実行中だけ存在する UID と、PrivateTmp=、ProtectSystem=strict、ProtectHome=read-only、NoNewPrivileges=、RestrictSUIDSGID=、RemoveIPC=が自動的に有効になります。UID を固定する必要がある場合は、static system user を使用してください。対象となるのは、これらのディレクトリ外に所有ファイルがある場合、SSH key を使う場合、NFS mount を使う場合、または同じデータを読み取る別のプロセスがある場合です。DynamicUser=yesではデータが実際には/var/lib/private/notesに保存される点にも注意してください。このディレクトリの mode は 0700 で、所有者は root です。そのため、root 以外の backup job は失敗します。