TenForward

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

スマートロック セサミmini その後

(特に検証していない私の勝手な推測が入った記事です)

スマートロック「セサミmin」、導入以来便利に使っていてなくてはならないものになっています。

ここしばらくセサミで鍵を開閉する際のキレが鈍いようで、これはパーツがヤバい予兆かも、と思って昨晩コレを注文したんですよ。

で翌日でかけて帰ると家に入れない!(中から開けてもらいましたが)なんという神タイミング!!金具がこんな風になって壊れてました。

f:id:defiant:20191212012145j:plain

以前、鍵に遊びがあるために、物理的な鍵で閉めた時とセサミで閉めた時の角度が合わなかったとき、それを解決するために、CANDY HOUSEのサポートと UnaStrada さんのアドバイスを元に、サムターン受けにクッションを貼ってなんとか調整しようとしていました。

tenforward.hatenablog.com

このあと、金具に力がかかるのか一度金具が今回と同じように破損しました。そして、↑のブログに書いたクッションよりはもう少し柔らかめの隙間テープを片側に貼って(もう片側は↑のブログの通りのちょっと硬めのやつ)運用していました。ちなみにこの時点で、鍵とアプリの閉めた位置の角度のズレはうまく解決できずあきらめ状態でした。

これでも力がかかるのか、片側の金具が少し開いてきていました。↑の回の写真でもちょっと片側の金具が開いた感じになってるのがわかるかと思いますし、今回取り外して金具単体で撮ってみたのが次の写真です。わかりづらいですが、よく見るとセサミに取り付ける部分とサムターン受けパーツを取り付ける部分は直角であるはずですが、少し傾いているのがわかるかと(わからんかw)。

f:id:defiant:20191212001652j:plain

今回、この開いた側の金具が折れたのかと思ったら、開いた金具が反対側になるので逆側に変な力が加わったのか、逆側がボキッと折れてしまっていたわけです。根元の部分が一番弱い箇所で力がかかるのか前回もここから曲がってきて破損しました。

これは、うちの鍵に遊びがあるのと、セサミ自体も上下左右に遊びがあるために、サムターン受けの隅っこの方でサムターンの隅っこを掴んで回すような形になることがあり、力がかかるのかと思います。こんな感じ(ちょっと大げさに書いてますし、勝手な推測です)。

f:id:defiant:20191212010225j:plain

さらには、サムターンを垂直で「開」、水平で「閉」にせずに、いずれもちょっと行き過ぎた位置で位置設定をしているため、手で開閉した場合、上の写真のようにセサミのサムターンがちょっと行き過ぎた位置で止まってしまい、「開」の位置なのに更に手で開けてしまおうとしたり、「閉」の位置なのにさらに手で閉めようとしてしまったりして、変な力が加わっていることも考えられます(私は注意しているけど家族みんながそんなに注意してセサミを扱っているわけではない)。(←セサミのサムターンの角度から、今開いているのか閉まってるのかの判別がしにくいんです)

というわけで、セサミの作り上、

  • 鍵のサムターンに遊びがある場合は鍵で閉めた場合とセサミで閉めた場合のサムターン位置に差が出てしまう

というこれまでにあった問題以外にも、

  • 鍵にも遊びがある上に、セサミにも上下左右方向に遊びがあるためにサムターン受けパーツに力がかかってしまい根元で折れる

という問題があるので、鍵に遊びがある場合はちょっと気をつけた方が良さそうです。

Qurio はウェブで見る限りはサムターン受けに遊びがなさそうな気がしますので、こういう問題は起こらないのかな?

結局以前 CANDY HOUSE さんに作っていただいた、結局使っていなかったアダプターを付けることにしました。↓参照。

tenforward.hatenablog.com

このアダプターは上下方向の遊びがなく、サムターン受けの上下方向の位置が固定されるため、サムターン受けが上下に動くことによってサムターン受けに力がかかることがないかなと思ったからです。これでも本来のサムターン受けは遊びがあって動くのですが、変に力が加わることはない程度しか動かないかなと。

このアダプターをつけても、鍵で閉めた場合とアプリで閉めた場合の角度の差は埋められなかったので、鍵で閉めた場合とセサミで閉めた場合の位置の差については完全にあきらめることに。

