TenForward

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

ping コマンドの file capability(2)

(理解が不十分なので間違っている所があれば優しく教えてください)

さて、ping コマンドの file capability(1) - TenForward で紹介したように ping コマンドの File capability は Permitted のみが有効になっていて、Effective ビットはオン(+e)になっていません。これで ping が実行できる理由は説明したとおりですが、+e が設定されていない理由は「不要だから」というだけの理由ではありません。

「なんで +e ないんだろう?」とつぶやいていると WhiteAnthrax さんが調べて教えてくれました!ありがとうございます!

これは

www.projectatomic.io

に書かれている通りです。Docker コマンド内の ping コマンドの実行で問題が起こるからということです。

まず Docker コンテナでは、普通に docker run すると cap_net_admin が許可されません。cap_net_adminping -m のように実行する際に必要です(man ping すると -m は "use mark to tag the packets going out." とのことです)。一方で ping を普通に実行する際に必要な cap_net_raw は許可されています。つまり Docker コンテナ内では ping コマンドで -m をつけても目的は達成できません。

ここで(これは明確に man capabilities には書いてない気がするけど…)

  • File capability で +e すると、指定したすべての capability が取得できない場合、コマンドを実行した瞬間に実行は失敗する

ということのようです。

特権が必要なシステムコールの実行が行われる、行われないに関わらず、すべての capability の取得に失敗すると、コマンド自体が実行できないということです。ping --help のような特権が不要な実行であってもです。

ということは、

  • Docker コンテナ内で cap_net_admin,cap_net_raw+ep と設定してしまうと、cap_net_admin が取得できないので、cap_net_raw のみが必要な実行であっても ping コマンドを実行した瞬間にコマンド実行が失敗する

ということです。

今回の ping コマンドはプログラム中で自身で capability をチェックして Permitted がオンであれば capset() を使って Effective を有効にしていますので、+p とだけしておくと、たとえ cap_net_admin が取得できなくてもコマンド実行は成功します。

つまりプログラム中で権限のチェックを行って capability をセットしていますので、

となります。それ以外のシステムコールは問題なく実行されるわけです。

ping -m と実行すると、プログラム中で cap_net_admin のチェックをしているところのみ失敗します。つまり "mark" だけが失敗して、cap_net_raw が必要な処理は行われます。

https://bugzilla.redhat.com/show_bug.cgi?id=1142311#c24 によると

[root@29b49db23971 /]# /opt/ping/ping -m 123 -c1 10.3.1.1
PING 10.3.1.1 (10.3.1.1) 56(84) bytes of data.
Warning: Failed to set mark 123
64 bytes from 10.3.1.1: icmp_seq=1 ttl=62 time=0.327 ms

のように mark 処理の部分だけ失敗して、それ以外の処理は行われるようです(試してませんw)。

というわけで +e してしまうと、ping コマンドはヘルプすら表示できず、本来の実行できる処理も実行できなくなるので、+p のみが設定されているということです。

(2019-09-30 追記) 『これは明確に man capabilities には書いてない気がするけど…』と書きましたが、id:udzura さんから man 2 execve

EPERM  A "capability-dumb" applications would not obtain the full set
       of permitted capabilities granted by the executable file.  See
       capabilities(7).

という説明があると教えてもらいました!(これでも曖昧ではあるけど "the full set" とあるのできっと…)

ping コマンドの file capability(1)

(いまさらのお話とは思いますが自分用のメモに)

LinuxCapability、何度も理解しようとして完全に理解できないままなんですが、ちょっとした理由で File capability を調べてます。最近は ambient capability なんてのもあって理解が遠のいてます。:-p

file capability といえば、まず登場するのはこのコマンドではないでしょうか?(違う?) Ubuntu 18.04.3 では次のように setuid されています。file capability は特に設定されていません。

$ ls -l /bin/ping
-rwsr-xr-x 1 root root 64424 Jun 28 20:05 /bin/ping
$ getcap /bin/ping
$ 

これは想定通りだったんですが、先日リリースしたばかりの CentOS 8 で見てみると、

