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 ユーザに切り替わるまでは下記の流れになっている。
    1. setuid,seteuid (setgid,setegidはよい?) を使用して root から非 root ユーザに切り替えた際に全ての CAP がクリアされるのを抑止 ( prctl( PR_SET_KEEPCAPS, 1L, 0L, 0L, 0L ) )
    2. chroot環境指定の場合は chroot 実行
    3. グループを ntp に切り替え (setgid, setegid)
    4. ユーザを ntp に切り替え (setuid, seteuid)
    5. 『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