- sudoer文件的作用
建议的方法是在/etc/sudoers.d/目录下新建一个文件,例如:
[root@localhost sudoers.d]# pwd
/etc/sudoers.d
[root@localhost sudoers.d]# echo 'zhengtianbao ALL = (root) NOPASSWD: ALL' > stack_sh
[root@localhost sudoers.d]# cat stack_sh
zhengtianbao ALL = (root) NOPASSWD: ALL
[root@localhost sudoers.d]# su zhengtianbao
[zhengtianbao@localhost sudoers.d]$ ls
ls: cannot open directory .: Permission denied
[zhengtianbao@localhost sudoers.d]$ sudo ls
stack_sh
通用格式:
user host run_as command
user:一位或几位用户,在/etc/group中可以用一个%代替它,组对象的名称一定要用百分号%开头.
host:一个或几个主机名.
run_as:作为哪个用户运行,常见选项是root和ALL.
command:想让用户或组运行的一个或几个根级别命令.
例如:
hans ALL=(root) useradd,userdel
授权hans用户在所有计算机上以root身份运行useradd,userdel命令.
%smith ALL=(ALL) NOPASSWD:useradd,userdel
授权smith组全部成员在所有计算机上以所有用户的身份运行useradd,userdel命令;且运行时不必输入密码.
一点点疑问:能否控制命令的参数呢?接下来做个测试:
[root@localhost sudoers.d]# echo 'zhengtianbao ALL = (root) NOPASSWD: /bin/ls -l, /bin/ls -a' > stack_sh
[root@localhost sudoers.d]# su zhengtianbao
[zhengtianbao@localhost sudoers.d]$ ls -a
ls: cannot open directory .: Permission denied
[zhengtianbao@localhost sudoers.d]$ sudo ls -a
. .. stack_sh
[zhengtianbao@localhost sudoers.d]$ sudo ls -l
total 8
-rw-r--r-- 1 root root 59 Jan 26 14:43 stack_sh
[zhengtianbao@localhost sudoers.d]$ sudo ls -a -l
[sudo] password for zhengtianbao:
可见能够控制的命令参数还是很严格的.
放在openstack中这也是可行的,但是随着项目的增大,单纯的修改sudoers影响了openstack的可维护性,因此引入了root warpper来管理命令权限相关的内容.
- devstack使用普通用户安装openstack,无需密码执行所有root命令
如果是根据devstack来安装openstack的话,查看devstack中的stack.sh里面有关于root权限的内容:
# root Access
# -----------
# OpenStack is designed to be run as a non-root user; Horizon will fail to run
# as **root** since Apache will not serve content from **root** user).
# ``stack.sh`` must not be run as **root**. It aborts and suggests one course of
# action to create a suitable user account.
if [[ $EUID -eq 0 ]]; then
echo "You are running this script as root."
echo "Cut it out."
echo "Really."
echo "If you need an account to run DevStack, do this (as root, heh) to create $STACK_USER:"
echo "$TOP_DIR/tools/create-stack-user.sh"
exit 1
fi
# We're not **root**, make sure ``sudo`` is available
is_package_installed sudo || install_package sudo
好,它这里表示需要先通过脚本tools/create-stack-user.sh来创建一个用户,再通过那个用户来执行,看脚本内容:
# Needed to get ``ENABLED_SERVICES``
source $TOP_DIR/stackrc
# Give the non-root user the ability to run as **root** via ``sudo``
is_package_installedsudo || install_package sudo
if ! getent group $STACK_USER >/dev/null;then
echo "Creating a group called $STACK_USER"
groupadd $STACK_USER
fi
if ! getent passwd $STACK_USER >/dev/null;then
echo "Creating a user called $STACK_USER"
useradd -g $STACK_USER -s /bin/bash -d $DEST -m $STACK_USER
fi
echo "Giving stack user passwordless sudo privileges"
# UEC images ``/etc/sudoers`` does not have a ``#includedir``, add one
grep -q"^#includedir.*/etc/sudoers.d" /etc/sudoers ||
echo "#includedir /etc/sudoers.d" >>/etc/sudoers
(umask 226 && echo "$STACK_USER ALL=(ALL) NOPASSWD:ALL" \
>/etc/sudoers.d/50_stack_sh )
$STACK_USER的值在stackrc文件中定义,当前环境是root身份时则为’stack’:
# Determine stack user
if [[ $EUID -eq 0 ]]; then
STACK_USER=stack
else
STACK_USER=$(whoami)
fi
ok,这里发现它在/etc/sudoers.d/目录下生成了一个50_stack_sh的文件,里面的内容是:
stack ALL=(ALL) NOPASSWD:ALL
显然它创建的stack用户现在可以在使用`sudo`执行任何命令都能省略输入密码的过程了.
- devstack安装nova,配置以nova身份运行命令`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf * 不需要输入密码
接下来继续看nova的安装过程,在devstack/lib/目录下的nova脚本中有configure_nova()的方法,它会在stack.sh中被调用到,正如名字所示,它用来设置nova的config文件,创建一些数据等工作:
# configure_nova() - Set config files, create data dirs, etc
function configure_nova() {
# Put config files in ``/etc/nova`` for everyone to find
if [[ ! -d $NOVA_CONF_DIR ]]; then
sudo mkdir -p $NOVA_CONF_DIR
fi
sudo chown $STACK_USER $NOVA_CONF_DIR
cp -p $NOVA_DIR/etc/nova/policy.json $NOVA_CONF_DIR
configure_nova_rootwrap
...
注意里面的configure_nova_rootwrap,查看该方法:
# configure_nova_rootwrap() - configure Nova's rootwrap
function configure_nova_rootwrap() {
# Deploy new rootwrap filters files (owned by root).
# Wipe any existing rootwrap.d files first
if [[ -d $NOVA_CONF_DIR/rootwrap.d ]]; then
sudo rm -rf $NOVA_CONF_DIR/rootwrap.d
fi
# Deploy filters to /etc/nova/rootwrap.d
sudo mkdir -m 755 $NOVA_CONF_DIR/rootwrap.d
sudo cp $NOVA_DIR/etc/nova/rootwrap.d/*.filters $NOVA_CONF_DIR/rootwrap.d
sudo chown -R root:root $NOVA_CONF_DIR/rootwrap.d
sudo chmod 644 $NOVA_CONF_DIR/rootwrap.d/*
# Set up rootwrap.conf, pointing to /etc/nova/rootwrap.d
sudo cp $NOVA_DIR/etc/nova/rootwrap.conf $NOVA_CONF_DIR/
sudo sed -e"s:^filters_path=.*$:filters_path=$NOVA_CONF_DIR/rootwrap.d:" -i $NOVA_CONF_DIR/rootwrap.conf
sudo chown root:root $NOVA_CONF_DIR/rootwrap.conf
sudo chmod 0644 $NOVA_CONF_DIR/rootwrap.conf
# Specify rootwrap.conf as first parameter to nova-rootwrap
ROOTWRAP_SUDOER_CMD="$NOVA_ROOTWRAP $NOVA_CONF_DIR/rootwrap.conf *"
# Set up the rootwrap sudoers for nova
TEMPFILE=`mktemp`
echo "$STACK_USER ALL=(root) NOPASSWD: $ROOTWRAP_SUDOER_CMD" >$TEMPFILE
chmod 0440 $TEMPFILE
sudo chown root:root $TEMPFILE
sudo mv $TEMPFILE/etc/sudoers.d/nova-rootwrap
}
显然,它在/etc/sudoers.d/目录下创建了nova-rootwrap的文件,里面的内容可能是:
nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *
nova:指用户名.
ALL:指主机名.
root:指运行用户名.
NOPASSWD:指运行下面命令时不需要输入密码.
/usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *:指能够运行的命令.
上面的文件定义就是说:
以nova身份运行命令`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf * `
时是不需要输入密码的,其中的’*’指的是任意字符串,例如:`ip route show …`.
- nova中执行外部命令怎么调用nova-rootwarp
/usr/bin/nova-rootwrap是一个可执行的脚本文件,/etc/nova/rootwrap.conf则是rootwrap相关的配置,里面定义了filters-path所在路径,以及缺省的可执行命令所在路径,具体的过滤逻辑如下:
1)获取要执行的命令,如:ip
2)通过filters-path加载配置文件中定义的可以执行的命令列表
3)判断命令是否在可执行命令列表中
4)若在则通过python的subprocess模块执行Popen方法;不在则给出错误信息,退出.
这些都可以在nova的bin/nova-rootwrap文件中查看.
所有的nova代码在执行外部命令的时候都会用到execute函数,这个函数定义在nova顶层目录下的utils.py模块下.
例如:
from nova import utils
utils.execute(‘chmod’, ’777′, tmpdir, run_as_root=True)
execute函数首先根据run_as_root参数进行了一些处理,如下所示:
def _get_root_helper():
return 'sudo nova-rootwrap %s' % CONF.rootwrap_config
def execute(*cmd,**kwargs):
"""Convenience wrapper around oslo's execute() method."""
if 'run_as_root' in kwargsand not 'root_helper' in kwargs:
kwargs['root_helper']= _get_root_helper()
return processutils.execute(*cmd,**kwargs)
然后丢给processutils中的execute,查看代码:
def execute(*cmd,**kwargs):
...
if run_as_rootand hasattr(os,'geteuid')and os.geteuid() != 0:
if not root_helper:
raise NoRootWrapSpecified(
message=_('Command requested root, but did not '
'specify a root helper.'))
cmd= shlex.split(root_helper)+ list(cmd)
cmd= map(str, cmd)
while attempts > 0:
...
obj= subprocess.Popen(cmd,
stdin=_PIPE,
stdout=_PIPE,
stderr=_PIPE,
close_fds=close_fds,
preexec_fn=preexec_fn,
shell=shell)
...
最终执行的命令cmd就是`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf chmod 777 tmpdir`
这里`chmod`这个命令在rootwrap.d目录下的filters文件中可以找到对应的配置:
chmod: CommandFilter, chmod, root
CommandFilter的定义是在oslo的rootwrap/filters.py中,里面还定义了其他的filter(RegExp,Path,Kill,ReadFile,Ip,Env,Chaining,IpNetnsExec).
更加详细的内容请查看nova/rootwarp目录下的filters.py与wrapper.py
- rootwarp模块详析
rootwrap已经迁移到项目oslo中。
我们以rootwrap在nova中的应用为例,在文件setup.cfg中可以看到nova-rootwrap的entrance为nova-rootwrap = oslo.rootwrap.cmd:main;也就是oslo_rootwrap/cmp.py的main函数,下面仔细介绍这个main函数的流程:
1.命令行解析
"""
sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
sys.argv = [
'/usr/bin/nova-rootwrap',
'/etc/nova/rootwrap.conf',
'cat',
'/etc/iscsi/initiatorname.iscsi']
"""
execname = sys.argv.pop(0)
configfile = sys.argv.pop(0)
userargs = sys.argv[:]
"""
execname = /usr/bin/nova-rootwrap
configfile = /etc/nova/rootwrap.conf
userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
"""
可以看到execname = /usr/bin/nova-rootwrap指定了一个脚本,查看其内容后,发现其所实现的功能是退出当前所运行的main方法,从后面所应用execname的代码也可以看出,其都应用在程序异常退出的场景中。
/usr/bin/nova-rootwrap脚本内容:
#!/usr/bin/python
# PBR Generated from u'console_scripts'
import sys
from oslo.rootwrap.cmd import main
if __name__ == "__main__":
sys.exit(main())
再来看配置文件configfile = /etc/nova/rootwrap.conf,来看其内容:
# Configuration for nova-rootwrap
# This file should be owned by (and only-writeable by) the root user
[DEFAULT]
# List of directories to load filter definitions from (separated by ',').
# These directories MUST all be only writeable by root !
filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap
# List of directories to search executables in, in case filters do not
# explicitely specify a full path (separated by ',')
# If not specified, defaults to system PATH environment variable.
# These directories MUST all be only writeable by root !
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
# Enable logging to syslog
# Default value is False
use_syslog=False
# Which syslog facility to use.
# Valid values include auth, authpriv, syslog, user0, user1...
# Default value is 'syslog'
syslog_log_facility=syslog
# Which messages to log.
# INFO means log all usage
# ERROR means only log unsuccessful attempts
syslog_log_level=ERROR
在这个配置文件中,主要指定了两方面的内容:
A.filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap
指定了若干过滤器文件所在的目录,这些过滤器文件有:api-metadata.filters baremetal-compute-ipmi.filters baremetal-deploy-helper.filters compute.filters network.filters;以compute.filters为例来看看其内容:
# nova-rootwrap command filters for compute nodes
# This file should be owned by (and only-writeable by) the root user
[Filters]
# nova/virt/disk/mount/api.py: 'kpartx', '-a', device
# nova/virt/disk/mount/api.py: 'kpartx', '-d', device
kpartx: CommandFilter, kpartx, root
# nova/virt/xenapi/vm_utils.py: tune2fs, -O ^has_journal, part_path
# nova/virt/xenapi/vm_utils.py: tune2fs, -j, partition_path
tune2fs: CommandFilter, tune2fs, root
# nova/virt/disk/mount/api.py: 'mount', mapped_device
# nova/virt/disk/api.py: 'mount', '-o', 'bind', src, target
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
# nova/virt/configdrive.py: 'mount', device, mountdir
# nova/virt/libvirt/volume.py: 'mount', '-t', 'sofs' ...
mount: CommandFilter, mount, root
# nova/virt/disk/mount/api.py: 'umount', mapped_device
# nova/virt/disk/api.py: 'umount' target
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
# nova/virt/configdrive.py: 'umount', mountdir
umount: CommandFilter, umount, root
# nova/virt/libvirt/utils.py: 'blockdev', '--getsize64', path
# nova/virt/disk/mount/nbd.py: 'blockdev', '--flushbufs', device
blockdev: RegExpFilter, blockdev, root, blockdev, (--getsize64|--flushbufs), /dev/.*
# nova/virt/disk/vfs/localfs.py: 'tee', canonpath
tee: CommandFilter, tee, root
# nova/virt/xenapi/vm_utils.py: resize2fs, partition_path
# nova/virt/disk/api.py: resize2fs, image
resize2fs: CommandFilter, resize2fs, root
# nova/network/linux_net.py: 'kill', '-9', pid
# nova/network/linux_net.py: 'kill', '-HUP', pid
kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
# nova/network/linux_net.py: 'kill', pid
kill_radvd: KillFilter, root, /usr/sbin/radvd
# nova/network/linux_net.py: dnsmasq call
dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq
# nova/virt/libvirt/connection.py:
read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi
# nova/utils.py:read_file_as_root: 'cat', file_path
# (called from nova/virt/disk/vfs/localfs.py:VFSLocalFS.read_file)
read_passwd: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/passwd
read_shadow: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/shadow
......
可见在这些过滤器文件中,定义了很多具体命令的相关信息,如命令的应用场景,命令封装所调用的过滤器,命令的执行权限和命令的执行参数等等;我们初步就可以想到,这些过滤器文件中定义描述的所有命令,都是能够以非特权用户的身份在免输入密码的情况下执行的,我们用rootwrap模块来封装的命令行,首先应该到这些文件中查询是否有相匹配的命令,具体实现后面接着解析。
2.读取解析配置文件
try:
rawconfig = moves.configparser.RawConfigParser()
rawconfig.read(configfile)
config = wrapper.RootwrapConfig(rawconfig)
except ValueError as exc:
msg = "Incorrect value in %s: %s" % (configfile, exc.message)
_exit_error(execname, msg, RC_BADCONFIG, log=False)
except moves.configparser.Error:
_exit_error(execname, "Incorrect configuration file: %s" % configfile,
RC_BADCONFIG, log=False)
这段代码很明白,就是对配置文件configfile = /etc/nova/rootwrap.conf进行读取解析;
3.加载过滤器文件中定义的所有命令行过滤对象
filters = wrapper.load_filters(config.filters_path)
"""
config.filters_path = ['/etc/nova/rootwrap.d', '/usr/share/nova/rootwrap']
filters =
[
<oslo.rootwrap.filters.CommandFilter object at 0x7f10f266ecd0>,
......
<oslo.rootwrap.filters.KillFilter object at 0x7f10f266efd0>,
......
<oslo.rootwrap.filters.EnvFilter object at 0x1744210>,
......
<oslo.rootwrap.filters.ReadFileFilter object at 0x1744ed0>,
......
<oslo.rootwrap.filters.RegExpFilter object at 0x174a1d0>,
......
]
注:遍历并加载/usr/share/nova/rootwrap路径下所有过滤文件中的命令行过滤对象;
"""
这段代码的作用就是遍历并加载/usr/share/nova/rootwrap路径下所有过滤文件(api-metadata.filters baremetal-compute-ipmi.filters baremetal-deploy-helper.filters compute.filters network.filters;以compute.filters)中的命令行过滤对象(系统安装后/etc/nova/rootwrap.d是不存在的);
可以看到用于命令行封装过滤的过滤器有很多,实际上共有CommandFilter、RegExpFilter、PathFilter、KillFilter、ReadFileFilter、IpFilter、EnvFilter、ChainingFilter和IpNetnsExecFilter共9种;这些过滤器类都以CommandFilter为父类,它们的具体区别主要体现在其方法match上,所以这9种过滤器主要是根据不同命令应用不同的匹配方式而区分实现的。
4.匹配到具体的命令行过滤对象
filtermatch = wrapper.match_filter(filters, userargs, exec_dirs=config.exec_dirs)
"""
userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
注:exec_dirs为读取配置文件configfile = /etc/nova/rootwrap.conf获取;
filtermatch = <oslo.rootwrap.filters.ReadFileFilter object at 0x2195e90>
"""
这里的功能实现就是通过具体的命令行信息,调用不同的过滤器类方法,从定义描述的所有的命令行过滤对象中进行过滤匹配操作,看能否找到匹配的结果,如果找到匹配结果,就说明这个命令行是可以通过rootwrap模块封装来实现以非特权身份执行的;否则就说明openstack系统中指定此命令行只能以root身份执行。当然我们是可以通过进行功能扩展来实现我们的需求的。
5.通过rootwrap的封装获取具体的命令行实现
command = filtermatch.get_command(userargs, exec_dirs=config.exec_dirs)
"""
userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
command = ['sudo', '-u', 'shinian' ,'/bin/cat', '/etc/iscsi/initiatorname.iscsi']
"""
这段代码所实现的功能就是获取具体的命令行实现,实际上最后的命令行实现不过就是应用了我们所熟悉的sudo命令赋予普通用户的root权限,最后的命令行实际上就是:
sudo -u XXXX /bin/cat /etc/iscsi/initiatorname.iscsi
来看代码,这里调用了前面所述过滤器类中的get_command方法,有的过滤器类重写了get_command方法,有的过滤器类没有重写,直接调用父类CommandFilter中的get_command方法,但是区别并不大,不过就是通过对命令行参数的加加减减,整合成最后的命令行实现并返回;
这里调用的是过滤器ReadFileFilter,它直接调用了其父类CommandFilter的get_command方法,我们来简单看一下:
def get_command(self, userargs, exec_dirs=[]):
"""
Returns command to execute (with sudo -u if run_as != root).
"""
"""
userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
"""
to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
"""
to_exec = /bin/cat
"""
if (self.run_as != 'root'):
# Used to run commands at lesser privileges
return ['sudo', '-u', self.run_as, to_exec] + userargs[1:]
return [to_exec] + userargs[1:]
这个方法很好理解,这里不再赘述;
6.派生一个子进程来执行命令行
obj = subprocess.Popen(command,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr,
preexec_fn=_subprocess_setup,
env=filtermatch.get_environment(userargs))
obj.wait()
sys.exit(obj.returncode)
这段代码所实现的功能就是派生一个新的子进程来执行rootwrap封装后的命令行,并等待其运行结束,当获取到命令行执行的返回值后退出这个子进程。
所以rootwarp实现的具体步骤就是:
1 命令行解析
2 读取解析配置文件
3 加载过滤器文件中定义的所有命令行过滤对象
4 匹配到具体的命令行过滤对象
5 通过rootwrap的封装获取具体的命令行实现
6 派生一个子进程来执行命令行
实际上就是把命令行:sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
经过若干参数处理和匹配操作转化为:sudo -u XXXX /bin/cat /etc/iscsi/initiatorname.iscsi
之后,启动一个子进程来实现这个命令行的执行操作;
=======================如下为原创==========================
7.各种Filter详解
class CommandFilter(object):
"""Command filter only checking that the 1st argument matches exec_path."""
def __init__(self, exec_path, run_as, *args):
self.name = ''
self.exec_path = exec_path
self.run_as = run_as
self.args = args
self.real_exec = None
def get_exec(self, exec_dirs=None):
"""Returns existing executable, or empty string if none found."""
exec_dirs = exec_dirs or []
if self.real_exec is not None:
return self.real_exec
self.real_exec = ""
if os.path.isabs(self.exec_path):
if os.access(self.exec_path, os.X_OK):
self.real_exec = self.exec_path
else:
for binary_path in exec_dirs:
expanded_path = os.path.join(binary_path, self.exec_path)
if os.access(expanded_path, os.X_OK):
self.real_exec = expanded_path
break
return self.real_exec
def match(self, userargs):
"""Only check that the first argument (command) matches exec_path."""
return userargs and os.path.basename(self.exec_path) == userargs[0]
def preexec(self):
"""Setuid in subprocess right before command is invoked."""
if self.run_as != 'root':
os.setuid(_getuid(self.run_as))
def get_command(self, userargs, exec_dirs=None):
"""Returns command to execute."""
exec_dirs = exec_dirs or []
to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
return [to_exec] + userargs[1:]
def get_environment(self, userargs):
"""Returns specific environment to set, None if none."""
return None
qemu-nbd: CommandFilter, qemu-nbd, root
对应调用代码为:
nova/blob/master/nova/virt/disk/mount/nbd.py
_out, err = utils.trycmd('qemu-nbd', '-c', device,
self.image.path,
run_as_root=True)
class RegExpFilter(CommandFilter):
"""Command filter doing regexp matching for every argument."""
def match(self, userargs):
# Early skip if command or number of args don't match
if (not userargs or len(self.args) != len(userargs)):
# DENY: argument numbers don't match
return False
# Compare each arg (anchoring pattern explicitly at end of string)
for (pattern, arg) in zip(self.args, userargs):
try:
if not re.match(pattern + '$', arg):
# DENY: Some arguments did not match
return False
except re.error:
# DENY: Badly-formed filter
return False
# ALLOW: All arguments matched
return True
其中zip函数:a=(1,2),b=(3,4), zip(a,b)=[(1, 3), (2, 4)]
典型配置为:
blockdev: RegExpFilter, blockdev, root, blockdev, (--getsize64|--flushbufs), /dev/.*
对应调用代码为:
nova/blob/master/nova/virt/disk/mount/nbd.py
utils.execute('blockdev', '--flushbufs',
self.device, run_as_root=True)
3)PathFilter
class PathFilter(CommandFilter):
"""Command filter checking that path arguments are within given dirs
One can specify the following constraints for command arguments:
1) pass - pass an argument as is to the resulting command
2) some_str - check if an argument is equal to the given string
3) abs path - check if a path argument is within the given base dir
A typical rootwrapper filter entry looks like this:
# cmdname: filter name, raw command, user, arg_i_constraint [, ...]
chown: PathFilter, /bin/chown, root, nova, /var/lib/images
"""
def match(self, userargs):
if not userargs or len(userargs) < 2:
return False
arguments = userargs[1:]
equal_args_num = len(self.args) == len(arguments)
exec_is_valid = super(PathFilter, self).match(userargs)
args_equal_or_pass = all(
arg == 'pass' or arg == value
for arg, value in zip(self.args, arguments)
if not os.path.isabs(arg) # arguments not specifying abs paths
)
paths_are_within_base_dirs = all(
os.path.commonprefix([arg, os.path.realpath(value)]) == arg
for arg, value in zip(self.args, arguments)
if os.path.isabs(arg) # arguments specifying abs paths
)
return (equal_args_num and
exec_is_valid and
args_equal_or_pass and
paths_are_within_base_dirs)
def get_command(self, userargs, exec_dirs=None):
exec_dirs = exec_dirs or []
command, arguments = userargs[0], userargs[1:]
# convert path values to canonical ones; copy other args as is
args = [os.path.realpath(value) if os.path.isabs(arg) else value
for arg, value in zip(self.args, arguments)]
return super(PathFilter, self).get_command([command] + args,
exec_dirs)
典型配置为:
class KillFilter(CommandFilter):
"""Specific filter for the kill calls.
1st argument is the user to run /bin/kill under
2nd argument is the location of the affected executable
if the argument is not absolute, it is checked against $PATH
Subsequent arguments list the accepted signals (if any)
This filter relies on /proc to accurately determine affected
executable, so it will only work on procfs-capable systems (not OSX).
"""
def __init__(self, *args):
super(KillFilter, self).__init__("/bin/kill", *args)
@staticmethod
def _program_path(command):
"""Try to determine the full path for command.
Return command if the full path cannot be found.
"""
# shutil.which() was added to Python 3.3
if hasattr(shutil, 'which'):
return shutil.which(command)
if os.path.isabs(command):
return command
path = os.environ.get('PATH', os.defpath).split(os.pathsep)
for dir in path:
program = os.path.join(dir, command)
if os.path.isfile(program):
return program
return command
def _program(self, pid):
"""Determine the program associated with pid"""
try:
command = os.readlink("/proc/%d/exe" % int(pid))
except (ValueError, EnvironmentError):
# Incorrect PID
return None
# NOTE(yufang521247): /proc/PID/exe may have '\0' on the
# end (ex: if an executable is updated or deleted), because python
# doesn't stop at '\0' when read the target path.
command = command.partition('\0')[0]
# NOTE(dprince): /proc/PID/exe may have ' (deleted)' on
# the end if an executable is updated or deleted
if command.endswith(" (deleted)"):
command = command[:-len(" (deleted)")]
if os.path.isfile(command):
return command
# /proc/PID/exe may have been renamed with
# a ';......' or '.#prelink#......' suffix etc.
# So defer to /proc/PID/cmdline in that case.
try:
with open("/proc/%d/cmdline" % int(pid)) as pfile:
cmdline = pfile.read().partition('\0')[0]
cmdline = self._program_path(cmdline)
if os.path.isfile(cmdline):
command = cmdline
# Note we don't return None if cmdline doesn't exist
# as that will allow killing a process where the exe
# has been removed from the system rather than updated.
return command
except EnvironmentError:
return None
def match(self, userargs):
if not userargs or userargs[0] != "kill":
return False
args = list(userargs)
if len(args) == 3:
# A specific signal is requested
signal = args.pop(1)
if signal not in self.args[1:]:
# Requested signal not in accepted list
return False
else:
if len(args) != 2:
# Incorrect number of arguments
return False
if len(self.args) > 1:
# No signal requested, but filter requires specific signal
return False
command = self._program(args[1])
if not command:
return False
kill_command = self.args[0]
if os.path.isabs(kill_command):
return kill_command == command
return (os.path.isabs(command) and
kill_command == os.path.basename(command) and
os.path.dirname(command) in os.environ.get('PATH', ''
).split(':'))
典型配置为:
kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
_execute('kill', '-HUP', pid, run_as_root=True)
5)ReadFileFilter
class ReadFileFilter(CommandFilter):
"""Specific filter for the utils.read_file_as_root call."""
def __init__(self, file_path, *args):
self.file_path = file_path
super(ReadFileFilter, self).__init__("/bin/cat", "root", *args)
def match(self, userargs):
return (userargs == ['cat', self.file_path])
典型配置为:
read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi
class IpFilter(CommandFilter):
"""Specific filter for the ip utility to that does not match exec."""
def match(self, userargs):
if userargs[0] == 'ip':
# Avoid the 'netns exec' command here
for a, b in zip(userargs[1:], userargs[2:]):
if a == 'netns':
return (b != 'exec')
else:
return True
典型配置为:
7)
class EnvFilter(CommandFilter):
"""Specific filter for the env utility.
Behaves like CommandFilter, except that it handles
leading env A=B.. strings appropriately.
"""
def _extract_env(self, arglist):
"""Extract all leading NAME=VALUE arguments from arglist."""
envs = set()
for arg in arglist:
if '=' not in arg:
break
envs.add(arg.partition('=')[0])
return envs
def __init__(self, exec_path, run_as, *args):
super(EnvFilter, self).__init__(exec_path, run_as, *args)
env_list = self._extract_env(self.args)
# Set exec_path to X when args are in the form of
# env A=a B=b C=c X Y Z
if "env" in exec_path and len(env_list) < len(self.args):
self.exec_path = self.args[len(env_list)]
def match(self, userargs):
# ignore leading 'env'
if userargs[0] == 'env':
userargs.pop(0)
# require one additional argument after configured ones
if len(userargs) < len(self.args):
return False
# extract all env args
user_envs = self._extract_env(userargs)
filter_envs = self._extract_env(self.args)
user_command = userargs[len(user_envs):len(user_envs) + 1]
# match first non-env argument with CommandFilter
return (super(EnvFilter, self).match(user_command)
and len(filter_envs) and user_envs == filter_envs)
def exec_args(self, userargs):
args = userargs[:]
# ignore leading 'env'
if args[0] == 'env':
args.pop(0)
# Throw away leading NAME=VALUE arguments
while args and '=' in args[0]:
args.pop(0)
return args
def get_command(self, userargs, exec_dirs=[]):
to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
return [to_exec] + self.exec_args(userargs)[1:]
def get_environment(self, userargs):
env = os.environ.copy()
# ignore leading 'env'
if userargs[0] == 'env':
userargs.pop(0)
# Handle leading NAME=VALUE pairs
for a in userargs:
env_name, equals, env_value = a.partition('=')
if not equals:
break
if env_name and env_value:
env[env_name] = env_value
return env
典型配置为:
dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq
cmd = ['env',
'CONFIG_FILE=%s' % jsonutils.dumps(CONF.dhcpbridge_flagfile),
'NETWORK_ID=%s' % str(network_ref['id']),
'dnsmasq',
'--strict-order',
'--bind-interfaces',
'--conf-file=%s' % CONF.dnsmasq_config_file,
'--pid-file=%s' % _dhcp_file(dev, 'pid'),
'--dhcp-optsfile=%s' % _dhcp_file(dev, 'opts'),
'--listen-address=%s' % network_ref['dhcp_server'],
'--except-interface=lo',
'--dhcp-range=set:%s,%s,static,%s,%ss' %
(network_ref['label'],
network_ref['dhcp_start'],
network_ref['netmask'],
CONF.dhcp_lease_time),
'--dhcp-lease-max=%s' % len(netaddr.IPNetwork(network_ref['cidr'])),
'--dhcp-hostsfile=%s' % _dhcp_file(dev, 'conf'),
'--dhcp-script=%s' % CONF.dhcpbridge,
'--no-hosts',
'--leasefile-ro']
# dnsmasq currently gives an error for an empty domain,
# rather than ignoring. So only specify it if defined.
if CONF.dhcp_domain:
cmd.append('--domain=%s' % CONF.dhcp_domain)
dns_servers = CONF.dns_server
if CONF.use_network_dns_servers:
if network_ref.get('dns1'):
dns_servers.append(network_ref.get('dns1'))
if network_ref.get('dns2'):
dns_servers.append(network_ref.get('dns2'))
if network_ref['multi_host']:
cmd.append('--addn-hosts=%s' % _dhcp_file(dev, 'hosts'))
if dns_servers:
cmd.append('--no-resolv')
for dns_server in dns_servers:
cmd.append('--server=%s' % dns_server)
_execute(*cmd, run_as_root=True)
8)IpNetnsExecFilter
class IpNetnsExecFilter(ChainingFilter):
"""Specific filter for the ip utility to that does match exec."""
def match(self, userargs):
# Network namespaces currently require root
# require <ns> argument
if self.run_as != "root" or len(userargs) < 4:
return False
return (userargs[:3] == ['ip', 'netns', 'exec'])
def exec_args(self, userargs):
args = userargs[4:]
if args:
args[0] = os.path.basename(args[0])
return args
典型配置为:
9)Chain
class ChainingFilter(CommandFilter):
def exec_args(self, userargs):
return []
class ChainingRegExpFilter(ChainingFilter):
"""Command filter doing regexp matching for prefix commands.
Remaining arguments are filtered again. This means that the command
specified as the arguments must be also allowed to execute directly.
"""
def match(self, userargs):
# Early skip if number of args is smaller than the filter
if (not userargs or len(self.args) > len(userargs)):
return False
# Compare each arg (anchoring pattern explicitly at end of string)
for (pattern, arg) in zip(self.args, userargs):
try:
if not re.match(pattern + '$', arg):
# DENY: Some arguments did not match
return False
except re.error:
# DENY: Badly-formed filter
return False
# ALLOW: All arguments matched
return True
def exec_args(self, userargs):
args = userargs[len(self.args):]
if args:
args[0] = os.path.basename(args[0])
return args