f:id:defiant:20191212002157j:plain

要望

ついでに要望を書いておこう。以前も書いたのですが、

  • 開閉位置の設定で、「手動で鍵で閉めた場合」と「アプリで閉めた場合」の位置をそれぞれ設定できるようにしてほしい

これで鍵とアプリの位置の差は吸収できると思います。ついでに書いておきますが、

  • Android 版アプリの場合、アプリを起動してから開閉ができるようになるまでの時間がかかりすぎ。逆に iOS 版はアプリ起動から開閉できるようになるまでの時間が早い気がします。これくらいの時間になれば…

子供はこの時間が待ちきれなくてアプリでの開閉を止めましたので。アプリ自体あまりアップデートしてないけど改良してるのかな? あとは、

  • 今回破損した金具、いかにも根元に力がかかって弱そうなのでもう少し構造的にどうにかならないのか?

って思います。

ICMP sockets

連載の第42回

ファイルケーパビリティは安全のためにコピーすると設定が外れます。

なんてことを書いてますが、同僚から「Arch Linux でやるとなぜか ping が実行できてしまう」という情報が。確かに Arch でやると実行できてしまいます。

$ lsb_release -d
Description:    Arch Linux
$ cp /usr/bin/ping .
$ getcap ./ping 
$ ./ping -c 1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.025 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.025/0.025/0.025/0.000 ms

strace とかで見てみるとどうも SOCK_RAW でなく SOCK_DGRAM を使ってる模様。どうやらこれらしい。hayajo さんからも同じ情報を頂いて自信を持って、これを手がかりにググってみると、

icmp(7) にも記載が。えー、2.6.39 から?

$ man icmp
    :(snip)
       ping_group_range (two integers; default: see below; since Linux 2.6.39)
              Range of the group IDs (minimum and maximum  group  IDs,  inclu‐
              sive) that are allowed to create ICMP Echo sockets.  The default
              is "1 0", which means no group is allowed to  create  ICMP  Echo
              sockets.

いやー、Linux 奥が深いですね。こんなの知りませんでした。Arch を確認してみると確かに設定されています。

$ sysctl -a 2>/dev/null | grep ping_group_range 
net.ipv4.ping_group_range = 0    2147483647

Plamo だと

$ /sbin/sysctl -a 2>/dev/null | grep ping
net.ipv4.ping_group_range = 1    0

Arch の値はやりすぎ感あるのでとりあえず 0〜65534 とすると、

$ sudo sysctl -w net.ipv4.ping_group_range="0 65534"
net.ipv4.ping_group_range = 0 65534
$ /sbin/sysctl -n net.ipv4.ping_group_range
0  65534

設定できたので、

$ cp /bin/ping .
$ /sbin/getcap ./ping 
$ ./ping -c1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) バイトのデータ
64 バイト応答 送信元 127.0.0.1: icmp_seq=1 ttl=64 時間=0.013ミリ秒

--- 127.0.0.1 ping 統計 ---
送信パケット数 1, 受信パケット数 1, パケット損失 0%, 時間 0ミリ秒
rtt 最小/平均/最大/mdev = 0.013/0.013/0.013/0.000ミリ秒

実行できました。

ちなみに「安全のためにコピーしたら外れる」とか書きましたが、cp コマンドでも CAP_SETFCAP ケーパビリティがあれば --preserve=xattr とかやればファイルケーパビリティ保存したままコピーされますね。

(補足・2019-11-29) Twitter で systemd がそのように設定していると教えていただきました。ありがとうございます。

        * This release enables unprivileged programs (i.e. requiring neither
          setuid nor file capabilities) to send ICMP Echo (i.e. ping) requests
          by turning on the "net.ipv4.ping_group_range" sysctl of the Linux
          kernel for the whole UNIX group range, i.e. all processes. This
          change should be reasonably safe, as the kernel support for it was
          specifically implemented to allow safe access to ICMP Echo for
          processes lacking any privileges. If this is not desirable, it can be
          disabled again by setting the parameter to "1 0".

ケーパビリティバウンディングセット

今更感たっぷりな話題ですが、今頃ケーパビリティについて調べてます。これまで何度も調べてはよくわからなくて挫折を繰り返してるとこです。

ケーパビリティについては

