Red Hatの森若です。
自分でsystemdのservice unitを作るときに、起動用のいくつかのコマンドを記述したシェルスクリプトを呼ぶ事は(理想的ではないですが)あるかと思います。
今回はこの場合に、sudoを利用するとまずい理由を説明して、かわりにsetprivを使うほうがよいという話です。
例題用のservice
sudoによるまずい動作を確認するためのできるだけ単純な例として、hoge.service を用意します。
/opt/hoge/hoge.sh
#!/bin/bash sudo -u moriwaka sleep 5000
/etc/systemd/system/hoge.service
[Unit] Description=hoge [Service] Type=oneshot ExecStart=/opt/hoge/hoge.sh
実行してみる
systemctl のデフォルトだとhoge.shの実行終了を待ってしまうので、 --no-block オプションをつけて実行します。
# systemctl --no-block start hoge.service
systemctl statusで状態を見てみます。ExecStartのスクリプトが実行中なのでactivatingなのは予想通りですが、実行中のはずの sleep がCGroupの中になく、bashだけが存在していることがわかります。
# systemctl status hoge
● hoge.service - hoge
Loaded: loaded (/etc/systemd/system/hoge.service; static)
Active: activating (start) since Wed 2022-09-21 13:42:32 JST; 8min ago
Main PID: 4110436 (hoge.sh)
Tasks: 1 (limit: 37738)
Memory: 1.7M
CPU: 10ms
CGroup: /system.slice/hoge.service
└─ 4110436 /bin/bash /opt/hoge/hoge.sh
Sep 21 13:42:32 turtle systemd[1]: Starting hoge.service - hoge...
Sep 21 13:42:32 turtle sudo[4110437]: root : PWD=/ ; USER=moriwaka ; COMMAND=/usr/bin/sleep 5000
cgroupの様子をみるときにはsystemd-cglsを使います。sudoとsleepが、サービスに対応する /system.slice/hoge.service ではなく /user.slice/user-1000.slice/session-c4.scope 内で実行されていることがわかります。
これはsudoで指定したユーザ(id: moriwaka, UID: 1000)のセッション「c4」が作成され、その中で実行されていることを示しています。
-.slice ├─user.slice (#1144) │ → trusted.invocation_id: 1041b6b580784b47b41c08d97ab05e23 │ └─user-1000.slice (#5335) │ → trusted.invocation_id: a9169cd85293489e9697b4d337962a86 │ ├─user@1000.service … (#5423) (中略) │ ├─session-c4.scope (#1663079) │ │ → trusted.invocation_id: d257de8a94d54a6eac713a35e228dc27 │ │ ├─ 4110437 sudo -u moriwaka sleep 5000 │ │ └─ 4110439 sleep 5000 (以下略)
systemctlでscopeの状態を表示させるとこのように表示されます。 sudoがpam_unixを経由してセッションを作成した旨のログが表示されています。
# systemctl status session-c4.scope
● session-c4.scope - Session c4 of User moriwaka
Loaded: loaded (/run/systemd/transient/session-c4.scope; transient)
Transient: yes
Active: active (running) since Wed 2022-09-21 13:42:32 JST; 13min ago
Tasks: 2
Memory: 392.0K
CPU: 2ms
CGroup: /user.slice/user-1000.slice/session-c4.scope
├─ 4110437 sudo -u moriwaka sleep 5000
└─ 4110439 sleep 5000
Sep 21 13:42:32 turtle systemd[1]: Started session-c4.scope - Session c4 of User moriwaka.
Sep 21 13:42:32 turtle sudo[4110437]: pam_unix(sudo:session): session opened for user moriwaka(uid=1000) by (uid=0)
ここでpstreeを実行すると、プロセスの親子関係としてはsystemd→hoge.sh→sudo→sleep のように意図どおりであることがわかります。
# pstree
systemd─┬─ModemManager───3*[{ModemManager}]
├─NetworkManager───2*[{NetworkManager}]
(中略)
├─hoge.sh───sudo───sleep
(以下略)
別のcgroupだと何がまずいのか?
起動に成功しているが、cgroupが別であることはわかりました。 これで問題が発生するのはどんなときでしょうか?
- systemdのhoge.serviceがプロセスの監視をできていない。systemdがサービスの名前からサービス本体のプロセスを発見できなくなります。そのため systemctl kill や、(この例にはありませんが)Restartディレクティブなどを使おうとするとうまくいきません。daemonizeするプログラムだと、bashを停止しても動作しつづけますから管理ができなくなります。
- ユーザセッション(session-c4.scope)とサービス(hoge.service)の間に前後関係などは定義されません。システム全体のシャットダウン時には(この例では定義していませんが)サービス終了コマンドを用意してもそれを使わずにデフォルトの終了方法(SIGTERM送信のあとタイムアウトまちしてからSIGKILL送信)で終了される場合があります。
- service unitでリソース設定や権限管理を行っていても、その制限がうまく反映されない場合があります。
- 関連するログがユーザセッションに所属するのでjournalをサービスで検索すると見逃す場合があります。
対策はsetprivコマンド
sudoはセッションを作るのでまずいことがわかりました。
このようなシェルスクリプトでsudoが利用されるのは典型的にはrootからサービス用ユーザへの切り替えのためですから、 UID, GIDの切り替えだけを行いセッションについては何もしない(PAMをつかわない)しくみがあると都合がいいです。 まさにそのようなコマンドがsetprivコマンドです。setprivコマンドはutil-linuxパッケージに含まれています。
さっそく /opt/hoge/hoge.sh のsudo をsetprivに置き換えます
/opt/hoge/hoge.sh
#!/bin/bash setpriv --reuid=1000 --regid=1000 --init-groups sleep 5000
今動いているsleepを止めます。(この例だとdaemonizeしていないので、systemctl stopでbashへシグナルを送ることでsleepを終了できます。)
# systemctl stop hoge.service
再度 hoge.service を起動してみます。今度は cgroup /system.slice/hoge.service の中でsleepが実行されていることがわかります。
# systemctl start --no-block hoge.service
# systemctl status hoge.service
● hoge.service - hoge
Loaded: loaded (/etc/systemd/system/hoge.service; static)
Active: activating (start) since Wed 2022-09-21 14:38:47 JST; 9s ago
Main PID: 4115616 (hoge.sh)
Tasks: 2 (limit: 37738)
Memory: 540.0K
CPU: 5ms
CGroup: /system.slice/hoge.service
├─ 4115616 /bin/bash /opt/hoge/hoge.sh
└─ 4115617 sleep 5000
Sep 21 14:38:47 turtle systemd[1]: Starting hoge.service - hoge...
期待どおりUID, GIDが変更されていることを確認します。
# cat /proc/4115617/status |grep [UG]id Uid: 1000 1000 1000 1000 Gid: 1000 1000 1000 1000
このようにsetprivを利用してUID, GIDを変更するとセッションを作成しないので、systemdによるサービス管理の仕組みと競合しません。