めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Systemd入門(5) - PrivateTmpの実装を見る

Systemd入門(4) - serviceタイプUnitの設定ファイル」で、[Service]セクションのオプション「PrivateTmp」を紹介しました。実はこの他にも、ファイルシステムのセキュリティ保護を図るオプションがあります。今回は、これらオプションの紹介に加えて、それがどのような仕組みで実装されているのかを解説します。

参考資料
systemd for Administrators, Part VI - Changing Roots
systemd for Administrators, Part XII - Securing Your Services

ファイルシステム保護オプション

serviceタイプのUnitについて、[Service]セクションで次のようなオプションを指定できます。(念のため、PrivateTmpも再掲しています。)

オプション 説明
ReadOnlyDirectories 指定のディレクトリ以下をReadOnlyモードにする
InaccessibleDirectories 指定のディレクトリ以下をアクセス不可にする
PrivateTmp このサービス専用の/tmpと/var/tmpを用意する
RootDirectory 指定のディレクトリにchrootする

最後の「RootDirectory」は、昔ながらのchroot環境を提供するものです。デフォルトでは、ExecStart、ExecReload、ExecStop、ExecStartPre/ExecStartPost、ExecStopPostなど、すべてのコマンドがchrootした状態で実行されます。一方、「RootDirectoryStartOnly=true」を指定すると、ExecStartで指定したコマンドのみがchrootした状態で実行されます。

しかしながら、chroot環境でデーモンを実行するには、そのデーモンが必要とするファイル(ライブラリなど)を一式そろえたディレクトリを用意する必要があり、事前準備が面倒でした。そこでsystemdでは、chrootする代わりに、指定のディレクトリをReadOnlyにしたり、アクセス不可にした状態でサービス(を提供するプロセス)を実行する機能を提供しています。これにより、該当サービスのプロセスに見せたくないディレクトリ(/homeなど)を隠すことができます。

実装

これらの実装は、比較的単純で、「ReadOnlyDirectries」の場合は、指定のディレクトリを自分自身にバインドマウントした上で、マウントオプションにROモードを指定してリマウントします。次のコマンドと同等の操作になります。

# mount --bind /readonly /readonly
# mount --bind -o remount,ro /readonly

同じく、「InaccessibleDirectories」の場合は、指定のディレクトリを「/run/systemd/inaccessible」というアクセス権のないディレクトリにバインドマウントしてしまいます。コマンドでやるなら次の通り。

# ls -ld /run/systemd/inaccessible
d---------. 2 root root 40  9月 23 04:52 /run/systemd/inaccessible
# mount --bind /run/systemd/inaccessible /noaccess

「ふーん。なるほど」と、ここで納得した貴方! 大事なことを忘れていませんか?

これは、あくまでsystemdから起動するプロセスに対して設定するべき内容です。普通に上記の操作をすると、システム上のすべてのプロセスに対して、ファイルシステムの状態が変更されてしまいます。ここで登場するのが、「ファイルシステムのnamespace」(以降は単に「namespace」と表記)の機能です。これを使うと、プロセスごとにファイルシステムの構成状態(マウント状態)を分離することができます。systemdでは、この機能によって、起動プロセスから見えるファイルシステムの構成状態(マウント状態)のみを変更しています。

namespaceの使い方は意外と簡単で、「unshare(CLONE_NEWNS)」というシステムコールを呼ぶだけです。次のセクションで実例を紹介します。

PrivateTmpを再現してみる

PrivateTmpの機能も同様に、namespaceを利用しています。「/tmp/systemd-private-XXXXXX」(XXXXXXはランダム)という空のディレクトリを用意して、該当プロセスに対しては、/tmpをここにバインドマウントします。これで、このプロセスに対して、/tmpの中身は、/tmp/systemd-private-XXXXXXになります。/var/tmpも同様に別の空ディレクトリにバインドマウントします。