udzura.hatenablog.jp

が詳しいのですが、「ケーパビリティバウンディングセット」の部分だけはちょっと良くわからんのですよね…(偉そうにスミマセン)。

そして、udzura さんのブログも Ambient については記載がまだありませんので、Ambient についてはこちら、

nojima.hatenablog.com

現時点では日本語ではここしかありません(勉強させてもらいました)。

そういうわけで、ケーパビリティバウンディングセットについて調べていてもなんかよくわからんので調べながらメモ。メモなので脈絡なかったり、急になんの前提もなく何かが出てくるかもしれません。

基本はマニュアル

ただでもややこしいのに英語だとなあ、という場合は日本語訳あるけど古いんですよね。ただ、このケーパビリティバウンディングセットの部分は順序が変わっていたりするものの文章の訳としてはそのまま使える部分がほとんどなので、英語苦手な私には参考になります。Ambient ケーパビリティの説明がない頃のやつなんですよね。

結局、マニュアルをすみずみまで読まないとダメということですね。すみません、つまみ食いしてました。man だから入門書のように書くわけにはいかないけど、機能は全部書かないといけない。そのあたりのバランスを取っている結果か、ひとつの機能についてあっちとこっちに書いてある(ときには別の man page にある)なんてことがあるんだなあと思ったりしてます。

まずはマニュアルの"Thread capability sets"の項にある"Bounding"の説明。

Bounding (per-thread since Linux 2.6.25)
       The capability bounding set is a mechanism that can be used to
       limit the capabilities that are gained during execve(2).

            :(略)

       For more details on the capability bounding set, see below.

まずここには

ケーパビリティバウンディングセットは execve(2) の間に得られるケーパビリティを制限するのに使えるメカニズムです。

とあります。ふむふむ、execve(2)の際に使う機能機能なんだな…

で、ケーパビリティあるあるで、execve(2) の際のケーパビリティ決定のアルゴリズム。これがなければ始まらないという感じですね。

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]

関係するのはP'(permitted)を決定している所です。

詳しくはこっちを見ろと書かれている "Capability bounding set" の項を見ると、

Capability bounding set

    :(略)

    * During an execve(2), the capability bounding set is ANDed with the
      file permitted capability set, and the result of this operation is
      assigned to the thread's permitted capability set.  The capability
      bounding set thus places a limit on the permitted capabilities that
      may be granted by an executable file.

ここは日本語訳と特に変化してないので、そっちを参照:

    * execve(2) 実行時に、ケーパビリティバウンディングセットと ファイルの許
      可ケーパビリティセットの論理和 (AND) を取ったものが、 そのスレッドの許
      可ケーパビリティセットに割り当てられる。つまり、ケーパビリティバウンディ
      ングセットは、実行ファイルが認めている許可ケーパビリティに対して制限を
      課す働きをする。

つまりファイルケーパビリティで設定されているケーパビリティであってもバウンディングセットで許可されていなければファイルケーパビリティの設定が無効になるということです。

ところが、さきほどのアルゴリズムを見ると、この制限したセット (F(permitted) & P(bounding)) は inheritable の条件である (P(inheritable) & F(inheritable)) との OR ですので、バウンディングセットで制限したとしてもプロセスとファイルの inheritable で許可されていれば、制限できないことになります。これは man の後の段落に書かれています。

Note that the bounding set masks the file permitted capabilities, but
not the inheritable capabilities.  If a thread maintains a capability
in its inheritable set that is not in its bounding set, then it can
still gain that capability in its permitted set by executing a file
that has the capability in its inheritable set.

日本語訳:

バウンディングセットがマスクを行うのは、継承可能ケーパビリティではなく、
ファイルの許可ケーパビリティのマスクを行う点に注意すること。あるスレッ
ドの継承可能セットにそのスレッドのバウンディングセットに 存在しないケー
パビリティが含まれている場合、そのスレッドは、継承可能セットに含まれ
るケーパビリティを持つファイルを実行することにより、許可セットに含ま
れるケーパビリティも獲得できるということである。

あくまでバウンディングセットはファイルケーパビリティの permitted を制限するだけで、inheritable で許可されれば、permitted に追加できてしまいます。これじゃあ制限できないのでは? と思いますが、ここで man の次の項目を見てみましょう。