$ ls -l /bin/ping
-rwxr-xr-x. 1 root root 69160 May 11 23:22 /bin/ping
$ getcap /bin/ping 
/bin/ping = cap_net_admin,cap_net_raw+p

setuid されておらず file capability が設定されていて、「おお、イマドキのセキュアな設定っぽい」と思いました(CentOS 7 からそうだったようなので今更感満載です)。でも +p と設定されています。あれ? +ep でじゃないとマズくない? +e がないと実行できなくない? 例のあのややこしい条件式からいっても。

$ man 7 capabilities
  : (snip)
           P'(ambient)     = (file is privileged) ? 0 : P(ambient)

           P'(permitted)   = (P(inheritable) & F(inheritable)) |
                             (F(permitted) & P(bounding)) | P'(ambient)

           P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

           P'(inheritable) = P(inheritable)    [i.e., unchanged]

           P'(bounding)    = P(bounding)       [i.e., unchanged]

それはそのとおりなのですが、CentOS で採用されている ping が含まれている iputils のコードを確認するとその理由がわかります(たぶん)。

#ifdef HAVE_LIBCAP
int modify_capability(cap_value_t cap, cap_flag_value_t on)
{
    cap_t cap_p = cap_get_proc();
    cap_flag_value_t cap_ok;
    int rc = -1;

    if (!cap_p) {
        error(0, errno, "cap_get_proc");
        goto out;
    }

    cap_ok = CAP_CLEAR;
    cap_get_flag(cap_p, cap, CAP_PERMITTED, &cap_ok);
    if (cap_ok == CAP_CLEAR) {
        rc = on ? -1 : 0;
        goto out;
    }

    cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &cap, on);

    if (cap_set_proc(cap_p) < 0) {
        error(0, errno, "cap_set_proc");
        goto out;
    }

    cap_free(cap_p);
    cap_p = NULL;

    rc = 0;
out:
    if (cap_p)
        cap_free(cap_p);
    return rc;
}

https://github.com/iputils/iputils/blob/4b0a0ab7898b093ef55c4a8cacea9a21c37a451f/ping_common.c#L189

CAP_PERMITTED を確認して、付与されていれば CAP_EFFECTIVE を設定してます(と思う)。

これで +ep でなく +p な理由がわかりました。ping コマンド自体で +p が付いていれば(というか付いている結果として permitted が設定されていれば、かな?)必要な capability を設定してシステムコールを呼ぶプログラムになっているからでした。

とはいえこれでは +e が不要な理由はわかりましたが、付けておいてマズい理由はわかりません。ま、セキュリティ観点から不要な権限は付けておく理由はない、というだけでも理由になりますが。

(続く)

tenforward.hatenablog.com

セサミ mini を導入してみた(3)

前回、わざわざアダプターを作って送ってもらったけれども改善しなかったセサミminiの鍵を使って施錠した場合の問題。

tenforward.hatenablog.com

セサミのサポートのメールで

硬めのクッション(セサミminiの箱の黒いクッションなど)をサムターンに貼って隙間を少し少なくして頂くことも有効かもしれません

とアドバイスをいただいていたのと、ブログへのコメントをいただいた id:UnaStrada さまのアドバイスで、

unastrada.hatenablog.jp

このブログエントリのように、吸水テープで隙間をなくすと良さそうということで試してみました。

とりあえず百均で隙間テープと防水テープというものを買ってきました(Can Doです)。

Can Do購入の防水テープ

隙間テープはスポンジ状の比較的柔らかい材質のものでしたが(捨ててしまって写真ないや)、これだと柔らかすぎてサムターンとセサミに押しつぶされる感じで隙間に入れた意味がありませんでした。少し硬い材質が良いのかもということで、この防水テープを使ってみました。

防水テープを装着したセサミmini

これだとサムターンの幅より狭いのですが、アダプターをこれ以上広くすると貼り付けた意味がなかったので、狭めにして無理矢理サムターンを隙間に押し込む感じで設置しました。

防水テープ付きの設置状態

これで無事鍵で施錠してもサムターンはそれなりの角度になるようになったので、鍵でもアプリでも施錠状態にできるようになりました。

