systemd serviceから呼ぶシェルではsudoではなくsetprivを使う

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によるサービス管理の仕組みと競合しません。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。