* (Since Linux 2.6.25) The capability bounding set acts as a limiting
  superset for the capabilities that a thread can add to its inheri‐
  table set using capset(2).  This means that if a capability is not
  in the bounding set, then a thread can't add this capability to its
  inheritable set, even if it was in its permitted capabilities, and
  thereby cannot have this capability preserved in its permitted set
  when it execve(2)s a file that has the capability in its inherita‐
  ble set.

これも日本語訳

* (Linux 2.6.25 以降) ケーパビリティバウンディングセットは、スレッドが
  capset(2) により自身の継承可能セットに追加可能なケーパビリティの母集団
  を制限する役割を持つ。 スレッドに許可されたケーパビリティであっても、
  バウンディングセットに含まれていなければ、スレッドはそのケーパビリティ
  は自身の継承可能セットに追加できず、その結果、継承可能セットにそのケー
  パビリティを含むファイルを execve(2) する場合、そのケーパビリティを許
  可セットに持ち続けることができない、ということである。

プロセス(スレッド)は自身のケーパビリティを操作できます。しかし、バウンディングセットに含まれていないケーパビリティは自身の inheritable に追加できないということです。なので、そこから execve(2) してプログラムを実行する場合は、バウンディングセットに含まれていないケーパビリティはそれ以上持ち続けられない(継承できない)ということです。

つまり元々 inheritable にセットされていないけど、自身に capset(2) を使ってケーパビリティを設定しようと思っても、バウンディングセットに含まれていなければ inheritable に追加できません。結果的に execve(2) してもバウンディングセットに含まれていないケーパビリティを持てないということです。

と思って読んでたら、マニュアルの「ケーパビリティバウンディングセット」の最後に書いてあるやん。

Removing a capability from the bounding set does not remove it from
the thread's inheritable set.  However it does prevent the capability
from being added back into the thread's inheritable set in the
future.
バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加することはできなくなる。

capset(2)

ここで capset(2) を使ってのケーパビリティの追加を見てみましょう。同じくマニュアルの CAP_SETPCAP の解説、

CAP_SETPCAP
       If file capabilities are supported (i.e., since Linux 2.6.24):
       add any capability from the calling thread's bounding set to
       its inheritable set; drop capabilities from the bounding set
       (via prctl(2) PR_CAPBSET_DROP); make changes to the securebits
       flags.

          :(略)

日本語訳:

ファイルケーパビリティがサポートされている場合(Linux 2.6.24以降): 呼
び出し元スレッドのバウンディングセットの任意のケーパビリティを 自身の
継承可能ケーパビリティセットに追加できる。 (prctl(2) PR_CAPBSET_DROP
を使って) バウンディングセットからケーパビリティを削除できる。
securebits フラグを変更できる。

こはちょっとカーネルを追ってみました(5.3 kernel)。細かい所は(理解してないので :-p)すっとばしますが、まず capset システムコールkernel/capability.c にあります。その後は

sys_capset (kernel/capability.c)
  security_capset (security/security.c)
    cap_capset (security/commoncap.c) <- call_int_hook マクロ経由

という風に呼ばれ、実際の処理は security/commoncap.c 内の cap_capset 関数で行われているようです。

まずは CAP_SETPCAP を持っていない場合のチェックです。このチェックは cap_inh_is_capped 関数で行っており、 CAP_SETPCAP ケーパビリティを持っていれば 0 を、持っていなければ 1 を返します。

int cap_capset(struct cred *new,
               const struct cred *old,
               const kernel_cap_t *effective,
               const kernel_cap_t *inheritable,
               const kernel_cap_t *permitted)
{
        if (cap_inh_is_capped() &&
            !cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_permitted)))
                /* incapable of using this inheritable set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | permitted" のサブセットであるかどうかをチェックし、そうでなければエラーとなります。

先のチェックが CAP_SETPCAP がないときのお話でしたので、次は CAP_SETPCAP を持っている場合です。

        if (!cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_bset)))
                /* no new pI capabilities outside bounding set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | bset" (bset=バウンディングセット) のサブセットであるかどうかをチェックし、そうでなければエラーとなります。先に引用したマニュアルの「スレッドに許可されたケーパビリティであっても、バウンディングセットに含まれていなければ、スレッドはそのケーパビリティは自身の継承可能セットに追加できず」というのがこの部分でしょうか。

次は新たな permitted のチェックは、

        /* verify restrictions on target's new Permitted set */
        if (!cap_issubset(*permitted, old->cap_permitted))
                return -EPERM;

