RHEL7でpingをreniceしようとすると失敗する話

Red Hat Enterprise Linux 7では、linuxのcapabilityを利用してsetuidの利用を少なくしています。capabilityについてあまり馴染みがない方も多いかと思います。トラブルシュート形式で、capabilityの利用時の特徴的なふるまいを見てみましょう。

レッドハットの森若です。今回は昔から存在するのですがあまり利用されていなかったCapabilityについて、RHEL7で活用されているという話です。

謎のエラー

以下の操作は、RHEL6では問題なく動作するのですがRHEL7では失敗します。

$ ping localhost &>/dev/null &  #バックグラウンドでping
$ renice 15 $!  # nice値を設定して優先度を下げる

今回はなぜこのような違いが発生するのか、その背景を見ていきましょう。

RHEL7でこの操作をすると、以下のようなエラーが出力されます。

$ renice 15 $!
renice: failed to set priority for 4609 (process ID): Operation not permitted

権限管理を追跡

rootでreniceをやってみます。

# renice 15 4609
4609 (process ID) old priority 0, new priority 15

今度は成功しました。エラーメッセージも「Operation not permitted(操作が許可されていない)」というものでしたが、renice処理そのものが何かの理由でできなくなっているわけではなく、なにか権限の問題で失敗しているようです。

もう一度同じことをしてpingプロセスの状態を見てみます。

$ ping localhost &> /dev/null &
[2] 2850
$ cat /proc/$!/status
Name:    ping
State:    S (sleeping)
Tgid:    2850
Ngid:    0
Pid:    2850
PPid:    2693
TracerPid:    0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize:    256
Groups:    10 1000
VmPeak:      116500 kB
VmSize:      116476 kB
VmLck:           0 kB
VmPin:           0 kB
VmHWM:         996 kB
VmRSS:         996 kB
VmData:         264 kB
VmStk:         136 kB
VmExe:          40 kB
VmLib:        2160 kB
VmPTE:          60 kB
VmSwap:           0 kB
Threads:    1
SigQ:    0/15086
SigPnd:    0000000000000000
ShdPnd:    0000000000000000
SigBlk:    0000000000000000
SigIgn:    0000000000000000
SigCgt:    0000000000002006
CapInh:    0000000000000000
CapPrm:    0000000000003000
CapEff:    0000000000000000
CapBnd:    0000001fffffffff
Seccomp:    0
Cpus_allowed:    f
Cpus_allowed_list:    0-3
Mems_allowed:    00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:    0
voluntary_ctxt_switches:    15
nonvoluntary_ctxt_switches:    1

reniceはPermission deniedと出て失敗するので権限の問題らしいですが、uid, gidは自分のもの(1000)のままです。特に変なところがありません。これは通常のuid, gidから独立した権限管理が影響していそうです。

statで/usr/bin/pingの状態を確認してみましょう。

$ stat /usr/bin/ping
File: ‘/usr/bin/ping’
 Size: 44896         Blocks: 88         IO Block: 4096   regular file
Device: fd04h/64772d    Inode: 21449341    Links: 1
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:ping_exec_t:s0
Access: 2015-08-28 19:40:27.213100343 +0900
Modify: 2015-06-08 15:42:30.000000000 +0900
Change: 2015-07-24 12:37:03.285137514 +0900
Birth: -

setuidはついていませんね。uid, gidが同じであっても有効な権限管理としては、ファイルシステムのattribute, SELinux, ACL, Capability があります。

ファイルシステムのattributeは、ファイルの操作を(uid, gid, パーミッションの設定が同一であっても)制限することができます。今回はファイルのアクセスについての話ではないので影響しなさそうです。念のためlsattrコマンドでattributeを確認すると、どのフラグも立っていないので何も設定されていないことがわかります。

$ lsattr /usr/bin/ping
---------------- /usr/bin/ping

SELinuxの場合、特別に記録を禁止されていなければ/var/log/audit/audit.log にログが残ります。またRHEL7ではシステムログにもSELinuxでアクセスが禁止された旨の出力がおこなわれますが、reniceが失敗したような記録はありません。念のため setenforce 0 としてSELinuxをPermissiveにして実験しても同じようにreniceが失敗します。

