TenForward

技術ブログ。はてなダイアリーから移転しました

LXDでコンテナごとに異なるユーザ権限でコンテナを起動する

$ lxc version
Client version: 3.0.1
Server version: 3.0.1

な環境の Ubuntu 18.04 上で試してます。

以下で何の前提もなく書いてるサブ ID のお話は、私の連載

に書いていますのでそちらをどうぞ。

LXD でコンテナを起動すると、デフォルトでは非特権コンテナが起動します。

$ lxc launch ubuntu:18.04 c1
Creating c1
Starting c1
$ lxc list
+------+---------+--------------------+-----------------------------------------------+------------+-----------+
| NAME |  STATE  |        IPV4        |                     IPV6                      |    TYPE    | SNAPSHOTS |
+------+---------+--------------------+-----------------------------------------------+------------+-----------+
| c1   | RUNNING | 10.56.2.183 (eth0) | fd42:dce3:4ae3:1cb3:216:3eff:fe25:e3c3 (eth0) | PERSISTENT | 0         |
+------+---------+--------------------+-----------------------------------------------+------------+-----------+

誰の権限で起動しているか確認してみます。

$ ps auxf
  : (snip)
root     13607  0.0  0.1 530556  7344 ?        Ss   13:31   0:00 [lxc monitor] /var/lib/lxd/containers c1
100000   13626  0.1  0.2 159436  8816 ?        Ss   13:31   0:00  \_ /sbin/init
  : (snip)

このように uid: 100000 のユーザで起動しています。

ここでもうひとつコンテナを起動します。

$ lxc launch ubuntu:18.04 c2
Creating c2
Starting c2
$ ps aux
  : (snip)
root     14507  0.0  0.1 529148  7264 ?        Ss   13:34   0:00 [lxc monitor] /var/lib/lxd/containers c2
100000   14532  1.4  0.2  77420  8660 ?        Ss   13:34   0:00 /sbin/init
  : (snip)

こちらも uid: 100000 のユーザで起動します。

このようにデフォルトでは同一ホスト上で起動するコンテナはすべて同じユーザ権限で起動します。マルチテナントで、コンテナごとにユーザが異なる場合、セキュリティを考えると異なるユーザ権限でコンテナが実行されている方が望ましいでしょう。

LXD ではこれを制御する設定 security.idmap.isolated があります。デフォルトではこれは false ですので、コンテナもしくはプロファイルで true としておくと、異なる uid/gid の範囲を使ってコンテナを起動します。

この辺りは、公式の userns-idmap あたりに詳しいです。

/etc/subuid, /etc/subgid の設定

デフォルトではコンテナには 65536 個の ID を割り当てるので、Ubuntu でユーザを登録すると、subuid/subgid として 65536 個の ID を使えるように設定されています(LXDはコンテナをrootユーザから起動するのでrootに対して)。

$ cat /etc/sub{u,g}id | grep root
root:100000:65536
root:100000:65536

十分なサブ ID が確保されていない場合、

Error: Failed container creation: Not enough uid/gid available for the container.

みたいなエラーでコンテナを作成できません。

コンテナごとに異なる ID の範囲を割り当てる場合、コンテナごとに 65536 必要ですから、sub{u,g}id でも必要な個数設定しておく必要があります。

とりあえずサブ ID はユーザごとに異なっている必要もありませんので、ケチケチ設定する必要もないので、適当に 200000 個くらい設定しましょう。(LXD がどういう計算しているかわからないけど、コンテナ 2 つ起動するから 131072 とか設定すると上記エラーが出ます)

$ cat /etc/sub{u,g}id | grep root
root:100000:200000
root:100000:200000

このあと、LXD を再起動しておきます。(サブ ID のマップを起動時に読み込むから)

$ sudo systemctl restart lxd

もう一つ気をつけること。ドキュメントにも書かれていないと思います。この辺りで発言があります。

/etc/sub{u,g}id に設定する ID の個数ですが、設定した最初の 65536 個(security.idmap.sizeの設定)は security.idmap.isolated を設定していないコンテナ専用です。

つまり先の例のように設定した場合、100000 〜 165535 までの ID は security.idmap.isolated を設定していないコンテナが使いますので、security.idmap.isolated を設定したコンテナを起動したい場合は、さらに 65536 個の ID が必要です。

ですので、ID の個数として /etc/sub{u,g}id に 65536 を設定してある環境で、ひとつもコンテナが起動していないところに、security.idmap.isolated を設定したコンテナを起動しようとしても失敗します。

