ntpdからみるcapability
CentOSではNTPデーモンがntpユーザ権限で動作している。root権限で動いていないということで『一応安全を意識してるんだな』ぐらいにしか思っていなかった。
しかし、システム時刻を変更というrootしかできない行為をntpユーザが行っていることの不思議に(今更)気がついたので調べてみた。
- これはCapability(ケーパビリティ)と呼ばれる機能を使用して実現されている。
- Capability は『強大すぎるroot権限』を30-40個程度のおおまかな小さな権限(それぞれをCAP0, CAP1, ..., CAPnとしておく)に分割し、プロセスごとに 『CAPx と CAPy のみ許可』 のような設定をできるようにする仕組みである。
- 26種類目のCAP (CAP25) が『CAP_SYS_TIME』(include/linux/capability.h で定義) であり、『システム時刻を変更する権限』である。
- NTPデーモンには『root権限』のうち 『CAP_SYS_TIME』のみがあればよいので、他のCAPは起動時に外してしまっている(外したCAPを元に戻す事はできない)。
Capability操作の前提知識
- 各Capability (CAP) に対するプロセスの振る舞いは下記3種類の設定で行われる。
- Permitted: ONであれば当該のCAPを有効化できる(EffectiveでONできる。あえてOFFにしてもよい)
- Effective: ONであれば当該のCAPを使った操作ができる
- Inheritable: 当該のプロセスが exec() により他のバイナリをロードした場合、Permitted に引き継げる CAP
- プロセスごとのCapabilityの状態は /proc/PID/status の CapInh, CapPrm, CapEff 行で確認できる(各CAPは1ビットのON/OFFで表現されるが、statusの表示は16進)
- Linuxケーパビリティ(2/3) | OSDN Magazine が詳しい。
- PermittedをOFFにしたら、自プロセスではPermittedを二度とONにできない
- なのでとりあえずNTPデーモンはrootで起動し、必要の無いCAPを削ぎ落とす流れにしかなり得ない
- NTPデーモンを乗っ取ってもCAPを追加することはできない
- 全プロセスについて共通で禁止(permittedを強制OFF)するCAPをカーネルパラメータ kernel.cap-bound で設定できる
- CentOSでは(たぶん他のディストリビューションでも)他のプロセスのCAPを操作する CAP_SETPCAP が指定されており、root でも他のプロセスの CAP をいじる事がでいない
- APIを呼び出して自プロセスのCAPを操作することはできる
確認
- CentOS5.6のSRPMから取得した『ntp-4.2.2p1』で確認。
- 肝心な部分は ntp-4.2.2p1/ntpd/ntpd.c の下記の部分(特に HAVE_LINUX_CAPABILITIES フラグを使っている範囲)
#ifdef HAVE_DROPROOT if( droproot ) { /* Drop super-user privileges and chroot now if the OS supports this */ #ifdef HAVE_LINUX_CAPABILITIES /* set flag: keep privileges accross setuid() call (we only really need cap_sys_time): */ if( prctl( PR_SET_KEEPCAPS, 1L, 0L, 0L, 0L ) == -1 ) { msyslog( LOG_ERR, "prctl( PR_SET_KEEPCAPS, 1L ) failed: %m" ); exit(-1); } #else /* we need a user to switch to */ if( user == NULL ) { msyslog(LOG_ERR, "Need user name to drop root privileges (see -u flag!)" ); exit(-1); } #endif /* HAVE_LINUX_CAPABILITIES */ if (user != NULL) { if (isdigit((unsigned char)*user)) { sw_uid = (uid_t)strtoul(user, &endp, 0); if (*endp != '\0') goto getuser; } else { getuser: if ((pw = getpwnam(user)) != NULL) { sw_uid = pw->pw_uid; } else { errno = 0; msyslog(LOG_ERR, "Cannot find user `%s'", user); exit (-1); } } } if (group != NULL) { if (isdigit((unsigned char)*group)) { sw_gid = (gid_t)strtoul(group, &endp, 0); if (*endp != '\0') goto getgroup; } else { getgroup: if ((gr = getgrnam(group)) != NULL) { sw_gid = gr->gr_gid; } else { errno = 0; msyslog(LOG_ERR, "Cannot find group `%s'", group); exit (-1); } } } if( chrootdir ) { /* make sure cwd is inside the jail: */ if( chdir(chrootdir) ) { msyslog(LOG_ERR, "Cannot chdir() to `%s': %m", chrootdir); exit (-1); } if( chroot(chrootdir) ) { msyslog(LOG_ERR, "Cannot chroot() to `%s': %m", chrootdir); exit (-1); } } if (group && setgid(sw_gid)) { msyslog(LOG_ERR, "Cannot setgid() to group `%s': %m", group); exit (-1); } if (group && setegid(sw_gid)) { msyslog(LOG_ERR, "Cannot setegid() to group `%s': %m", group); exit (-1); } if (user && setuid(sw_uid)) { msyslog(LOG_ERR, "Cannot setuid() to user `%s': %m", user); exit (-1); } if (user && seteuid(sw_uid)) { msyslog(LOG_ERR, "Cannot seteuid() to user `%s': %m", user); exit (-1); } #ifdef HAVE_LINUX_CAPABILITIES do { /* We may be running under non-root uid now, but we still hold full root privileges! * We drop all of them, except for the crucial one: cap_sys_time: */ cap_t caps; if( ! ( caps = cap_from_text( "cap_sys_time=ipe" ) ) ) { msyslog( LOG_ERR, "cap_from_text() failed: %m" ); exit(-1); } if( cap_set_proc( caps ) == -1 ) { msyslog( LOG_ERR, "cap_set_proc() failed to drop root privileges: %m" ); exit(-1); } cap_free( caps ); } while(0); #endif /* HAVE_LINUX_CAPABILITIES */ } /* if( droproot ) */ #endif /* HAVE_DROPROOT */
- root ユーザから ntp ユーザに切り替わるまでは下記の流れになっている。
- setuid,seteuid (setgid,setegidはよい?) を使用して root から非 root ユーザに切り替えた際に全ての CAP がクリアされるのを抑止 ( prctl( PR_SET_KEEPCAPS, 1L, 0L, 0L, 0L ) )
- chroot環境指定の場合は chroot 実行
- グループを ntp に切り替え (setgid, setegid)
- ユーザを ntp に切り替え (setuid, seteuid)
- 『CAP_SYS_TIME』のみを設定 (caps = cap_from_text( "cap_sys_time=ipe" ) と cap_set_proc( caps ) )
- NTPが呼び出す時間調整用の adjtimex (kernel/time.c) から呼び出される do_adjtimex (kernel/time/ntp.c) の始めの部分で 『CAP_SYS_TIME』の有無をチェックしており、同CAPを有していないプロセスからの呼び出しについては EPERM が返ることが確認できた。
int do_adjtimex(struct timex *txc) { struct timespec ts; int result; /* Validate the data before disabling interrupts */ if (txc->modes & ADJ_ADJTIME) { /* singleshot must not be used with any other mode bits */ if (!(txc->modes & ADJ_OFFSET_SINGLESHOT)) return -EINVAL; if (!(txc->modes & ADJ_OFFSET_READONLY) && !capable(CAP_SYS_TIME)) return -EPERM; } else { /* In order to modify anything, you gotta be super-user! */ if (txc->modes && !capable(CAP_SYS_TIME)) return -EPERM;
検証
- 下記をrootユーザで実行してみた。
#include <sys/prctl.h> #include <sys/capability.h> #include <sys/types.h> #include <stdio.h> void printmycaps() { cap_t cap = cap_get_proc(); printf("Running with uid %d\n", getuid()); printf("Running with capabilities: %s\n", cap_to_text(cap, NULL)); cap_free(cap); } int main(int argc, char *argv[]) { int newuid = 1000; char *capstr = "cap_sys_time=ipe"; cap_t newcaps = cap_from_text(capstr); printmycaps(); sleep(10); /* A */ prctl(PR_SET_KEEPCAPS, 1); setresuid(newuid, newuid, newuid); printmycaps(); sleep(10); /* B */ cap_set_proc(newcaps); cap_free(newcaps); printmycaps(); sleep(20); /* C */ return 0; }
- ソース上の A 地点での /proc/xxxx/status は以下の通り。とりあえず cap-bound で制限しているビット (e になってしまってるところ) 以外は ON である。
CapInh: 0000000000000000 CapPrm: 00000000fffffeff CapEff: 00000000fffffeff
- ソース上の B 地点での /proc/xxxx/status は以下の通り。ユーザIDを変更したことにより Effective のビットがすべてクリアされている。しかし、Permitted は残っているので ON にすることができる。
CapInh: 0000000000000000 CapPrm: 00000000fffffeff CapEff: 0000000000000000
- ソース上の C 地点での /proc/xxxx/status は以下の通り。"cap_sys_time=ipe" としか指定していないため、他のビットはすべてクリアされ、CAP_SYS_TIMEのi(inheritable)p(permitted)e(effective) のみが ON になった。※25ビット(0ビットスタート)目がONなので2表示。
CapInh: 0000000002000000 CapPrm: 0000000002000000 CapEff: 0000000002000000