🏝️

Landlock: ユーザ権限によるアクセス制御

2022/12/27に公開

LandlockはLinux 5.13で追加され、5.19で更新(ABI V2)されたプロセス単位のアクセス制御機構です。
https://landlock.io/

この機能は主に自分自身の権限を制限してサンドボックスを作るために使います。例えばこの記事の後半では信頼できない実行バイナリをサブプロセスとして起動する際にアクセス出来るファイルシステムの範囲を制限する例を見ます。

rust-landlock/examples

今回はLandlockをRustから使えるようにしたrust-landlockを試していきます。ドキュメントはlandlock.io/rust-landlockに公開されています。
https://github.com/landlock-lsm/rust-landlock

特にCによるサンプルRustで書き直したサンプルを見ていきましょう。とりあえず実行してみるとヘルプを出してくれます:

cargo run --example sandboxer
usage: LL_FS_RO="..." LL_FS_RW="..." target/debug/examples/sandboxer <cmd> [args]...

Launch a command in a restricted environment.

Environment variables containing paths, each separated by a colon:
* LL_FS_RO: list of paths allowed to be used in a read-only way.
* LL_FS_RW: list of paths allowed to be used in a read-write way.

example:
LL_FS_RO="/bin:/lib:/usr:/proc:/etc:/dev/urandom" LL_FS_RW="/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp" target/debug/examples/sandboxer bash -i

2つの環境変数LL_FS_ROLL_FS_RWにそれぞれ読み込み専用のパスと読み書きできるパスを指定して、引数に起動するプログラムを指定します。例を示してくれているので起動してみましょう:

LL_FS_RO="/bin:/lib:/usr:/proc:/etc:/dev/urandom" \
LL_FS_RW="/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp" \
target/debug/examples/sandboxer bash -i

するとbashが開始されます。とりあえずlsを実行してみましょう:

[myname@mymachine rust-landlock]$ ls
ls: cannot open directory '.': Permission denied

現在のディレクトリが開けません。サンドボックスっぽいですね。では上で指定したディレクトリに行ってみましょう:

[myname@mymachine rust-landlock]$ cd /etc/
[myname@mymachine etc]$ ls | head -5
adjtime
alsa
alternatives
anacrontab
anthy-conf

読み込めますね。書き込めるかも見ておきましょう:

[myname@mymachine etc]$ echo "homhom" /tmp/homhom
homhom /tmp/homhom
[myname@mymachine etc]$ echo "homhom" > /tmp/homhom
[myname@mymachine etc]$ cat /tmp/homhom
homhom

上手く動いていますね。

sandboxer.rs

ではソースコードを見ていきましょう。前半に色々書いていますが、肝心なのはmainの後半です:

    let abi = ABI::V2;

    // アクセス制御の為のルールを作る
    let status = Ruleset::new()
        .handle_access(AccessFs::from_all(abi))?
        .create()?
        // Read-onlyのパスの追加
        .add_rules(PathEnv::new(ENV_FS_RO_NAME, AccessFs::from_read(abi))?.iter())?
        // Read-Writeのパスの追加
        .add_rules(PathEnv::new(ENV_FS_RW_NAME, AccessFs::from_all(abi))?.iter())?
        .restrict_self()
        .expect("Failed to enforce ruleset");

    // Landlockをサポートしていないカーネルで動かした場合、制限に失敗する
    if status.ruleset == RulesetStatus::NotEnforced {
        bail!("Landlock is not supported by the running kernel.");
    }

    // サブプロセスとして引数で受け取ったプログラム(上の例だと`bash -i`)を起動する
    Err(Command::new(cmd_name)
        .env_remove(ENV_FS_RO_NAME)
        .env_remove(ENV_FS_RW_NAME)
        .args(args)
        .exec()
        .into())

コメントは私が追加しています。処理は単純で、handle_accessadd_rulesによってルールセットを作り、restrict_selfによって現在のプロセスにルールを適用し、サブプロセスを起動しています。サブプロセスには自動的に制約が継承されます。所々abiを引数にもらっているのは互換性の為です。

PathFd, O_PATH in open(2)

処理は名前から期待される通りでドキュメントを順番に見れば詳細は分かりますが、1つ気になる点としてPathFdというのが出てきます。add_rulesにはパスとアクセス権の組を表す構造体PathBeneathを渡しますが、これはパスの表現としてファイル記述子を使います。これはstd::os::unix::io::AsRawFdを使ってファイル記述子をもらうAPIになっており、例えばstd::fs::Fileもこれは実装していますが、ここで代わりにPathFdを使うようになっています。これは何故かというとLandlockに渡すファイル記述子はファイルシステム上でのファイルの位置さえ分かればよく、ユーザーが実際にこのファイルを開く権限が無くても使えるようになっています。そのような用途のためにO_PATHというオプションがopenシステムコールには存在し、PathFdはそれを使うようになっています。

GitHubで編集を提案

Discussion