少なくとも 131073 以上の設定が必要です。まあ、ケチることはないので先の例のように多めに確保しておきましょう。

profile の設定

default プロファイルの設定を変えてもいいですが、同じ ID 範囲で起動するコンテナを起動したい場合のためにそれは置いておいて、新しいプロファイルを作ります。

$ lxc profile copy default secure
$ lxc profile set secure security.idmap.isolated true
$ lxc profile show secure
config:
  security.idmap.isolated: "true"
  : (snip)

コンテナの起動

$ lxc launch ubuntu:18.04 c1 --profile=secure
Creating c1
Starting c1
$ lxc launch ubuntu:18.04 c2 --profile=secure
Creating c2
Starting c2
$ ps aux
  : (snip)
root     17072  0.0  0.1 604544  7324 ?        Ss   14:03   0:00 [lxc monitor] /var/lib/lxd/containers c1
165536   17093  0.2  0.2 159472  8892 ?        Ss   14:03   0:00  \_ /sbin/init
  : (snip)
root     17935  0.0  0.1 529148  7196 ?        Ss   14:03   0:00 [lxc monitor] /var/lib/lxd/containers c2
231072   17953  0.2  0.2 159420  8840 ?        Ss   14:03   0:00  \_ /sbin/init
  : (snip)

はい、違うユーザ権限で起動していますね。

cgroup v2 の nsdelegate オプション(1)〜 namespace 外へのプロセス移動の禁止

cgroup v2がカーネルに導入された時点では、cgroup v2にはマウントオプションはありませんでした。

しかし、4.13 で nsdelegate というオプションが導入されました。これは現時点でも cgroup v2 唯一のマウントオプションです。

このオプションは初期の namespace でマウントするときにのみ指定できます。それ以外の namespace では無視されます。

cgroup namespace については、私の連載第34回 で説明しました。ここでは、cgroup namespace で cgroup ツリーを独立させた後でも、cgroup namespace 内の /(root)を超えてプロセスを移動できました。

これはある意味コンテナ内のプロセスを、別のコンテナに移動させるという意味になります。

普通にコンテナを起動すると、mount namespace を分離させますので、他のコンテナのファイルシステムは見えないはずです。したがってこのようなコンテナをまたいだプロセスの移動はできないはずです。しかし、何らかの理由で別の cgroup 階層が見えるような場合は移動ができることになります。このような操作は通常は行わないケースがほとんどで、禁止したいケースがほとんどであると思います。

nsdelegate を使うとこれを禁止できます。

試してみましょう。以下は Plamo 7.0(4.14.44 kernel)で試しています(sysvinit バンザイ!)。

namespace をまたいだプロセスの移動

nsdelegate がないとき

オプションを指定せずに cgroup v2 をマウントします。これは、私の連載第34回で説明しています。詳しくはそちらをどうぞ。

# mount -t cgroup2 cgroup2 /sys/fs/cgroup/

test01test02 cgroup を作成します。

# mkdir /sys/fs/cgroup/test0{1,2}

現在のシェルの PID を test01 に登録します。

# echo $$ | tee /sys/fs/cgroup/test01/cgroup.procs 
4213

unshare で cgroup namespace を作成してシェルを起動します。

# unshare --cgroup -- /bin/bash

起動したシェルは親 cgroup と同じ cgroup に属することになるので、namespace 作成時点で親と自身が cgroup の /(root)にいることになります。namespace 作成時点にいる cgroup が namespace 内では root となる、これが cgroup namespace でした。

# echo $$
4284
# cat /proc/4213/cgroup 
0::/
# cat /proc/4284/cgroup 
0::/

親の namespace から見ると /test01 cgroup にいることになっています。ちゃんと namespace として働いているのがわかりますね。

parent namespace # cat /proc/4213/cgroup 
0::/test01
parent namespace # cat /proc/4284/cgroup 
0::/test01

ここでおもむろに現在のシェルの PID を test01 と同じレベルの別階層にある test02 に登録します。

# echo $$ > /sys/fs/cgroup/test02/cgroup.procs

すると /(root)の一つ上の test02 を表す /../test02 という cgroup に属することになっています。/test01 が root になっているので、同じレベルの別階層だとこうなるのはわかりますね。

# cat /proc/$$/cgroup 
0::/../test02

nsdelegate があるとき

