jailを利用すると、FreeBSDの中で、もう一つ(あるいは複数)のFreeBSDを動かすことが出来ます。厳密に言うと、ディレクトリツリーやプロセスを封じ込めて、各種サービスやアプリケーションを実行することができます。ですから、見かけ上、FreeBSDの中で別のFreeBSDが動いているように見えます。
これと似たようなものにchroot(8)があります。これは、特定のコマンドを実行する際、ルートディレクトリの位置を変更することにより、そのコマンド以下のプロセスが、許可されているディレクトリより上位の部分にあるコマンドの実行やファイルの閲覧を防止する、というものです。
ただ、chrootでは、上位ディレクトリは隠せてもプロセスまでは隠せません。丸見えです。これでは万が一、root等の高い権限が乗っ取られた場合、chroot環境下以外の全く別のプロセスを停止させられたり、そのプロセス自体が乗っ取られる危険性があります。
それに対して、jailではjail外のプロセスは参照できません。何か不都合が生じたとしても、それはそのjail内だけの話です。ですから、jailはchrootよりも、安全であると言えます。外部に公開するサービスなどはjail内に置いた方が良い場合もあるかも知れません。
jailの構築
ソースからの構築
まず、構築の準備として、jailの親となるホスト側の/etc/make.confを適切に設定して、不要なものをビルドしないようにしておきます。
さて、ソースツリーに入り、jailを構築します*1。DESTDIRには、jailがインストールされるPATHを示します。ここでは、便宜上、環境変数JAIL_ROOTに、jailがインストールされるPATHを予めセットしておきます*2。
# setenv JAIL_ROOT /home/jail/www
# mkdir -p ${JAIL_ROOT}
# cd /usr/src
# make buildworld
# make installworld DESTDIR=${JAIL_ROOT}
次に、/usr/src/etcに入り、jail側の/etcに展開される設定ファイルをインストールします。
# cd /usr/src/etc
# make distribution DESTDIR=${JAIL_ROOT}
*1ホスト側をmakeworldで構築し直した際、それの使いまわしでもOK
*2jail環境をどこに置かなければならないかということは決まっておらず、当然、各自の都合の良い場所に設けてもらって構わない。この例では、/usr/homeにマウント(/home自体は/usr/homeへのsymlink(7))されているパーティション以下に${JAIL_ROOT}を置いて説明している。やや話が逸れるが、例えば、ホスト側の/usrを後で触れるmount_nullfsを利用して、jail側の/usrに使い回そうとした場合、これだと/usrと/usr/home/jail/www/usrのPATHは直系の関係(そのまま${JAIL_ROOT}から上に登っていけば/usrにたどり着く)になってしまい、「not distinct paths」とか言われて、nullfsではマウントすることができない。ホスト側の/usr/ports、/usr/bin、/bin等は、ここでの${JAIL_ROOT}とは枝分かれした傍系の関係(上に遡っても、どの時点かで一旦、下に降りなければ該当する場所にたどり着けない)になるので問題なくマウントできる。したがって、もし、ここで例示したような/home以下に${JAIL_ROOT}を設定するケースにおいて、nullfsを介してホスト側の/usr以下を一括してjail側に使いまわしたいような場合には、事前に、/usr/homeにマウントしているパーティションのマウント先を/homeに変更し、既存の/usr/homeはそれへのsymlinkに置き換えるなどと工夫が必要になる。もっとも、jail専用のパーティションを設けるなりして、/jailなどとマウントし、他と重複しないような場所に${JAIL_ROOT}を最初から設定していれば、このような問題はそもそも発生しないが。
jailの基本設定
ビルドが終了したら、jailが正常に動作するための基本設定をします。
kernel等のダミーファイルを作成
jailの起動時にエラーが表示されないよう、kernelと/etc/fstabのダミーファイルを作成。
# cd ${JAIL_ROOT}
# ln -sf dev/null kernel
# touch etc/fstab
/etc/rc.confを作成
jail内の/etc/rc.confを次のように作成。
network_interface="" syslogd_flags="-ss" sendmail_enable="NONE"
任意で、各jailごとにセキュリティレベルを設定することができます(init(8)参照)。
#kern_securelevel_enable="YES" #kern_securelevel="3"
必要に応じて、sshdを起動するように以下を追加しても良いでしょう*3。
#sshd_enable="YES"
名前解決
加えて、jail側の/etc/resolv.confも設定し、jail内で名前解決が解消できるようにしておきます。
# echo 'nameserver 123.123.123.123' > etc/resolv.conf
その他
あと、jail側の/homeは、ホスト側から覗く頻度も多いと思います。しかし、そのシンボリックリンクが、絶対PATHで指定されていると混同して紛らわしいので、予め相対PATHで作成しておくと便利です。
# mkdir usr/home # ln -sf usr/home .
としておきます。
ここまでで大体出来上がり。後で使いまわせるように、バックアップを取っておきます。
# cd ${JAIL_ROOT}
# tar zcf /var/tmp/jail.tar.gz .
jailの起動と停止
起動する前の準備
事前に、ホスト自体に、IPアドレスを明示せずにlistenしているサービスがあるかどうかを確かめます。
# sockstat | grep "\*:[0-9]"
ここで、もし、sockstat(1)を実行した際、「LOCAL ADDRESS」に該当する項目で、IPアドレスが明示されていないもの*4があれば、そのポートの使用に関しては、jail側と競合することになります。この問題を回避するため、予めホストに割り当ててあるIPアドレスを明示してlistenさせるように、該当するサービスの設定を見直しましょう。
*4「 *: 」のように表示されるもの。jail(8)にマニュアルに具体例が書かれています。その他、メジャーなサービスについては、Creating a FreeBSD Jail - Section6wikiに解かり易く記載されていました。
手動での起動
さて、実際に構築したjailが起動するかどうか確かめましょう。まず、jailに割り当てるIPアドレスを確保するため、ネットワークインターフェースにエイリアスをつけます。
# ifconfig fxp0 alias 192.168.0.20/32
次に、devfsがjailからも見えるように、jail側の/devにもマウントします。
# mount -t devfs dev ${JAIL_ROOT}/dev
念のため、jail用のdevfsルールセット*5を適用し、不要なデバイスファイルを隠します。
# devfs -m ${JAIL_ROOT}/dev rule -s 4 applyset
これで起動できると思います*6。
# jail ${JAIL_ROOT} www.somedomain.com 192.168.0.20 /bin/sh
自動起動させるための設定
システム起動時、自動的に/etc/rc.d/jailスクリプトを実行して、jailを起動するようにするためには、ホスト側の/etc/rc.confに次を加えます*7。
ifconfig_fxp0_alias0="inet 192.168.0.20 netmask 255.255.255.255 broadcast 192.168.0.255"
jail_enable="YES"
jail_list="www" # 起動したいjailを列挙。複数ある場合は、"www www2"の様に、スペースで区切る
### 全jail共通の設定 ###
#jail_set_hostname_allow="YES" # jail内のrootユーザが、そのjailに割り当ててある
# hostnameを変更しようとしても、許可しないのならNO
#jail_socket_unixiproute_only="YES" # jail内から、UNIXドメインソケット・IPv4アドレス・
# ルーティングソケット以外へのアクセス(例えば、
# 他のドメイン、IPX等へのアクセス)を許可するのならNO
#jail_sysvipc_allow="NO" # jail内でSystemVの共有メモリの使用を許可するならYES
### 各jail固有の設定 ###
jail_www_rootdir="/home/jail/www"
jail_www_hostname="www.somedomain.com"
jail_www_ip="192.168.0.20"
jail_www_devfs_enable="YES"
#jail_www_exec_start="/bin/sh /etc/rc" # jail起動スクリプトの既定値
#jail_www_exec_shutdown="/bin/sh /etc/rc.shutdown" # jail終了スクリプトの既定値
#jail_www_fdescfs_enable="NO" # jail内でfdescfs(5)を使用するならYES
#jail_www_procfs_enable="NO" # jail内でprocfs(5)を使用するならYES
#jail_www_mount_enable="NO" # 当該jail起動/終了時、特別にmount/umountを実行するかどうか
#jail_www_devfs_ruleset="ruleset_name" # jail用の/devに適用するルールセットが特別にあれば指定
# # 既定では/etc/defaults/devfs.rulesで定義されている
# # "devfsrulesets_jail"になる
#jail_www_fstab="" # 上記jail_www_mountをYESとした場合、参照されるfstabの名前
# # 通常、マウントポイントはjail_www_rootdir以下のディレクトリを指定
### その他に、jail_listに列挙したjailがあれば、上記を参考に設定 ###
#ifconfig_fxp0_alias1="inet 192.168.0.21 netmask 255.255.255.255 broadcast 192.168.0.255"
#
#jail_www2_hostname="www2.somedomain.com"
#jail_www2_ip="192.168.0.21"
#jail_www2_rootdir="/home/jail/www2"
#jail_www2_devfs_enable="YES"
これにより、システム起動時、/etc/rc.d/jailスクリプトから、上記設定*8が読み込まれ、jailが開始します。
直接、このjailスクリプトから開始したい場合は、
# /etc/rc.d/jail start
とします。
必要な設定を済ませているjail環境であれば、たとえ、上記/etc/rc.conf内の「jail_list」に列記していなくても、
# /etc/rc.d/jail start www2
といったように、直接、jail名を指定することでも実行できます。
*7コメントにしてあるものは任意。また、FreeBSD 6.1-RELEASEからは、「jail_www_interface="fxp0"」のように、各jail環境ごとに、(IPアドレスを割り当てるための)インターフェースを指定することができるようになりました。jail環境で用いるIPアドレスは、/etc/rc.d/jailの起動時、指定したインターフェース上に、エイリアスとして、割り当ててくれます。したがって、jail環境で用いるIPアドレスを確保するため、上記「ifconfig_fxp0_alias0」のような記述を事前にしておく必要はなくなりました(FreeBSD Notes - FreeBSD 6.1-RELEASEにアップグレードを参照)。
*8なお、上記「全jail共通の設定」に関しては、これ以外にもsysctl(8)でカーネルのMIB変数を操作することで、細かな設定が可能です。それらについては、jailの最新のオンラインマニュアル、「Sysctl MIB Entries」の項を参照してください。
停止に関して
手動での停止
jailを任意に停止する場合は、停止させたいjail内にrootでログインし、
# kill -TERM -1 # kill -KILL -1
のいずれかのように、すべてのプロセス*9に対して、目的に応じたシグナルを送るか、もしくは、ホスト側で、
# killall -TERM -j JID
の様に、killall(1)に、-jオプションを付けて、JID*10を指定してシグナルを送ることにより、該当jailを停止する事が出来ます。
/etc/rc.d/jailによる停止
/etc/rc.confに前述のjailに関する設定をしてあれば、単に、
# /etc/rc.d/jail stop
とコマンドスクリプトを実行する事により、jailは停止します。また、
# /etc/rc.d/jail restart
とすることにより、jailを再起動することも出来ます*11。
ただし、これらの方法では、/etc/rc.conf内の「jail_list」に列挙されている全てのjailが対象となるので、注意して下さい。任意のものを指定したい場合には、
/etc/rc.d/jail stop www2
等とjail名を指定します。
ユーザの管理
rootのパスワード
まず、第一に、rootのパスワードを設定します。初期の状態では、設定されていないので大変危険です。jail内でpasswd等のコマンドを実行して任意のパスワードを設定するか、あるいは、vipw(8)を実行してjail内のrootのパスワードの箇所を「*」のように書き換えましょう。
# vipw -d ${JAIL_ROOT}/etc/
root:*:0:0::0:0:Charlie &:/root:/bin/csh
この方法では、rootでは通常、入れなくなります。しかし、root権限が必要な場合もあるので、そういった場合は、ホスト側からjexec(8)というコマンドを使用するという風にします。
ホスト側で、jlsとコマンドを打ち込むと、
JID IP Address Hostname Path
1 192.168.0.20 www.somedomain.com /home/jail/www
のように表示されるので、このJID*13に該当するjailで操作が行いたければ、
# jexec 1 /bin/csh
とすれば、該当jail内のcshを起動して、root権限で作業が行えます。
ただし、この場合、ホスト側の環境変数が引き継がれてしまうので、引き継ぎたくなければ、csh起動後、
# su -
を実行するか、最初から、
# jexec 1 /usr/bin/su -
等と、ログインすると、うまくいくようです。
portsの利用
導入するアプリケーションは、packagesやportsを利用した方が管理が容易です。
ホスト側のportsツリーを同期した後、jail側に既にインストールされているものも更新されてないかを知りたい時には、
env PKG_DBDIR=${JAIL_ROOT}/var/db/pkg pkg_version -l '<'
とすることにより、チェックできます。
また、各jail内にportauditの導入を考えている場合、ports-mgmt/jailauditというものもあるので、検討してみるのも良いでしょう。これを使うと、個々のjailにportauditを導入する手間が省け、ホスト側から一元して、各jail内にインストールされているportsの脆弱性のチェックを行うことができるので便利です。
nullfsを用いた事例
しかし、jail側でportsを利用する場合、一番問題となるのは、膨大な量の/usr/ports以下のファイルをjail側にも展開しなければならないことです。jail環境にまでportsツリーを展開するのは資源の無駄*14なので、ここでは、mount_nullfs(8)を利用*15*16して、ホスト側のportsツリーを借用することにします。
# mkdir ${JAIL_ROOT}/usr/ports
# mount_nullfs /usr/ports ${JAIL_ROOT}/usr/ports
これでhost側の/usr/portsが、jail側の/usr/portsにマウントされることになります。
導入するportによっては、/usr/srcのファイルを必要とするものもあるので、適宜、
# mount_nullfs /usr/src ${JAIL_ROOT}/usr/src
としておきます。これで、
# jexec JID /bin/csh www# cd /usr/ports/someports www# make install && make clean www# exit
の様にすれば、jail側のportsツリーからアプリケーションを導入する事が出来ます。
もし、nullfsでマウントした/usr/portsに、portsをビルドする際、書き込みされるのが不都合*17であれば、jail側の/etc/make.confに、
DISTDIR=/home/data/distfiles PACKAGES=/home/data/packages WRKDIRPREFIX=/var/tmp
等と、予め適当な作業ディレクトリを環境変数に指定*18しておくと良いでしょう。
アプリケーションの導入が完了し、先ほどマウントしたportsツリー等の必要がなくなれば、ホスト側で以下のようにします。
# umount /usr/ports # umount /usr/src
これでアンマウントされます*19。
*14初心者もOK! FreeBSD質問スレッド その41++のログ内、795,797-798,801,810,835番の書き込み参照。
*15なお、もし、このケース以外にnullfsを活用する場合でも、書き込みが可能でマウントした時には、nullfsを介してアクセスできる箇所(この場合だと/usr/ports以下にあるファイル群)が、jail側から不本意に書き換えられるかも知れないという危険性に留意しておく必要がある(/usr等のホスト環境の安全性に関わる部分を、書き込み可能でマウントしたnullfsを介して、jail側に使い回そうとするのは危険)。
*16それに加え、通常のファイル・ディレクトリ以外(要するにスペシャルファイル)には、nullfsを介してアクセスしないよう注意。そういう場合は、前述したホスト側の/etc/rc.confの設定などで各${JAIL_ROOT}以下に専用のものを用意する。
*17現在のところ、nullfsは公式にはサポートされておらず、万が一の場合はデータ破壊の恐れもあり得る。通常、書き込み可能モードよりも、読み込み専用モードの方が安全性が高いと考えるのが妥当であって、全く書き込みされるのを避けたいのであれば、otsune's FreeBSD memo :: jailの作り方にも記載されているように、最初から「mount_nullfs -o ro /usr/ports ${JAIL_ROOT}/usr/ports」などと読み込み専用の状態でマウントする方針で、上記make.confの設定をした方が良い(ただし、4.10以前では問題があったので注意。なお、FreeBSD 6.0以降でのnullfsは、実用に耐え得るものに改善されているようです)。
*18各環境変数の役割についてはports(7)を参照。これらを指定しておかないと、jail側の作業であるにも拘らず、nullfsを介して、ホスト側の/usr/ports内でビルドに関連した作業が行われてしまう。それに、もし、同時にホスト側や他のjail環境でも、/usr/portsを共有してビルドしたような場合、それだと、作業が競合してトラブルが発生する可能性もあるので、特に理由がなければ、上記設定を各jail内の/etc/make.confに明示して作業用のディレクトリは、それぞれ固有のものに別けておいた方が良い。
*19定時に/etc/crontabから呼び出されるperiodic(8)等で、重複してfind(1)されたりしても無駄なので、こまめにumount(8)するよう習慣づけておいた方が何かと無難(もっとも、FreeBSD 6.0以降、nullfsは安定していると言われているので、sysutils/ezjailというportのように、nullfsを常時利用することを前提にしたものもある。各jail環境ごとにmakeworld等で個別のベースシステムを用意するのではなく、予めjail環境専用のベースシステムを一つ用意しておき、それをnullfsを介し、複数のjail環境と共有することで、ディスク領域を節約しようという試みである)。
その他
マニュアルを読むと、現在のところnullfsは完全にサポートされていない*20とのことなので、そのようなリスクを侵してまで使用したくない、しかし、一つのシステム内にportsツリーを多重に展開するのは明らかに無駄だと考える方にとって、/usr/ports以下を別パーティション、あるいは、ファイルベースのファイルシステムに移行*21して、必要に応じてホスト側とjail側とで切替え、便宜上、portsビルドの際に使い回すというのも一つの選択肢です。
既に/usr/portsを別パーティション等に設けてあれば問題はありませんが、設けてない場合、新規に物理的なディスクを用意したり、既存の領域を見直したりするよりも、通常、ファイルベースの仮想ディスクを作成する方が容易です。
以下は、mdconfig(8)を利用して、ファイルベースの仮想ディスクを作成し、/usr/ports以下*22をそれに移行させて、ホスト側とjail側でportsツリーを使い回すことにしようという一例です*23。
# date
Fri Sep 23 21:03:06 JST 2005
# dd if=/dev/zero of=/var/tmp/ports.image bs=1k count=500k
# mdconfig -a -t vnode -f /var/tmp/ports.image
md0
# bsdlabel -w md0
# bsdlabel md0
# /dev/md0:
8 partitions:
# size offset fstype [fsize bsize bps/cpg]
a: 1023984 16 unused 0 0
c: 1024000 0 unused 0 0 # "raw" part, don't edit
# newfs -i 2048 md0a
# bsdlabel md0
# /dev/md0:
8 partitions:
# size offset fstype [fsize bsize bps/cpg]
a: 1023984 16 4.2BSD 2048 16384 60864
c: 1024000 0 unused 0 0 # "raw" part, don't edit
# ;;
# mount /dev/md0a /mnt
# ( cd /usr/ports/ && tar cf - . ) | ( cd /mnt/ && tar xfp - )
# df -i /mnt
Filesystem 1K-blocks Used Avail Capacity iused ifree %iused Mounted on
/dev/md0a 435670 294292 106526 73% 94954 209364 31% /mnt
# ls -l /var/tmp/ports.image
-rw-r--r-- 1 root wheel 524288000 Sep 23 21:39 /var/tmp/ports.image
# umount /mnt
# ;;
# cd /usr
# mv ports ports.bak
# mkdir ports
# ;;
# mount /dev/md0a /usr/ports
# mount | grep md0a
/dev/md0a on /usr/ports (ufs, local)
# ls -l /usr/ports/INDEX.*
-rw-r--r-- 1 root wheel 14385152 9 23 14:06 /usr/ports/INDEX.db
# umount /usr/ports
# ;;
# mount -o ro /dev/md0a /home/jail/www/usr/ports
# mount | grep md0a
/dev/md0a on /usr/home/jail/www/usr/ports (ufs, local, read-only)
# jls
JID IP Address Hostname Path
1 192.168.0.20 www.somedomain.com /home/jail/www
# jexec 1 su -
www# cd /usr/ports
www# touch test
touch: test: Read-only file system
www# logout
# umount /home/jail/www/usr/ports
# ;;
# mdconfig -d -u md0
その2*24
# mdconfig -a -t vnode -f /home/data/disks/ports.image -u 0
# mount /dev/md0a /usr/ports/
# mount | grep md0
/dev/md0a on /usr/ports (ufs, local)
# ;;
# cd /usr/ports/ && make update && portsdb -Uu
# ;;
# mount -u -r /usr/ports
# mdconfig -a -t vnode -f /home/data/disks/ports.image -u 1
# mount -o ro /dev/md1a ${JAIL_ROOT}/usr/ports
# mdconfig -l
md1 md0
# mount | grep "md[0-1]"
/dev/md0a on /usr/ports (ufs, local, read-only)
/dev/md1a on /usr/home/jail/www/usr/ports (ufs, local, read-only)
# ;;
# umount /dev/md[0-1]a
# mdconfig -d -u 0
# mdconfig -d -u 1
*20FreeBSD 6.1のマニュアルでは、このような記述は消えました(実質、6.0でも実用に耐え得るものです)。
*21/usr/ports以下にソースファイルを置いたり、ビルド等を行わない方針であれば、確保するディスク容量は、400MB弱ぐらいあれば(現時点で)十分足りると思うが、ファイル数が多いので、問題となるのはnewfs(8)時のinodeの密度。通常、全体のファイルサイズの平均値辺りが目安といわれている。
*22作成時で、総ファイル数 94,953(内ディレクトリ数 20,343)、合計 158,262,103bytes(なお、この内、portsdb(1)によって作成されたINDEX.dbの14,385,152bytesが含まれる)、平均値 1,666.7bytes、標準偏差 53,408.9(?)でした。
*23portsツリーの更新はホスト側で行うこととし、jail側にマウントする際は、不用意な書き換えを防ぐため、読み込み専用にしてマウントすることを指向しています。多少、容量に余裕を持たせていますが、/usr/ports(仮想ディスク)内に、ソースファイルを置いたり、ビルド作業を行う等は、この例では考慮していません。それらはホスト及び各jail内に個別に設けたディレクトリで行います。したがって、各/etc/make.conf内に、nullfsを用いた事例で触れたDISTDIR、WRKDIRPREFIX等の環境変数を設定しておくことが前提です。もし、distfilesやpackagesも仮想ディスク内に置きたい場合は、もっと容量をとった方が良いでしょう。なお、mdconfigを用いて既存のディスクイメージをマウントすることになるので、通常のファイルシステムのように/etc/fstabに記述して、起動時、自動的にマウントさせるといったことはできません。別途、設定が必要になります。
*24異なるリソースを割り当てて、同じイメージをする同時に使用したいような場合は、全て読み込み専用の状態でマウントした方が無難(書き込み可能であれば、事前に、読み込み専用にマウント状態を変更。なお、この例ではホスト側と同時にビルドを行うことを想定していましたが、煩瑣になるので割愛しました)。
jailの運用
留意点
- ホスト側で使用しているループバックインターフェースlo(4)は、jail内では使用できないので、IPアドレスは明示的に、jailに割り当てたものを使用しなければならない(例えば、よく「127.0.0.1」として割り当てられている、webサーバやsshdがlistenするアドレスは変更しなければならない)。
- ホスト側でIPアドレスを明示せずにlistenしているサービスも、jail側と競合するので、IPアドレスを明示するように設定。
- /etc/rc.d/以下のコマンドスクリプトで、jail内では使用できないものに関しては、nojailというKEYWORDが付くようになった。
- ホスト側のprocfs(5)を有効にしてあれば、jailされたプロセスのホスト名は、 /proc/<pid>/status から取得できる。
- ホスト側をinstallworld等で更新した場合、jail側も同様に更新すること。mergemster(8)等で/usr/src/etcとjail内の/etcの内容を同期させるのも忘れずに。
関連情報
- man jail - オンラインマニュアル 【日本語訳】
- Jails: Confining the omnipotent root.
- sysutils/jailctl のportsでJailを導入・設定メモ [FreeBSD](fkimura.com) - sysutils/jailctlを使ったjail環境の構築例について解説されている。上記のような手順を定型化したい場合には便利そう。
- otsune's FreeBSD memo :: jailの作り方
- otsune's FreeBSD memo :: thin jailの作り方 - ここでは触れていない最小限の構成のjail環境の構築例について解説されている。
- Creating a FreeBSD Jail - Section6wiki - jail環境構築のチュートリアル。マニュアルでは解説していないものも含め、競合する可能性のあるサービスについてのIPアドレスの設定例が具体的で解かり易い。
- FreeBSD - このWikiフォーラム内におけるFreeBSDに関係するものの目次です。 ...