インフラ・DevOps
500行のコードで学ぶLinuxコンテナ
Linux containers in 500 lines of code (blog.lizzie.io)
要約
この記事は、Linuxコンテナの仕組みを理解するために、約500行のCコードで実装されたシンプルなコンテナランタイムを紹介しています。著者は、名前空間、ケーパビリティ、リソース制限、システムコールフィルタリングといった、コンテナを構成する主要なLinuxカーネルのメカニズムを解説し、それらをどのように組み合わせて最小限の制限でコードを実行できるかを示しています。
全文翻訳
Linuxコンテナを500行のコードで
目次
コンテナのセットアップ
contained.c
名前空間
ケーパビリティ
ドロップされたケーパビリティ
保持されたケーパビリティ
マウント
システムコール
禁止されたシステムコール
許可されたシステムコール
リソース
ネットワーキング
私は長年、Linuxコンテナを直接的にも間接的にも利用してきましたが、それらをより深く理解したいと思いました。そこで、コードを書いてみました。これは以前は500行のコードだったのですが、公開後にいくつか改訂し、70行ほど増えました。特に、信頼できないコードを実行するための最小限の制限セットを見つけたいと思いました。これは、何らかの露出があるものに対してコンテナを扱うべき方法ではありません。本来は、制限できるものはすべて制限すべきです。しかし、どの権限が根本的に安全でないかを知ることは重要だと思います!私が述べていることを、コードや信頼する人々へのリンクで裏付けようとしましたが、何か見落としがあればぜひ知りたいです。これはnowebスタイルのリテラートコードです。<<x>>と名前付けされた参照は、xという名前のコードブロックに展開されます。コンパイルされたソースはここで見つけることができます。このドキュメントはorgmodeドキュメントであり、そのソースはここで見つけることができます。このドキュメントとこのコードはGPLv3ライセンスの下で提供されており、そのソースはここで見つけることができます。
コンテナのセットアップ
現代のLinuxコンテナを構成するメカニズムには、いくつかの補完的かつ重複する仕組みがあります。大まかに言うと、名前空間はカーネルオブジェクトを特定のプロセスツリーがアクセスできる異なるセットにグループ化するために使用されます。例えば、pid名前空間はプロセスリストのビューを名前空間内のプロセスに制限します。いくつかの異なる種類名前空間があります。これについては後で詳しく説明します。ケーパビリティは、uid 0ができることにいくつかの粗い制限を設定するためにここで使用されます。cgroupsは、メモリ、ディスクI/O、CPU時間などのリソースの使用を制限するメカニズムです。setrlimitは、リソース使用を制限するための別のメカニズムです。これはcgroupsよりも古いですが、cgroupsができないこともいくつかできます。これらはすべてLinuxカーネルのメカニズムです。Seccomp、ケーパビリティ、setrlimitはすべてシステムコールで行われます。cgroupsはファイルシステムを通じてアクセスされます。ここには多くの要素があり、各メカニズムの範囲はかなり不明瞭です。それらは多く重複しており、物事を制限するための最良の方法を見つけるのは難しいです。ユーザー名前空間は比較的新しく、この動作の多くを統一することを約束しています。しかし残念ながら、ユーザー名前空間を有効にしてカーネルをコンパイルすると、物事が複雑になります。ユーザー名前空間を有効にしてコンパイルすると、カーネルのセマンティクスがシステム全体で変更され、さらに問題を引き起こしたり、少なくとも混乱を招いたりする可能性があります1。ユーザー名前空間によって、数多くの特権昇格バグが露呈しています。「Linuxコンテナの理解と強化」では、ユーザー名前空間がセキュリティに関して大きな利点をもたらすにもかかわらず、ユーザー名前空間の機密性の高い性質、いくらか相反するセキュリティモデル、そして大量の新しいコードのために、いくつかの深刻な脆弱性が発見されており、残念ながら新しい脆弱性も発見され続けていると説明されています。これらは、ユーザー名前空間の実装自体に関わるものや、特権昇格を実行するためにユーザー名前空間の不正または意図しない使用を許可するものなどがあります。これらの問題は、コンテナが使用されていないシステムで、カーネルバージョンがユーザー名前空間をサポートするのに十分新しい場合に現れることがよくあります。執筆時点ではLinuxではデフォルトで無効になっていますが2、多くのディストリビューションは限定的な方法で有効にするパッチを適用しています3。しかし、これらの問題はすべて、ユーザー名前空間がコンパイルされているホストに適用されます。ネストされたユーザー名前空間の使用を防止する予定なので、ユーザー名前空間を使用するかどうかはあまり関係ありません。したがって、ユーザー名前空間が利用可能な場合のみ使用します。(このコードのユーザー名前空間の処理は、元々はかなり壊れていました。特にJann Horn氏から素晴らしいフィードバックを得ました。感謝します!)
contained.c
このプログラムは、以下のように、/misc/imgをルートとして、/misc/img/bin/shを実行するために使用できます。
[lizzie@empress l-c-i-500-l]$ sudo ./contained -m ~/misc/busybox-img/ -u 0 -c /bin/sh
=> Linuxバージョンを検証中...4.7.10.201610222037-1-grsec on x86_64。
=> cgroupsを設定中...memory...cpu...pids...blkio...完了。
=> rlimitを設定中...完了。
=> すべてをMS_PRIVATEで再マウント中...再マウントしました。
=> 一時ディレクトリを作成し、そこにバインドマウントを作成中...完了。
=> ルートをピボット中...完了。
=> /oldroot.oQ5jOYをアンマウント中...完了。
=> ユーザー名前空間を試行中.../proc/32627/uid_mapを書き込み中.../proc/32627/gid_mapを書き込み中...完了。
=> uid 0 / gid 0に切り替え中...完了。
=> ケーパビリティをドロップ中...バウンディング...インヘリタブル...完了。
=> システムコールをフィルタリング中...完了。
/ # whoami
root
/ # hostname
05fe5c-three-of-pentacles
/ # exit
=> cgroupsをクリーンアップ中...完了。
したがって、そのためのスケルトンは以下のようになります。
リスト7: contained.c
/* -*- compile-command: "gcc -Wall -Werror -lcap -lseccomp contained.c -o contained" -*- */
/* このコードはGPLv3ライセンスの下で提供されています。ライセンスの全文はここで見ることができます: https://www.gnu.org/licenses/gpl-3.0.en.html */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <sched.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/capability.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <linux/capability.h>
#include <linux/limits.h>
struct child_config {
int argc;
uid_t uid;
int fd;
char *hostname;
char **argv;
char *mount_dir;
};
<<capabilities>>
<<mounts>>
<<syscalls>>
<<resources>>
<<child>>
<<choose-hostname>>
int main (int argc, char **argv) {
struct child_config config = {0};
int err = 0;
int option = 0;
int sockets[2] = {0};
pid_t child_pid = 0;
int last_optind = 0;
while ((option = getopt(argc, argv, "c:m:u:"))) {
switch (option) {
case 'c':
config.argc = argc - last_optind - 1;
config.argv = &argv[argc - config.argc];
goto finish_options;
case 'm':
config.mount_dir = optarg;
break;
case 'u':
if (sscanf(optarg, "%d", &config.uid) != 1) {
fprintf(stderr, "badly-formatted uid: %s\n", optarg);
goto usage;
}
break;
default:
goto usage;
}
last_optind = optind;
}
finish_options:
if (!config.argc) goto usage;
if (!config.mount_dir) goto usage;
<<check-linux-version>>
char hostname[256] = {0};
if (choose_hostname(hostname, sizeof(hostname))) goto error;
config.hostname = hostname;
<<namespaces>>
goto cleanup;
usage:
fprintf(stderr, "Usage: %s -u -1 -m . -c /bin/sh ~\n", argv[0]);
error:
err = 1;
cleanup:
if (sockets[0]) close(sockets[0]);
if (sockets[1]) close(sockets[1]);
return err;
}
システムコールとケーパビリティをブラックリスト化するので、新しいものがないことを確認することが重要です。
リスト8: <<check-linux-version>>
= fprintf(stderr, "=> Linuxバージョンを検証中...");
struct utsname host = {0};
if (uname(&host)) {
fprintf(stderr, "failed: %m\n");
goto cleanup;
}
int major = -1;
int minor = -1;
if (sscanf(host.release, "%u.%u.", &major, &minor) != 2) {
fprintf(stderr, "weird release format: %s\n", host.release);
goto cleanup;
}
if (major != 4 || (minor != 7 && minor != 8)) {
fprintf(stderr, "expected 4.7.x or 4.8.x: %s\n", host.release);
goto cleanup;
}
if (strcmp("x86_64", host.machine)) {
fprintf(stderr, "expected x86_64: %s\n", host.machine);
goto cleanup;
}
fprintf(stderr, "%s on %s.\n", host.release, host.machine);
(これはバグがありました。redditのcaptainjeyさんが教えてくれました。感謝します!)
そして、コード行数が500行に少し満たなかったので、素敵なホスト名を構築するスペースがあると思いました。
リスト9: <<choose-hostname>>
=int choose_hostname(char *buff, size_t len) {
static const char *suits[] = { "swords", "wands", "pentacles", "cups" };
static const char *minor[] = { "ace", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "page", "knight", "queen", "king" };
static const char *major[] = { "fool", "magician", "high-priestess", "empress", "emperor", "hierophant", "lovers", "chariot", "strength", "hermit", "wheel", "justice", "hanged-man", "death", "temperance",