f:id:defiant:20190718004817p:plain
鍵で閉めた場合のサムターン位置

ところで使い続けているとアダプターに結構力がかかるのかアダプターが開いてきませんかね?サムターン受けパーツが開いてくるような。根元に結構力かかってるような…(↑の画像は無理矢理押し込んでるので開くのは仕方ないけど、防水テープ貼る前にしばらく使ってたら結構開いていたような)

P.S. ここに来て WiFi モジュールが不調? WiFi 経由で接続できないことがあったので何度かコンセント抜き差しとリセットしてみて様子見(コンセント抜き差しして復活して数日後またアクセスできず…)

ShiftFS ふたたび 〜 Ubuntu 19.04で導入された ShiftFS を試してみた

ちょうど 2 年ほど前、Open Source Summit で話を聞いて ShiftFS について試したことがありました。

tenforward.hatenablog.com

その後はとりあえずカーネルにパッチを当てたりはしてましたが、あまり使わないままでした。少し前に gihyo.jp の Ubuntu の記事を読んでいると、

discoのカーネルフリーズが4月4日に迫る中,興味深いパッチが投稿されています。パッチの中身は「ShiftFS」と呼ばれる,コンテナ環境でのセキュリティ機能を提供するためのラッパーファイルシステムです
https://gihyo.jp/admin/clip/01/ubuntu-topics/201903/29

なんて書かれています。その後の記事で

ShiftFSは, 前回取り上げた後,4月4日に無事にマージに辿り着いています。
https://gihyo.jp/admin/clip/01/ubuntu-topics/201904/19

と書かれていたので、また変わった所がないか調べて試してみたいなと思っていました。

そこでようやく重い腰を上げて、Ubuntu 19.04 環境を用意して試してみました。

ShiftFS について

詳しくは先に紹介した 2 年前の記事を参照してください。

簡単に言うと、非特権コンテナを起動する場合、どの uid/gid で起動するかは一般的に決まっているわけではないですが、世間で流通しているコンテナイメージ内のファイルの所有権は作成時点で一意に決まりますし、自作のコンテナイメージでも同様でしょう。

そこで非特権コンテナを起動するときには、それを起動するユーザの uid/gid に合わせて chown してあげる必要があります。

この問題を解消するために考えられたのが ShiftFS です。

ShiftFS は OverlayFS のように重ね合わせのファイルシステムとして実装されているようです。元のファイルシステムを「下層」(lower filesystem)とし、その上に「上層」(upper filesystem)のファイルシステムを重ねて所有権を調整しているのでしょう(たぶん)。

この ShiftFS は Ubuntu 19.04 の 5.0 カーネルにパッチを適用して使えるようにしており、upstream のカーネルには導入されていない機能です(されそうなのかしら?)。

Ubuntu は 12.04 だったか 14.04 の頃も、まだ upstream な Linux カーネルに導入されていない OverlayFS を先取りして入れていました(カーネルに入ったときには仕様が変わっていて LXC にパッチ送ったりしたのはいい思い出)。また 16.04 なんかでも、かなり後のカーネルで導入される機能がバックポートされていたりして、かなり意欲的に自分たちの求める機能を入れてきます。

ShiftFS によるマウント

マウント方法は2 年前の記事と大きく変わっていません。マウントオプションが増えた程度でしょうか。

マウントは 2 段階で行います。

ShiftFS がないとき

これも2 年前の記事と同じなのですが、簡単に紹介しておきましょう。

ここではファイルシステムとして LXC コンテナのイメージを展開して使用しています(alpineイメージ)。検証のために一部ディレクトリの所有権などをいじっています。root 権限で作成されたコンテナです。

コンテナファイルシステムの root(/)は /var/lib/lxc/ct01/rootfs です。

$ sudo ls -l /var/lib/lxc/ct01/rootfs
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

上の例のように rootfs 以下は root 所有です。

現在のユーザーをコンテナ内の rootマッピングして User Namespace を作成します。