ACLでしょうか? これもファイルシステムのattributeと同様ファイルの操作にひもづくので望み薄ですが一応確認しましょう。getfaclコマンドで確認してみると、通常のパーミッション以上の設定は行われていないことがわかります。

$ getfacl /usr/bin/ping
getfacl: Removing leading '/' from absolute path names
# file: usr/bin/ping
# owner: root
# group: root
user::rwx
group::r-x
other::r-x

Capabilityはどうでしょう? これは追加の権限を与えるものです。getcapコマンドでファイルのCapabilityを確認すると、cap_net_admin, cap_net_rawという権限が付与されていました。これがあやしいですね。

$ getcap /usr/bin/ping
/usr/bin/ping = cap_net_admin,cap_net_raw+p

Capabilityとは?

Capability とは何でしょう? rootユーザの権限を細かく分割したものだと考えることができます。rootユーザは一般ユーザでは禁止されている様々な操作が可能ですが、それぞれを分類し、名前をつけて個別に付与するなどの管理ができるようにしたものがCapabilityです。詳しくはman capabilities(7)を確認してみましょう。rootが持っている特権的な操作が列挙・分類されているので、capabilityを利用する予定がない人にも面白いとおもいます。

RHEL6ではpingにsetuidを使ってroot権限を利用していました。RHEL7ではcapabilityにより最小限の権限を付与しています。

  • RHEL6ではsetuidを使い、実行ファイルpingのownerがrootユーザなのでpingを実行するとrootユーザの権限が自動的に与えられます。root権限が必要な処理をしたあとsetuid(),setgid()によりpingプロセス自身を元のユーザの権限に制限しています。

  • RHEL7ではcapabilityを使い、ネットワーク管理に必要な操作を行う権限と、RAWソケットをopenして任意のパケットを送信する権限が与えられます。

この変更はセキュリティを改善するために導入されました。

pingの実装にセキュリティ上の問題があり、任意のコマンド実行が可能だと仮定してみましょう。この時rootユーザ権限で動作していると、/etc/shadowを一般ユーザが覗き見したり、/bin/bashを書き替えて何か悪いことをすることができてしまいます。

一方RHEL7のCapabilityでは、ネットワーク設定の変更やパケットの送信までは可能ですがファイルへのアクセスなどは元のユーザの権限のままです。このため、pingに未知の問題があったとしても被害が限定されます。

さきほどの/proc/$!/status での出力をよくみると、以下のような行があってCapabilityが追加されていることがわかります。

CapPrm:    0000000000003000

なぜreniceに失敗するのか?

さてRHEL7のpingはCapabilityで権限が追加されていることがわかりました。ではなぜreniceに失敗するのでしょうか?

該当するソースコードをlinux kernelのsecurity/commoncap.c から引用します。__task_cred(p)がreniceの対象になるプロセス(今回の例ではping)、current_cred() がreniceを実行しているプロセス(今回の例ではrenice)です。この2つの権限を比較して、サブセットになっていなければ失敗するようになっています。

/*
* Rationale: code calling task_setscheduler, task_setioprio, and
* task_setnice, assumes that
*   . if capable(cap_sys_nice), then those actions should be allowed
*   . if not capable(cap_sys_nice), but acting on your own processes,
*      then those actions should be allowed
* This is insufficient now since you can call code without suid, but
* yet with increased caps.
* So we check for increased caps on the target process.
*/
static int cap_safe_nice(struct task_struct *p)
{
       int is_subset;

       rcu_read_lock();
       is_subset = cap_issubset(__task_cred(p)->cap_permitted,
                                current_cred()->cap_permitted);
       rcu_read_unlock();

       if (!is_subset && !capable(CAP_SYS_NICE))
               return -EPERM;
       return 0;
}

一方のRHEL6では、一旦root権限を得るものの(この時はreniceできません)、その後setuid(), setgid()にて権限を元のユーザのものに戻すため、reniceをすることができます。

関連資料

ということでpingをreniceできないという謎の裏にはセキュリティを強化するための仕組みが隠れていました。あまり馴染みのない権限管理の仕組みがでてきたと思います。関連するドキュメントを記載しておきます。

man capabilities , (和訳)

man acl(和訳)

man chattr(和訳)