シンプルに現在の permitted のサブセットであれば OK です。

新しい effective のチェックは、

        /* verify the _new_Effective_ is a subset of the _new_Permitted_ */
        if (!cap_issubset(*effective, *permitted))
                return -EPERM;

新しい permitted のサブセットであれば OK です。

ここまでくれば新たなケーパビリティセットを設定できるということですので、次の 3 行で新たなケーパビリティとして設定しています。

        new->cap_effective   = *effective;
        new->cap_inheritable = *inheritable;
        new->cap_permitted   = *permitted;

最後に Ambient ケーパビリティです。

        /*
         * Mask off ambient bits that are no longer both permitted and
         * inheritable.
         */
        new->cap_ambient = cap_intersect(new->cap_ambient,
                                         cap_intersect(*permitted,
                                                       *inheritable));
        if (WARN_ON(!cap_ambient_invariant_ok(new)))
                return -EINVAL;
        return 0;
}

新たに決定したケーパビリティセットのうち permitted と inherited の両方で有効になっていないケーパビリティは ambient としては許可されない(マニュアルのAmbientの説明部分参照)ので、permitted & inherited の値と AND を取ります。(new->cap_ambient はどこで設定されてるん? <力尽きたので教えて)

マニュアルの "Capability bounding set" の残りの部分で現在のカーネルに相当する部分は日本語訳で OK です(わざわざ転載する必要もないけど説明として引用しておきます)。

カーネルのバージョンにより、ケーパビリティバウンディングセットはシス
テム共通の属性の場合と、プロセス単位の属性の場合がある。

Linux 2.6.25 以降のケーパビリティバウンディングセット

Linux 2.6.25 以降では、「ケーパビリティバウンディングセット」はスレッ
ド単位の属性である (システム共通のケーパビリティバウンディングセットは
もはや存在しない)。

バウンディングセットは fork(2) 時にはスレッドの親プロセスから継承され、
execve(2) の前後では保持される。

スレッドが CAP_SETPCAP ケーパビリティを持っている場合、そのスレッドは
prctl(2) の PR_CAPBSET_DROP 操作を使って自身のケーパビリティバウンディ
ングセットからケーパビリティを削除することができる。いったんケーパビ
リティをバウンディングセットから削除してしまうと、スレッドはそのケー
パビリティを再度セットすることはできない。 prctl(2) の PR_CAPBSET_READ
操作を使うことで、スレッドがあるケーパビリティが自身のバウンディングセッ
トに含まれているかを知ることができる。

バウンディングセットからのケーパビリティの削除がサポートされるのは、
カーネルのコンパイル時にファイルケーパビリティが有効になっている場合
だけである。Linux 2.6.33 より前のカーネルでは、ファイルケーパビリティ
は 設定オプション CONFIG_SECURITY_FILE_CAPABILITIES で切り替えられる追
加の機能であった。Linux 2.6.33 以降では、この設定オプションは削除され、
ファイルケーパビリティは常にカーネルに組込まれるようになった。ファイ
ルケーパビリティがカーネルにコンパイル時に組み込まれている場合、(全て
のプロセスの先祖である) init プロセスはバウンディングセットで全てのケー
パビリティが セットされた状態で開始する。ファイルケーパビリティが有効
になっていない場合には、 init はバウンディングセットで CAP_SETPCAP 以
外の全てのケーパビリティがセットされた状態で開始する。このようになっ
ているのは、 CAP_SETPCAP ケーパビリティがファイルケーパビリティがサポー
トされていない場合には 違った意味を持つからである。

バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加すること はできなくなる。

まとめ

ケーパビリティバウンディングセットは、

  • ファイルケーパビリティに設定されている permitted セットを制限する。しかし、プロセスとファイルの inherited で許可されていれば制限が効かない
  • capset(2) で、スレッドのケーパビリティバウンディングセットで設定されているケーパビリティしか、inherited に追加できない

と理解しました。(間違ってたら優しく教えてください)

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