$ id -u && id -g
1000
1000
$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# ls -l /var/lib/lxc/ct01/rootfs/
total 68
drwxr-xr-x  2 nobody nogroup 4096 Jul  1 13:00 bin
drwxr-xr-x  3 nobody nogroup 4096 Jul  2 07:17 dev
drwxr-xr-x 19 nobody nogroup 4096 Jul  2 07:17 etc
drwxr-xr-x  2 nobody nogroup 4096 Jun 19 17:14 home
drwxr-xr-x  6 nobody nogroup 4096 Jul  1 13:00 lib
drwxr-xr-x  5 nobody nogroup 4096 Jun 19 17:14 media
  : (snip)

上のように uid/gid=1000/1000 のユーザーをコンテナ内の uid/gid=0/0 にマッピングしただけなので、マッピングされていない uid/gid については nobody/nogroup の 65534/65534 となってしまっています。

ShiftFS があるとき

まずはコンテナの root(/)である /var/lib/lxc/ct01/rootfs を ShiftFS の下層(lower filesystem)としてマークします。この場合、マウントオプションに mark を指定します。

$ sudo mount -t shiftfs -o mark /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
[sudo] password for ubuntu: 
$ grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs

ShiftFS としてマウントされています(このイメージは root 所有なので、ここでは root ユーザーで mark しています)。

ここで、さきほどと同じように User Namespace を作成します。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

そしてさきほど mark した ShiftFS マウントを使って、権限のあるディレクトリに ShiftFS マウントしてみます。

# mount -t shiftfs /var/lib/lxc/ct01/rootfs/ /home/ubuntu/mnt
# grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
device /var/lib/lxc/ct01/rootfs mounted on /home/ubuntu/mnt with fstype shiftfs

マウントされています。マウント先の /home/ubuntu/mnt を確認してみると、

# ls -l /home/ubuntu/mnt
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

ちゃんと root 所有になっており、ShiftFS が効いていることがわかります。

ちなみにこの「下層」(mark)のマウントはホストのNamespaceで行っているので、ホストから見えますが、ホストでは「上層」は見えません。

$ grep shiftfs /proc/self/mountstats (ホスト上で実行)
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
$

コンテナ起動後は、余計な情報を見せないためにホスト上ではこの mark 用 ShiftFS は umount できます。

動きは 2 年前の変わっていないようです。

passthrough オプション

Ubuntuのパッチでは ShiftFS のオプションとして passthrough オプションが追加されているようです。2 年前の記事の時点ではおそらくなかったオプションだと思います。

さきほどの例では何も指定していませんので、passthrough=0 が指定されているのと同じ意味になるようです。

さきほどの例でマウントした ShiftFS で 1 が指定されていない効果を確認してみます。

# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: UNKNOWN (0x6a656a62)
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3014986    Available: 2801214
Inodes: Total: 1048576    Free: 972147

ファイルシステムタイプとして Type: UNKNOWN と返っています。

(2 の指定は気軽に試すには私にはハードルが高そうなので、)とりあえず 3 を指定してみます。

$ sudo mount -t shiftfs -o mark,passthrough=3 /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
$ grep shiftfs /proc/self/mountinfo 
540 28 0:53 / /var/lib/lxc/ct01/rootfs rw,relatime shared:294 - shiftfs /var/lib/lxc/ct01/rootfs rw,mark,passthrough=3

passthrough=3 付きでマウントされています。

では User namespace を作成しましょう。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# mount -t shiftfs -o passthrough=3 /var/lib/lxc/ct01/rootfs /home/ubuntu/mnt
# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3019139    Available: 2805367
Inodes: Total: 1048576    Free: 972149

stat コマンドで確認すると Type: ext2/ext3 と返ってきています。

2 の ioctl() 周りですが、ShiftFS のパッチ(の一部) を見る限りでは、

