4-3. pivot_rootを用いたルート移動
前節で扱ったchrootは、比較的弱い機構です。
もしコンテナ内のユーザーがchrootする権限を持っていた場合、元のルートディレクトリに移動できてしまいます。
詳しくはこちら:
https://container-security.dev/namespace/chroot-and-pivot_root#chroot-の問題点
少々扱いは難しいですがより強固なルートディレクトリの移動方法として、pivot_rootがあります。
さあ、pivot_rootでより強固なセキュリティを手に入れましょう。
【前提】この節で扱うsyscall
pivot_root
ルートファイルシステムのマウントを入れ替えます。
より正確には、今までのルートファイルシステムをputoldのディレクトリにマウントし、newrootのディレクトリを新しいルートファイルシステムとして/にマウントします。
4-1で書いた通り、ルートファイルシステムもマウントによって構築されています。
このマウントを入れ替えてしまえば、抜ける手段はないだろうという算段です。
OS側で登録されているルートフォルダを変更するだけのchrootと違い、比較的安全なルート移動方法です。
func PivotRoot(newroot string, putold string) (err error)ルートを移動させる
pivot_rootにはいくつかの制約があります。
new_rootおよびput_oldには以下の制限がある。
- ディレクトリでなければならない。
new_rootとput_oldは現在のrootと同じファイルシステムにあってはならない。put_oldはnew_root以下になければならない。
さて、これらの制約をクリアするにはどうすれば良いでしょうか?
chrootよりもはるかに複雑なマウント処理が必要になりますが、頑張ってみましょう。
ヒント1
new_rootとput_oldは現在のrootと同じファイルシステムにあってはならない。
この条件は、put_oldをnew_root配下に作ったのち、new_rootをbind mountすることによって実現できます。
ヒント2
pivot_root後にput_oldをアンマウントしないと、過去のルートがすべて見えてしまいます。
ヒント3
ヒント2のアンマウントはデバイスが使用中扱いなので、lazy unmountをしないといけません。
ヒント4
Mount Namespaceを切り分けていても、Mount Propagationを正しく設定しないと、全てのマウントが引きずられます。/dev /sys /procなど、カーネルから特殊マウントされているフォルダ群は/のマウントからPropagationされるので、pivot_rootした際にマウントが移動し、仮想ターミナルを含むデバイス群が使用不可能になります。
pivot_root前に/に対してモードを変えて再マウントを行い、Propagationを切ってあげましょう。
想定解答
想定解答
func SetupRootfs(c RootfsConfig) error {
// ルートディレクトリから再帰的にマウントのプロパゲーションを無効にする
// これをやらないと、pivot_root時にホストマシン側の/devや/sysなどの特殊ファイルの
// マウントが壊れ、新しいシェルセッションが開けなくなるなどの支障が出る
if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_SLAVE, ""); err != nil {
return errors.WithStack(err)
}
// 既存のルートファイルシステムを移動させるディレクトリを作成
if err := os.MkdirAll(filepath.Join(c.RootfsPath, "/.old_root"), 0755); err != nil {
return errors.WithStack(err)
}
// RootfsPathをバインドマウントし、ルートファイルシステムの管轄外とする
if err := unix.Mount(c.RootfsPath, c.RootfsPath, "", unix.MS_BIND, ""); err != nil {
return errors.WithStack(err)
}
// ルートファイルシステムをRootfsPathにマウントし直す
if err := unix.PivotRoot(c.RootfsPath, filepath.Join(c.RootfsPath, ".old_root")); err != nil {
return errors.WithStack(err)
}
// 古いルートファイルシステムはアンマウント・削除し、不可視にする
// 注: MNT_DETACHを付けてlazy unmountにしないとアンマウントできない
if err := unix.Unmount("/.old_root", unix.MNT_DETACH); err != nil {
return errors.WithStack(err)
}
if err := os.Remove("/.old_root"); err != nil {
return errors.WithStack(err)
}
// カレントディレクトリをルートに
if err := os.Chdir("/"); err != nil {
return errors.WithStack(err)
}
return nil
}ルートディレクトリが変わったことを確かめる
ルートディレクトリが変わったことを確かめましょう。
ルートディレクトリの変更にはroot権限が必要なので、sudo suを実行してrootになってからプログラムを実行して下さい。
シェルが開いた瞬間少し様子が変わっていたり、カレントディレクトリが/になっていたり、goコマンドが見つからなかったりと様々な違いが表れているはずです。
$ sudo su
# make run
go build -o main *.go
./main run bash
# go
bash: go: command not found
#