nsdelegate 機能を使うには cgroup v2 をマウントする際に nsdelegate オプションを指定します。既に cgroup v2 がマウント済みの場合は -o remount,nsdelegate と指定して再マウントすれば使えるようになります。

# mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/

先ほどと同様に test01test02 cgroup を作りましょう。

# mkdir /sys/fs/cgroup/test0{1,2}

そして、test01 にシェルのプロセスを登録し、所属する cgroup を確認します。

# echo $$ > /sys/fs/cgroup/test01/cgroup.procs 
# unshare --cgroup /bin/bash
# cat /proc/$$/cgroup 
0::/

test01 が root cgroup になりました。ここまではさきほどと同じですね。

ここで先程と同じようにシェルの PID を test02 に移動させてみましょう。

# echo $$ > /sys/fs/cgroup/test02/cgroup.procs 
bash: echo: write error: No such file or directory

エラーになりました。nsdelegate を指定して cgroup v2 をマウントすると、このように namespace の境界(root)をまたいで、別階層の cgroup にプロセスを移動できません(ENOENT が返ります)。

nsdelegate には他にも重要な機能がありますので、次回にでも。

LXD コンテナに物理NICを直接与える

あまり役に立たないメモです。

コンテナホスト上でコンテナを起動する場合、ホスト上にブリッジを作成し、そこにアタッチする veth インターフェースを接続する場合が多いかと思います。

しかし、ベアメタル上に物理 NIC が多数あったり、SR-IOV で物理 NIC 上で仮想的な NIC が多数作成できる場合は、LXD コンテナに直接 NIC をアタッチできます(ただし、最新の LXD は SR-IOV 対応していたのでこのブログエントリは関係ないかも? < 良く知らない)。

コンテナホスト上にブリッジを作成し、veth でアタッチしてコンテナを起動する場合、コンテナに割り当てる profile は default として、veth でホスト上のブリッジにアタッチするデバイスが定義されています(環境依存です)。

ホスト上の NIC を直接割り当てる場合はこのような profile は無駄ですので、一旦デバイスとして nic が存在しないコンテナ用 profile を作成します。これは default プロファイルをコピーすれば良いでしょう。

LXD 用に、ネットワークは定義しないプロファイル nonet を定義しましょう。defaultlxd init 時に設定されそうなので環境依存だと思う)をコピーして lxc profile edit nonet とかやるとエディタで編集できます。

$ lxc profile copy default nonet
$ lxc profile edit nonet
$ lxc profile show nonet
config: {}
description: Default LXD profile
devices:
  root:
    path: /
    pool: default
    type: disk
name: nonet
used_by: []

コンテナを作ります。

$ lxc init ubuntu:18.04 --profile=nonet
Creating the container
Container name is: still-hamster              
$ lxc list
+---------------+---------+------+------+------------+-----------+
|     NAME      |  STATE  | IPV4 | IPV6 |    TYPE    | SNAPSHOTS |
+---------------+---------+------+------+------------+-----------+
| still-hamster | STOPPED |      |      | PERSISTENT | 0         |
+---------------+---------+------+------+------------+-----------+

ネットワーク設定のないコンテナが作られます。

$ lxc config show still-hamster | grep devices
devices: {}

ここで、ホスト上にあるけど使われていない物理 NIC を確認してみましょう。次のようなアドレスも割当らず、UP していないインターフェース eth1 がありました。この eth1 が接続されているネットワークには DHCP サーバがあり、そこからアドレスがもらえるとします。

$ ip a
  : (snip)
3: eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 0e:9a:02:d4:20:38 brd ff:ff:ff:ff:ff:ff

これを先ほど作成したコンテナ still-hamster に割り当てます。

$ lxc config device add still-hamster eth0 nic nictype=physical name=eth0 parent=eth1
Device eth0 added to still-hamster

これでコンテナ内に eth0 という名前で、ホスト上の eth1 が割り当たります。

$ lxc config show still-hamster
  : (snip)
devices:
  eth0:
    name: eth0
    nictype: physical
    parent: eth1
    type: nic
  : (snip)

コンテナを起動します。

$ lxc start still-hamster 
$ lxc list
+---------------+---------+--------------------+------+------------+-----------+
|     NAME      |  STATE  |        IPV4        | IPV6 |    TYPE    | SNAPSHOTS |
+---------------+---------+--------------------+------+------------+-----------+
| still-hamster | RUNNING | 10.7.11.252 (eth0) |      | PERSISTENT | 0         |
+---------------+---------+--------------------+------+------------+-----------+