static long shiftfs_ioctl(struct file *file, unsigned int cmd,
              unsigned long arg)
{
    switch (cmd) {
    case FS_IOC_GETVERSION:
        /* fall through */
    case FS_IOC_GETFLAGS:
        /* fall through */
    case FS_IOC_SETFLAGS:
        break;
    default:
        return -ENOTTY;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

static long shiftfs_compat_ioctl(struct file *file, unsigned int cmd,
                 unsigned long arg)
{
    switch (cmd) {
    case FS_IOC32_GETVERSION:
        /* fall through */
    case FS_IOC32_GETFLAGS:
        /* fall through */
    case FS_IOC32_SETFLAGS:
        break;
    default:
        return -ENOIOCTLCMD;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

のような実装になってるので、ここに挙がってるリクエストは許可されているのでしょう(知らんけど)。

LXD

ちなみに LXD ではすでに ShiftFS が使えるようになっています(3.12以降)。

github.com

使い方は次で説明されています。

discuss.linuxcontainers.org

セサミ mini を導入してみた(2)

先日設置したセサミ mini ですが、先日も少し書いたとおり、ちょっとだけ問題がありました。

tenforward.hatenablog.com

うちの鍵は

  • サムターンが縦(12時)方向で解錠
  • サムターンが縦から右に45度回転した状態でチェーンがかかった状態
  • サムターンが横(3時)方向で施錠

となります。

サムターンの施錠、解錠状態はそれぞれ少々の遊びがあって、縦から少し行き過ぎた状態と少し手前の状態をぐらぐら動く感じです。

そしてセサミ mini のサムターンアダプターとサムターンの間には少し隙間があり、セサミ mini のつまみ自身も色々な方向に少し遊びがあります(設置が少しズレたときの対応や、色々な鍵への対応を考えてそういう設計になっているようです)。

色々な所に余裕と遊びがある関係で、うちに設置すると:

解錠状態: セサミ mini のつまみがちょうど縦になる状態だと、鍵のサムターンが解錠状態の縦にならず、少し手前で止まってしまい、開場されずにチェーンがかかった状態になる。そこで解錠状態は少し縦より行き過ぎた状態で設定する。

f:id:defiant:20190520233705p:plain
解錠状態のセサミの角度

これくらいの角度を解錠に設定します。こちらが問題になることはありません。

施錠状態: 問題は施錠状態です。

セサミのつまみが真横になった状態を「施錠状態」に設定すると、鍵のサムターンは横(3時)の少し手前で止まってしまい、きちんと施錠されずにチェーンがかかった状態になります

そこで少し行き過ぎたところ(次の画像の通り)あたりに設定します。セサミアプリを使って施錠するとこれで施錠できます

f:id:defiant:20190609133745j:plain
セサミの施錠状態のつまみ角度

ところが鍵を使って施錠すると、鍵のサムターンは横(3時)を向きますが、セサミ miniのつまみは真横のちょっと手前の状態で止まります。

f:id:defiant:20190609133721j:plain
鍵で施錠した際のセサミの角度

この状態だとセサミは施錠状態のところまでつまみが回っていないと認識し、アプリで見ると "Unlocked" という表示のままです。

この状態でアプリをタップすると、施錠状態に移行します。再度タップすると解錠されます。つまり 2 度タップしないといけないことになります。

上記ツイートを行うと、公式に補足され、ありがたいことにタダでアダプターを作成し送ってくれるとのことですので、サムターンの詳細なサイズを送りました。

届いたのでアダプターを装着しました。装着したアダプターとセサミのサムターンアダプターとの間にも少し隙間があります。

f:id:defiant:20190609132335j:plain
届いたアダプターを装着

そして設置。写真のようにアダプターをつけてもサムターンとの間には隙間があります。

f:id:defiant:20190609133839j:plain
作ってもらったアダプターを付けて設置

このため、

サムターン←(すき間)→作成してもらったアダプター←(すき間)→セサミ miniのサムターンアダプター

という状態になり、やはりいくら調整してもセサミ mini の施錠状態の角度と鍵を使った時の施錠状態の角度の間に差が出来てしまい、症状は改善されませんでした。

とはいうものの、便利さが勝っていますので、作ってもらったアダプターは取り外し、元の状態で再度装着して使っています。

アプリでの対応が待たれますね。

セサミの公式サポート手厚すぎですね。安心できる対応ですね。