これと同じことを再現したサンプルプログラムが次になります。実際のsystemdのソースでは、「src/core/namespace.c」の「setup_namespace()」にあたります。

private_tmp.c

#define _GNU_SOURCE
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mount.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <linux/fs.h>

void main() {
        if (unshare(CLONE_NEWNS) < 0)  // (*1)namespaceを分離する
                exit (-errno);

        if (mount(NULL, "/", NULL, MS_SLAVE|MS_REC, NULL) < 0)  // (*2)このnamespaceでの変更を元のnamespaceに反映しない
                exit (-errno);

        mkdir ("/tmp/mytmp", 0700);
        if (mount("/tmp/mytmp", "/tmp", NULL, MS_BIND|MS_REC, NULL) < 0) // (*3)/tmpを/tmp/mytmpにバインドマウント
                exit (-errno);

        if (mount(NULL, "/", NULL, MS_SHARED|MS_REC, NULL) < 0)  // (*4)本文の説明を参照
                exit (-errno);

        execl("/bin/sh", "/bin/sh", NULL);
}

これを実行すると、/tmpが/tmp/mytmpにバインドマウントした状態でシェルが起動します。このシェルから/tmp以下にアクセスすると、実際には、/tmp/mytmpにアクセスすることになります。このシェルから/tmp以下にファイルを作って、別の端末からログインすると、同じファイルが/tmp/mytmpにある事が分かります。

また、このシェルからは、次のように/tmpがバインドマウントされているエントリが確認できますが、別の端末からログインした方では、このエントリは見えません。これからもファイルシステムのnamespaceが分離されていることが分かります。

# mount | grep /tmp
/dev/vda1 on /tmp type ext4 (rw,noatime,seclabel,data=ordered)

プログラム内で、具体的にnamespaceの分離を行う部分をコメントに記してあります。(*1)でnamespaceを分離した後に、(*2)で/以下に「MS_SLAVE」というマークをつけています。これは、このnamespaceで行った変更(追加のバインドマウントなど)を元のnamespaceには反映しないという指定です。これによって、(*3)のバインドマウントは、このプロセスのnamespaceのみで実施されて、他のプロセスからは見えなくなります。

最後に(*4)で/以下に「MS_SHARED」というマークをつけています。これは、意図としては「MS_SLAVE」の指定をはずして、このnamespaceで、さらに行った変更(追加のバインドマウントなど)は、元のnamespaceにも反映されるようにするものです。

・・・・が!

今のカーネルの実装では、この処理は意味を持ちません(これを実行しても「MS_SLAVE」の指定ははずれない)。つまり、このプロセスが何らかの必要性があって追加のバインドマウントを行った場合、それは他のプロセスからは見えない状態となります。ということに、この記事を書いていて気づいたので、BZをオープンしておきました。

Bug 1010669 - bind-mount marked as MS_SLAVE cannot be converted to MS_SHARED.

PrivateTmp利用時の注意点

上記で説明したように、PrivateTmpを利用してプロセスを起動すると、そのプロセスが追加のバインドマウントを行っても、それは他のプロセスからは見えません。実は、これに起因する問題を経験したことがあります。

Bug 881741 - Restarting l3_agent causes Stderr: 'RTNETLINK answers: Invalid argument\n'

OpenStackのQuantum/Neutronでは、network namespace(netns)という機能を使っているのですが、この機能は、新しいnetnsを作成すると、/var/run/netns/というバインドマウントを作成して、このnetnsの情報を他のプロセスに公開します。ところが、Quantum/NeutronをPrivateTmp=trueで実行していると、上記の理由により、このバインドマウントは他のプロセスから参照できず、netnsの機能がうまく働きません。

一般論として、「ReadOnlyDirectories/InaccessibleDirectories/PrivateTmp」を使用した場合、該当プロセスのファイルシステムのnamespaceは他のプロセスから分離され続けており、このプロセスが実施したバインドマウントは、他のプロセスに見えないということを覚えておきましょう。