起動してアドレスも割当たっています。ちなみに Ubuntu:18.04 イメージでは、DHCP でアドレスをもらえる設定がなされています。

$ lxc exec still-hamster -- cat /etc/netplan/50-cloud-init.yaml
  : (snip)
network:
    version: 2
    ethernets:
        eth0:
            dhcp4: true

コンテナ内でルーティングテーブルを見てみると次のようにちゃんと割当たっていることがわかります。

$ lxc exec still-hamster -- ip route show table main
default via 10.7.0.1 dev eth0 proto dhcp src 10.7.11.252 metric 100 
10.7.0.0/16 dev eth0 proto kernel scope link src 10.7.11.252 
10.7.0.1 dev eth0 proto dhcp scope link src 10.7.11.252 metric 100 

ちなみに、コンテナが起動した状態で eth1 を確認してみると、eth1 はコンテナの namespace に移動していますので、ホスト上からは見えません。

$ ip a | grep eth1
$ 

ここでは LXD での例を示しましたが、LXC でも設定ファイルにネットワークで物理インターフェースを指定することで同じことができます。

LXC 3.0 新機能の予習

ここ最近、新バージョンリリース時と、ドキュメント(man pages)に更新があったときに翻訳する以外、新しい機能について全く調査していませんでした。

なんとなく見てると LXC 3.0 が近いようですので、どう変わるのかをまとめてみます。

cgroup ドライバの整理

これまで LXC には cgroup 関連のドライバが 3 つ含まれていました。

  • cgfs
  • cgmanager
  • cgfsng

cgfs ドライバはもっとも古くからあるドライバです。今や cgroup は /sys/fs/cgroup にマウントされますが、昔は特にマウントする場所は決まっておらず、/dev/cgroup にマウントしたり、/cgroup にマウントしたりしていました。どこにマウントされるかわからないファイルシステムを検出するロジックなど、今や不要になったロジックが多く含まれています。

cgfs ドライバの機能は cgfsng でカバーされていますので、cgfs ドライバは削除されるようです。

cgmanager ドライバは Ubuntu 14.04 のあたりに導入された cgmanager を使って cgroup を管理するためのドライバでした。カーネルに cgroup namespace が導入された今となっては、cgmanager は不要で、すでに廃止予定のプロジェクトになっていますので、cgmanager ドライバも廃止されます。

テンプレートの整理

LXC 2.x までは、様々なディストリビューション用のコンテナイメージを作成するために、シェルスクリプトで書かれたテンプレートが各ディストリビューションごとに用意されていました。

LXC 3.0 では、ディストリビューション依存のテンプレートが削除されるようです。ただ、バサッと切り捨てるのではなく、lxc-templatesというプロジェクトに分離されました。

LXC プロジェクト配下には、新たに distrobuilder という、イメージを作成するための Go 言語によるツールの開発が始まっています。

distrobuilder

distrobuilder は、

という機能を持っているようです。

現時点では、各ディストリビューションが準備している

  • alpine イメージ(alpine-minirootfs-*.tar.gz)
  • arch イメージ(archlinux-bootstrap-*-x86_64.tar.gz)
  • CentOS イメージ(iso イメージ?)
  • Ubuntu イメージ(ubuntu-base--base-.tar.gz)

を使用してイメージを作成する機能と、

  • debootstrap

を使用してイメージを作成する機能があるようです。

各種言語バインディングの分離

LXC のソースアーカイブに含まれていた、

は独立したリポジトリに分離されるようです。

pam_cgfs の LXC への移動

ユーザログイン時に、ユーザ用の cgroup を作成するための pam モジュールとして pam_cgfs が LXCFS で開発されていましたが、LXC 3.0 からは LXC ツリー配下で開発されるようです。

cgroup v2 サポート

cgroup v2 がサポートされたようです。


他にもあるでしょうけど、とりあえずこのあたりで。

参考

GnuPGで鍵取得しようとするとdirmngrに繋がらないと怒られる

単なるメモ。Plamo-7.0 開発中環境でのお話。

gnupg 2.1.19 までは大丈夫なんだけど、gnupg 2.1.23、2.2.0、2.2.1 にすると、dirmngr がうまく動かない… (2.1.20 〜 22 は作ってないので知らない)。

$ gpg --recv-keys ()
gpg: connecting dirmngr at '/home/karma/.gnupg/S.dirmngr' failed: IPC connect呼び出しに失敗しました
gpg: 鍵サーバからの受信に失敗しました: dirmngrがありません

