鍵を配るか、部屋を仕切るか、それが問題だ
안녕하신게라!パナソニック コネクト株式会社クラウドソリューション部の加賀です。
前回「プロセスとメモリの基本の"キ"」では、1台のサーバの中で何百ものプロセスが同時に動く仕組みを、厨房に立つシェフにたとえて追いかけました。仮想メモリのおかげで、各プロセスのまな板(メモリ領域)は互いに手を出せないよう隔離されている、と説明しました。
最後にこんな宿題を残していました。まな板は守られていても、プロセスが触れるファイルシステムや持っている権限、依存するライブラリのバージョンまでは、OS全体で共有されたまま。つまり「1つのプロセスが乗っ取られたら、そこを足がかりにサーバ全体まで持っていかれるのでは?」という不安が残ります。
この不安に対して、Linuxが数十年かけて出してきた答えは、大きく三つの方向に整理できます。強い権限をどう貸し借りするか(鍵を配る)、プロセスに見える世界そのものをどう狭めるか(部屋を仕切る)、そして使える資源にどう上限を課すか(部屋の広さを決める)。今回は、root・su・sudo という鍵の貸し方から、chroot という素朴な仕切り、カーネル自身が部屋を作り出すNamespace、広さを決めるcgroupsまで、隔離の歴史と基本的な考え方を押さえていきましょう。次回登場するコンテナは、この歴史の到着点にほかなりません。
この記事のコマンドについて
掲載しているコマンドと出力は Ubuntu 22.04(カーネル 5.15、cgroup v2)で確認したものです。ディストリビューションやバージョンによって、パスやファイルサイズ、出力の書式は変わります。
また chroot・unshare・cgroups の操作は、後述する神の力を借りるため、手元を間違えると自分のシェルや稼働中のサービスを巻き込みます。本番サーバではなく、WSL2 や使い捨ての仮想マシンで試してください。
すべての頂点に立つ、rootという神様
Linux(Unix)はそもそも「1台の大きなコンピュータを大勢で共同利用する(マルチユーザ)」ことを前提に設計されています。だからこそ、誰が何に触れてよいかを定める権限(パーミッション)が土台になります。
一般ユーザ(例えば app-user)は、自分のホームディレクトリのファイルは好きに編集できても、システムの根幹をなす設定ファイルには手を出せません。ここで身を守っているのが、ファイルごとに定められた「読み・書き・実行」の許可設定(パーミッション)です。前回、プログラムは「ディスクに置かれただけのただのファイル」だと整理しましたが、そのファイル一つひとつに「誰が読めて、誰が書けて、誰が実行できるか」が刻まれていて、この素朴な仕組みがシステム全体の守りにもそのまま使われています。
# 自分が何者か(UIDと所属グループ)を確認する
$ id
uid=1000(app-user) gid=1000(app-user) groups=1000(app-user)
# ユーザ一覧の定義ファイルは、rootだけが書ける
$ ls -l /etc/passwd
-rw-r--r-- 1 root root 2841 Jul 2 09:15 /etc/passwd
# ↑所有者(root)は読み書き、それ以外は読むだけ
$ echo "DANGER" >> /etc/passwd
-bash: /etc/passwd: Permission denied
ところが、この仕切りをすべて飛び越えて何でもできてしまう、絶対的な権限を持つユーザがただ一人だけ存在します。それが root(スーパーユーザ)、いわばシステムの神様です1。
初期の管理者は、設定を変えるたびに root でログインして作業していました。しかし神の力は諸刃の剣で、たった一度のタイプミス(うっかり叩いた rm -rf / など)がシステム全体を消し飛ばす。そんな危うさと常に隣り合わせだったのです。
必要なときだけ、力を借りる
そこで浸透したのが、「普段は非力な一般ユーザとして安全に過ごし、どうしても必要なときだけ神の力を借りる」という作法です。普段は神様に近寄らず、困ったときだけ頼る。いわゆる困った時の神頼みで構わない、というわけです。代表的な頼み方を見ていきましょう。
suで、いまだけ神に変身する
su(Substitute User)は、「いまだけ別のユーザ、多くの場合は root に変身する」コマンドです。かつては su - で root のパスワードを打ち、作業が済んだら exit で元のユーザに戻る、という運用が一般的でした。
$ su - # -付きはログインシェル。環境変数もPATHもroot本人のものに入れ替わる
Password: # ← rootのパスワードを入力
# id
uid=0(root) gid=0(root) groups=0(root)
# exit # 用が済んだら、すぐ返す
$
ただ、この方法には大きな穴があります。root に化けるには root のパスワードが要る。つまり運用担当者全員に root のパスワードを配って回ることになります。誰が実際に操作したのかも分からず、担当者が一人抜けるたびにパスワードを変えて全員へ再周知……と、なかなかの綱渡りです。
【コラム】一般ユーザなのに /etc/shadow を書き換えられる謎(SUIDビット)
自分のパスワードを変える passwd コマンド。
パスワードのハッシュが保存されている /etc/shadow は、書き込めるのが root だけという超重要ファイルです(Debian/Ubuntu系なら -rw-r----- 1 root shadow で、読めるのも root と shadow グループに限られます)。
なのになぜ、一般ユーザが sudo も付けずに passwd だけで書き換えれるのでしょうか?
種明かしは、実行ファイルの権限をよく見ると分かります。
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Nov 24 2022 /usr/bin/passwd
# ↑本来 x があるはずの場所が s になっている
この s が SUID(Set User ID)ビットです。SUIDが立った実行ファイルは、誰が起動しても「ファイルの所有者(ここでは root)の権限で動く」という特殊な性質を持ちます。つまり passwd は、実行中だけ神の力を借りて /etc/shadow を書き換え、用が済んだら終了する。「必要な処理だけを、あらかじめ安全に作り込んだプログラムに任せる」という、これも権限の貸し方の一種です。
便利な反面、SUIDが立ったプログラムに脆弱性があれば、そこはroot権限を奪う最短ルートになります。攻撃者が侵入後にまず find / -perm -4000 で身に覚えのないSUIDファイルを探す、というのは定番の手口です。逆に言えば守る側も同じコマンドで棚卸しできるので、定期的に一覧を取って前回との差分を見るのが基本の防御になります。加えて、ユーザが書き込めるパーティションは nosuid オプション付きでマウントする、コンテナなら --security-opt no-new-privileges を付けて昇格そのものを封じる、といった打ち手も効きます。
なお「自作のシェルスクリプトにSUIDを立てれば楽できるのでは」と思っても、そうは問屋が卸しません。Linuxカーネルは #! で始まるスクリプトのSUID/SGIDビットを実行時に無視する仕様です(chmod u+s 自体は成功してビットも立つので、なおさら紛らわしい)。インタプリタへ渡るまでのわずかな隙を突く古典的な攻撃を封じるための措置で、execve(2) に明記されています。
つまり現実に危ないのは「SUIDを立てたスクリプト」ではなく、それを実現しようとして書かれるsetuidラッパ(C言語などで作ったバイナリ)や、書き換え可能なスクリプトを sudo で丸ごと許可してしまう設定のほうです。
攻撃側の手口から入りましたが、裏を返せば守る側の手順書でもあります。SUIDの一覧を定期的に取り、増減を追い、そもそも数を増やさない。楽をするための近道として自作バイナリにSUIDを立てるのは、避けるのが賢明です。
sudoで、必要な分だけ記録を残して借りる
su の不便を解決したのが、おなじみの sudo(su "do"=substitute user do)です。「特定の一般ユーザに、その人自身のパスワードだけで、一時的に root 権限でコマンドを実行させる」仕組みで、しかも誰が・いつ・どこで・何を実行したかがログに残ります2。
さらに sudo は、渡す権限をコマンド単位で絞れます。設定は /etc/sudoers(と /etc/sudoers.d/ 配下)に書きますが、文法を間違えると誰も昇格できない詰み状態になるため、必ず構文チェック付きの visudo で編集するのがお作法(先人の知恵)です。許可するコマンドは絶対パスで書く必要があるので、command -v systemctl で実機のパスを確かめてから書くのが確実です(/bin と /usr/bin が統合済みの環境が主流になっています)。
# 「この人にはNginxの再起動だけ許可する」例(visudoで編集)
app-user ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
# 自分に何が許可されているかは、本人が確認できる
$ sudo -l
User app-user may run the following commands on web01:
(root) NOPASSWD: /usr/bin/systemctl restart nginx
必要な分だけを渡す。これがセキュリティの基本原則、最小権限の原則(Least Privilege)の実装そのものです。神の力は、必要なときに必要なだけ借りて、すぐ返す3。
【コラム】ポート80番は誰でも開けるわけじゃない「特権ポート問題」
「必要なときだけ借りる」の実例をもうひとつ。Webサーバ(Nginxなど)を起動するとき、sudo systemctl start nginx のように root 権限が要ることがあります。これはLinuxの仕様で、1024番未満のポート(特権ポート)は特別な権限がないと開けないというルールがあるためです。TCP/UDPの回で「ウェルノウンポート(0〜1023)」として登場した80番(HTTP)や443番(HTTPS)が、まさにこの範囲にあたります。厳密に言えば条件は「root権限」そのものではなく、先ほどの脚注で触れた CAP_NET_BIND_SERVICE というケイパビリティを持っているかどうかで、net.ipv4.ip_unprivileged_port_start(既定値1024)を下げれば一般ユーザが開けるポートの範囲を広げることもできます。
とはいえ、root権限のままWebサーバを動かし続けるのは危険です。万一脆弱性を突かれれば、そのまま神の権限を奪われてしまう。そこで賢く作られたWebサーバは、起動して80番を開いた直後に、自分の権限を www-data や nginx といった非力な一般ユーザへ格下げ(Drop privileges)してから走り続けるという、涙ぐましい安全策をとっています。ps を眺めると、親プロセスだけ root で、実際にリクエストを捌く子プロセスは一般ユーザになっているのはこのためです。
$ ps -eo user,pid,ppid,cmd | grep nginx
root 1234 1 nginx: master process /usr/sbin/nginx
www-data 1235 1234 nginx: worker process # ← 権限を捨てて働いている
先ほど脚注で触れたケイパビリティを使い、sudo setcap cap_net_bind_service=+ep /usr/sbin/nginx のように「このプログラムには特権ポートを開く力だけ与える」として、よりお上品な渡し方もできます。まさに、必要な最小権限だけを貸し出す発想です。
見える世界ごと書き換える、chroot
権限のおかげで、プロセスは「自分に許されたファイルしか触れない」状態になりました。しかし、これでもまだ足りません。触れはしなくても、他人のファイルやシステムの全体像が「見えてしまう」のは、隔離としては不十分です。見えてしまえば、そこが次の攻撃の足がかりになりかねません。
そこで、「プロセスから見える『ルートディレクトリ(/)』の位置を、システム本来の / ではなく、別のディレクトリ(例:/var/jail/)だと思い込ませればいいのでは?」という発想が出てきます。
この発想にぴったり当てはまるシステムコールが、chroot(Change Root)です4。プロセスを特定のディレクトリに囲い込み(この状態を chroot jail と呼びます)、「あなたの世界の頂点はここですよ」と信じ込ませます。プロセスから見れば /var/jail/ が世界の / になり、その外側は存在しないも同然になる。
面白いのは、jail という物々しい名前に反して、実態は無人島へキャンプに行くイメージな点です。必要なものは全て自分で持ち込む必要があり、シェルを使うなら bash の実行ファイルだけでなく、依存する共有ライブラリも動的リンカも全て揃えて持ち込まないといけません。中で ls を叩きたければ、ls も持ち込みです。見事に大荷物になります。
もっとも、この島は本土から目と鼻の先です。その微妙な距離感は、後のコラムで触れます。
# 島を用意し、使いたいコマンドと依存ライブラリを担ぎ込んでから移り住む
$ sudo mkdir -p /var/jail/{bin,lib/x86_64-linux-gnu,lib64}
$ sudo cp /bin/bash /bin/ls /var/jail/bin/
$ ldd /bin/bash /bin/ls # 何が必要かを調べ、ldd が示すパスと同じ構造で配置する
$ sudo cp /lib/x86_64-linux-gnu/{libtinfo.so.6,libc.so.6,libselinux.so.1,libpcre2-8.so.0} \
/var/jail/lib/x86_64-linux-gnu/
$ sudo cp /lib64/ld-linux-x86-64.so.2 /var/jail/lib64/ # 動的リンカを忘れると何も起動しない
$ sudo chroot /var/jail /bin/bash
bash-5.1# ls / # 本物の / ではなく、島に持ち込んだ物しか見えない
bin lib lib64
持ち込むライブラリの顔ぶれとパスは環境で変わるので、ldd の出力に並んだものを、そのままのパス構造で島の中へ配置するのがコツです。この「動かしたい一式を全部持ち込む」という面倒さは、あとでコンテナイメージの話につながる伏線でもあります。
このファイル隔離の考え方が、のちのサンドボックス(砂場)の原点になりました。
いまでもOSのインストールやレスキュー作業で chroot は現役ですし、FTPサーバなどが利用者を自分のホームディレクトリに閉じ込める方法にも使われています。
【コラム】chroot の限界と「島抜け(chroot escape)」
画期的だった chroot ですが、完全なセキュリティ機構ではありませんでした。やっていることは突き詰めると「見える / の位置を差し替えているだけ」。しかも chroot は「ルートの位置」は変えても、「いま作業しているディレクトリ(カレントディレクトリ)」までは動かしてくれません。
この穴を突くのが典型的な島抜け(chroot escape)です。root 権限(厳密には CAP_SYS_CHROOT)を持ったままのプロセスが、島の内側にもう一段サブディレクトリを作り、chroot(2) を自分で呼んでそこへ移ると、作業ディレクトリだけが新しい島の"外"に取り残されます。あとはそこから .. を繰り返して上位(浅い方)へよじ登っていくだけで、あっさり本物の / にたどり着けてしまうのです(chroot コマンド経由なら新しいルートへ移動してくれるので、これはシステムコールを自前で呼ぶプログラムの話です)。
先ほど「本土から目と鼻の先の無人島」と書いたのは、この話です。島に置いてきたつもりでも、rootのままの住人は自前のボートで本土まで漕ぎ戻れてしまう。つまり chroot は「root権限を持った相手」を閉じ込める前提では作られていない。あくまで、権限を落としたプロセスの視界を狭める仕切りにすぎませんでした。
無人島ではなく、マジックミラーの部屋に(Namespace)
「パスを誤魔化すだけでは足りない。ファイルシステムも、ネットワークも、プロセス空間も、OSのレベルからもっと根本から隔離しなければ」。この切実な願いに、Linuxカーネル自身が答えを出しはじめます5。それが Namespace(名前空間) です。
chrootが「見せる / を差し替える」だけだったのに対し、Namespaceはプロセスから見えるリソースの世界を、種類ごとにまるごと作り替えます。
| Namespace | 隔離するもの | 中から見える世界 |
|---|---|---|
| mnt(Mount) | ファイルシステムのマウント状態 | 自分専用のディレクトリ構造だけ(ホストの構造は見えない) |
| pid(PID) | プロセスIDの空間 | 自分のグループのプロセスだけ(他は存在しないも同然) |
| net(Network) | NIC・IPアドレス・ポート・経路表 | 自分専用のNICとポート(ホストの占有状況と無関係) |
| uts(UTS) | ホスト名・ドメイン名 | 自分で勝手に名乗ったホスト名 |
| ipc(IPC) | プロセス間通信(共有メモリなど) | 自分のグループ内の共有メモリだけ |
| user(User) | UID/GIDの対応関係 | 「中のroot」は、外では単なる一般ユーザ |
| cgroup | cgroupの階層の見え方 | 自分の所属cgroupが根っこに見える |
| time | 起動時刻や単調増加時計 | ホストとは別の起動時刻・uptime |
言葉で読むより、触ってみるのが早いです。unshare コマンドを使えば、その場でホスト名とPID空間だけを隔離したシェルが作れます。
# ホスト名とPID空間を隔離したシェルを、その場で作る
$ sudo unshare --uts --pid --fork --mount-proc /bin/bash
# hostname box01 # 中のホスト名を勝手に変えてみる
# ps -ef # ※以下、出力は列を抜粋
UID PID PPID CMD
root 1 0 /bin/bash # ← 自分がPID 1。他のプロセスは一切見えない
root 9 1 ps -ef
# exit
$ hostname # ホスト側は何も変わっていない
host01
前回、「PID 1 は init(systemd)で、プロセスツリーの根っこ」という話をしました。Namespaceの中のプロセスは、「我こそが世界の根っこだ」と信じ込んでいます。実際には同じOSの上に何百ものプロセスがひしめいているのに、本人にはまったく見えない。無人島に置いてくるというより、マジックミラーの部屋に入ってもらうイメージです6。
面白いのは、その部屋の在り処もまたファイルとして見えている点です。ls -l /proc/$$/ns/($$ は自分自身のPID)を叩けば、いま自分が属しているNamespaceが mnt や pid といった名前で並んでいます。nsenter は、このファイルを指定して「この部屋に入れてくれ」とカーネルに頼むコマンドにすぎません。
$ ls -l /proc/$$/ns/
lrwxrwxrwx 1 app-user app-user 0 Aug 3 10:21 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 app-user app-user 0 Aug 3 10:21 net -> 'net:[4026531840]'
lrwxrwxrwx 1 app-user app-user 0 Aug 3 10:21 pid -> 'pid:[4026531836]'
# ↑角括弧の中はNamespaceの識別番号。この番号が同じなら「同じ部屋の住人」
そして、コラムで紹介した chroot と root の組み合わせで島抜けできてしまう弱点も、mount Namespace と pivot_root(古い / そのものを外して捨てる仕組み)の組み合わせで塞がれました。見せるパスを差し替えるのではなく、マウントの世界ごと作り替えてしまうので、cd .. でよじ登る先がもう存在しないのです。
資源の食い合いを止める(cgroups)
ただ、部屋を仕切って見えなくしただけでは、まだ足りません。前回のOOM Killerの話を思い出してください。ある住人がメモリを食い尽くせば、OS全体を巻き込んで他の住人まで道連れにしてしまいます。見えていなくても、まな板と調理台は共有のままなのです。
これを塞ぐのが、Googleのエンジニアが「process containers」として開発を始め、2008年のカーネル 2.6.24 で取り込まれた cgroups(control groups) です。プロセスのグループごとに「使えるメモリは512MBまで」「CPUは1コアの30%まで」と上限を課し、誰か一人の暴飲暴食が全体に波及しないようにします。
仕組みは仰々しいですが、触り方は驚くほど素朴です。cgroupsの操作口は /sys/fs/cgroup 以下に疑似ファイル(ディスク上には存在せず、カーネルがその場で見せているだけのファイル)として並んでいて、上限を課すのもプロセスを放り込むのも、そこへ書き込むだけで済みます。
先ほどNamespaceを /proc/<PID>/ns/ のファイルとして覗いたのと、まったく同じ発想です。以前「UNIX哲学に学ぶ「組み合わせる」思考法」で挙げたすべてはファイルである(Everything is a file) という原則が、隔離やリソース制御という現代的な仕組みでも、そのまま貫かれているわけです。
# 子グループでメモリ上限を扱えるようにする(環境によっては既に有効)
$ echo +memory | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 「メモリは50MBまで」のグループを作り、いまのシェルを放り込む
$ sudo mkdir /sys/fs/cgroup/demo
$ echo 50M | sudo tee /sys/fs/cgroup/demo/memory.max
$ echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs
# この配下でひたすらメモリを食わせてみる
$ tail /dev/zero
Killed
$ grep oom_kill /sys/fs/cgroup/demo/memory.events # ※memory.eventsは5行出るので抜粋
oom_kill 1
# 後片付け。シェルを元のグループへ戻してから、空になったグループを消す
$ echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs
$ sudo rmdir /sys/fs/cgroup/demo
tail が仕留められずに粘るときは、Ctrl-C で止めてください(swapに逃がせる構成だと、上限に触れてもしばらく生き延びます)。後片付けを忘れるとグループが残り続けるので、rmdir まで含めてひと続きの作業にしておくのが安全です。
なお、上限に触れた瞬間に即kill、というわけではありません。まずカーネルがページの回収(reclaim)を試み、swapが使える構成ならswapへ逃がし、それでも足りないと判断された時点で初めてOOM Killerが動きます。前回「メモリを回収してもこれ以上割り当てられないとカーネルが判断したときに発動する」と書いたのと、同じ振る舞いがグループ単位で起きるわけです。
そして大事なのは、OOM Killerの被害がグループの中で止まることです。上限を超えたグループのプロセスだけが仕留められ、ホストや隣のグループは無傷で走り続ける。前回「メモリ設計が甘ければ本番DBが突然死する」というトラウマ話をしましたが、cgroupsは影響範囲を仕切りの内側に閉じ込めてくれます7。
おわりに
今回は、Linuxが「権限の貸し方」「見え方の仕切り方」「資源の分け方」をどう積み上げてきたか、その歴史をひと続きにたどりました。
root はシステムの神様ですが、その力を常用するのは危険です。だから su と sudo で必要なときだけ借り、passwd の SUID のように限られた処理だけに託す。そして視界を狭める chroot は、root権限が残っていれば島抜けされてしまう。その反省の上に、カーネル自身が Namespace で見える世界ごと作り替え、cgroups で使える資源に上限を課すようになりました。
権限や隔離もまた、普段は意識されないまま足元を支えている基本の"キ"です。
id で自分の立場を確かめ、ls -l で誰が何に触れられるかを読み、sudo -l で借りられる力を確認する。隔離の側なら lsns と systemd-cgls で、部屋と広さを覗く。この道具立てを知っているかどうかで、「なぜかPermission deniedになる」「なぜかポートが開けない」「なぜかメモリ上限で落ちる」といった場面での見通しがまるで違ってきます。
さて、独立した箱を作るための部品は、これでカーネルに揃いました。ところが unshare や /sys/fs/cgroup を手で組み合わせて安全な箱を仕立てるのは職人芸で、しかも無人島の荷造りと同じように、中で動かす一式は依然として手で持ち込まなければならない。
この部品を誰でも扱える形に包み、中身をまるごと配れるようにしたら、何が起きるのか。
次回「コンテナとDockerの基本の"キ"」へ続きます。
お断り
記事内容は個人の見解であり、所属組織の立場や戦略・意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。
-
厳密には、Linuxが特別扱いしているのは「root」という名前ではなくUID 0 という数字のほうです。名前を
adminに変えてもUIDが0なら神のままですし、逆にUID 0のユーザを増やせば神が二人になります(監査上おすすめはしません)。ちなみに現代のカーネルでは、この「神の力」はCAP_NET_BIND_SERVICE(特権ポートを開く)やCAP_SYS_ADMIN(システム管理全般)といったケイパビリティという単位に細分化されていて、「root権限まるごと」ではなく必要な一片だけを渡すこともできます。 ↩ -
ログの置き場所はディストリビューションによって異なり、Debian/Ubuntu系なら
/var/log/auth.log、RHEL系なら/var/log/secure、journaldに集約されている環境ならjournalctl -t sudo(journalctl _COMM=sudoでも同じ)で追えます。障害対応で「誰がこの設定を変えたのか」を突き止める初動は、だいたいココから始まります。 ↩ -
ここまで見てきた権限は、ファイルの所有者が許可を自由に決められる任意アクセス制御(DAC)です。実際のサーバにはもう一段、管理者が定めたポリシーで「rootであっても、このプロセスにこの操作は許さない」と強制する強制アクセス制御(MAC)の層が乗っています。RHEL系の SELinux、Ubuntu系の AppArmor がそれで、DACを掻い潜られてもMACで止める二段構えです。今回は踏み込みませんが、
ls -Zでファイルに付いたラベルを、sudo aa-statusやgetenforceで自分の環境の状態を覗けます。 ↩ -
順番として正確に書くと、
chrootはセキュリティのために作られた仕組みではありません。1979年のVersion 7 Unixの開発中に登場し、初期の用途はビルドやインストールの検証環境を切り替えることでした(BSDでは1982年に、インストーラとビルドシステムの動作確認のために持ち込まれたという記録が残っています)。「jail」という呼び名も後付けで、1991年にBill Cheswickが侵入者を観察するハニーポットにchrootを使った事例が早いものとして知られています。いまでもDebian/Ubuntuのパッケージビルドや、RPM系のmockが、依存関係の混入を防ぐ目的でchrootを使っています。つまり「隔離に転用してみたら意外と使えた」という順番で、だからこそ後述する限界も残っているわけです。 ↩ -
この限界に先に手を打ったのがFreeBSDで、2000年のFreeBSD 4.0で登場した
jailはファイルシステムだけでなくプロセスやネットワークまで丸ごと区切る仕組みでした。Linux側の答えであるNamespaceは2002年のmount namespaceを皮切りに種類を増やし、2013年のuser namespaceで主要な6種類(mnt・uts・ipc・pid・net・user)が揃います。次の表に挙げるcgroup(2016年)とtime(2020年)はさらに後発の追加分です。一次情報としてはnamespaces(7)が便利です。「chrootの弱点を埋める」という問題設定自体は、20年以上前から共有されていたわけです。 ↩ -
ホスト側からは
lsnsでどんなNamespaceが存在するかを一覧でき、nsenter -t <PID> -n ss -tulpnのように「どのプロセスの、どの部屋に入るか」を指定して調査できます。この「外からは全部見えるが、中からは何も見えない」非対称性がNamespaceの本質で、トラブルシュートのときはホスト側から覗くのが基本になります。何か楽しそうな記述を期待してこの脚注を読んだ方は居ませんよね? ↩ -
cgroupsには v1 と v2 があり、現代のディストリビューションはほぼ v2(統合階層)に移行済みです。設定の実体は
/sys/fs/cgroup/以下のファイルで、メモリ上限はmemory.max、OOMの発生回数はmemory.eventsのoom_kill、CPUの割り当てはcpu.maxで確認できます(詳細は cgroup v2 のカーネル公式ドキュメントが一次情報です)。なお上のデモは動きを見るための手動操作で、systemd管理下のサーバで恒久的に上限を課したいなら、ユニットにMemoryMax=を書くのが本筋です。ちなみにsystemdのサービスも裏では全部cgroupsで管理されていて、systemd-cglsやsystemctl statusで所属グループが見えます。コンテナを使っていなくても、あなたのサーバはすでにcgroupsの上で動いているわけです。 ↩