となる。この時、dirmngr は起動しているけど、

tcp        0      1 10.200.200.232:45602    127.0.0.1:9050          SYN_SENT    27056/dirmngr

のように、どうやら Tor に接続に行っている模様。これコンパイル時に無効化できんの? もしくはデフォルト使わないってできんの?

もちろん dirmngr.conf で使わないように設定すれば使わない。でも Plamo では、システムワイドで dirmngr 起動するわけではなく、ユーザごとに起動することになるので、ユーザの dirmngr.conf にいちいち書かなければならない。

$ cat ~/.gnupg/dirmngr.conf 
no-use-tor

いや、GnuPG 詳しくないから知らんけど

マニュアルには The default is to use Tor if it is available on startup or after reloading dirmngr. と書いてある。でも Tor なんて入ってないと思うんだけど…

GnuPGのdirmngrのコード見ると、dirmngr/server.c

/* This function returns true if a Tor server is running.  The status
 * is cached for the current connection.  */
static int
is_tor_running (ctrl_t ctrl)
{
  /* Check whether we can connect to the proxy.  */

  if (!ctrl || !ctrl->server_local)
    return 0; /* Ooops.  */

  if (!ctrl->server_local->tor_state)
    {
      assuan_fd_t sock;

      sock = assuan_sock_connect_byname (NULL, 0, 0, NULL, ASSUAN_SOCK_TOR);
      if (sock == ASSUAN_INVALID_FD)
        ctrl->server_local->tor_state = -1; /* Not running.  */
      else
        {
          assuan_sock_close (sock);
          ctrl->server_local->tor_state = 1; /* Running.  */
        }
    }
  return (ctrl->server_local->tor_state > 0);
}

こんな関数がある。ここで tor_state が 1 になってる?

libassuan を見ると、src/assuan-socket.c 内に

_assuan_sock_connect_byname (assuan_context_t ctx, const char *host,
                             unsigned short port, int reserved,
                             const char *credentials, unsigned int flags)
{
  assuan_fd_t fd;
  unsigned short socksport;

  if ((flags & ASSUAN_SOCK_TOR))
    socksport = TOR_PORT;
  else if ((flags & ASSUAN_SOCK_SOCKS))
    socksport = SOCKS_PORT;
  else
    {
      gpg_err_set_errno (ENOTSUP);
      return ASSUAN_INVALID_FD;
    }

  if (host && !*host)
    {
      /* Error out early on an empty host name.  See below.  */
      gpg_err_set_errno (EINVAL);
      return ASSUAN_INVALID_FD;
    }

  fd = _assuan_sock_new (ctx, AF_INET, SOCK_STREAM, 0);
  if (fd == ASSUAN_INVALID_FD)
    return fd;

  /* For HOST being NULL we pass an empty string which indicates to
     socks5_connect to stop midway during the proxy negotiation.  Note
     that we can't pass NULL directly as this indicates IP address
     mode to the called function.  */
  if (socks5_connect (ctx, fd, socksport,
                      credentials, host? host:"", port, NULL, 0))
    {
      int save_errno = errno;
      assuan_sock_close (fd);
      gpg_err_set_errno (save_errno);
      return ASSUAN_INVALID_FD;
    }

  return fd;
}

なんて関数がある。よくわからんw

いや、違うな。dirmngr に Tor のオプションを指定すると、オプションが --use-tor--no-use-tor かに関わらず dirmngr はすぐに起動するけど、何も指定しないと起動後 127.0.0.1:9050 へのアクセスをして、だいぶタイムアウトを待った後に、使わない設定で起動するな。dirmngr.conf を準備して、ちゃんと設定しろ、ということかな。なんて不便なソフトウェアだ。

$ strace dirmngr --server --homedir /home/karma/.gnupg -vvv --debug-all
  :(snip)
write(2, "dirmngr[27628]: enabled debug fl"..., 108dirmngr[27628]: enabled debug flags: x509 crypto memory cache memstat hashing ipc dns network lookup extprog) = 108
write(2, "\n", 1
)                       = 1
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(9050), sin_addr=inet_addr("127.0.0.1")}, 16
  :(↑でだんまり)
  :(snip)

うん、接続を延々待ってるわ。これで失敗したあとにちゃんと dirmngr は起動して、クライアントからのリクエストに応える。不便すぎて涙出るな。