openstack rootwrap详解

1.前言

网上关于openstack rootwrap的讲解还是有一些的,只知道是一个控制nova,neutron等普通用户权限的工具。但是这些代码是怎么实现以nova,neutron用户启动的呢?

这个问题我研究了一下,还是要从openstack每一项服务的开机启动脚本看起。先总结一下,openstack 实现nova,neutron普通用户的权限控制的基本方法可以概括为“以普通用户运行,只对特定命令提供免密执行权限”

2.当我们在shell输入service neutron-server start后发生了什么?

Queens版本为例,openstack相关服务启动的代码,都在我们熟悉的/etc/init.d以及 /lib/systemd/system目录下,先看看/lib/systemd/system/neutron-server.service服务是怎么写的

[Unit]
Description=OpenStack Neutron Server
After=mysql.service postgresql.service rabbitmq-server.service keystone.service



[Service]
"""启动时使用的用户"""
User=neutron
"""启动时使用的用户组"""
Group=neutron
"""启动的进程为主进程"""
Type=simple
WorkingDirectory=~
RuntimeDirectory=neutron lock/neutron
CacheDirectory=neutron
"""启动入口在/etc/init.d/neutron-server"""
ExecStart=/etc/init.d/neutron-server systemd-start
Restart=on-failure
LimitNOFILE=65535
TimeoutStopSec=15

[Install]
WantedBy=multi-user.target

这是一个基本的sytemd服务的模板,绿色注释的几行行是我关心的。这里是以neutron用户与用户组启动的

在/etc/init.d/neutron-server中,上面的/etc/init.d/neutron-server systemd-start对应着

#! /bin/sh

DESC="OpenStack Neutron Server"
PROJECT_NAME=neutron
NAME=${PROJECT_NAME}-server
[ -r /etc/default/neutron-server ] && . /etc/default/neutron-server


"""DAEMON_ARGS"""
[ -n "$NEUTRON_PLUGIN_CONFIG" ] && DAEMON_ARGS="--config-file=$NEUTRON_PLUGIN_CONFIG"
    
    ......

"""DAEMON"""
if [ -z "${DAEMON}" ] ; then
        DAEMON=/usr/bin/${NAME}
fi

    ......
"""DAEMON_ARGS"""
LOGFILE=/var/log/${PROJECT_NAME}/${NAME}.log
if [ -z "${NO_OPENSTACK_CONFIG_FILE_DAEMON_ARG}" ] ; then
        DAEMON_ARGS="--config-file=${CONFIG_FILE} ${DAEMON_ARGS}"
fi

    ......

"""DAEMON_ARGS"""
if [ -z "${NO_OPENSTACK_LOGFILE_DAEMON_ARG}" ] ; then
        [ "x$USE_LOGFILE" != "xno" ] && DAEMON_ARGS="$DAEMON_ARGS --log-file=$LOGFILE"

    ......

"""do_systemd_start"""
do_systemd_start() {
        exec $DAEMON $DAEMON_ARGS
}

    ......


case "$1" in 
    """省略其他的case分支"""
    ......

systemd-start)
    do_systemd_start
;;


相当于直接执行

sudo  exec start-stop-daemon --start --chuid neutron --exec /usr/bin/neutron-server -- \
      --config-file /etc/neutron/neutron.conf \
      --config-file /etc/neutron/plugins/ml2/ml2_conf.ini
      --log-file /var/log/neutron/server.log 

这里,显然是以neutron用户的身份执行的neutron-server脚本

3.普通用户neutron是如何执行一些需要root权限的linux命令的?

似乎到这里还没有看到root-wrap的作用,别着急,在neutron-openvsitch-agent中,代码有时候会直接调用命令行,如

    def add_bridge(self, bridge_name):
        self.run_vsctl(["--", "--may-exist", "add-br", bridge_name])

代码在执行这个函数的时候,是直接通过Popen执行下面的命令行

ovs-vsctl add-br bridge_name

如果是在普通用户下运行的python代码,在执行这段代码后会出错(Permission denied),一种解决思路是这样的:

1.在执行这段命令之前,加上"sudo"

2.对neutron账户设置免密码登录,这样加上sudo执行一些系统命令时就不用输入密码了。

在/etc/sudoers.d里面确实出现了一些免密配置的文件

root@controller:/etc/sudoers.d# ls
glance_sudoers  neutron_sudoers  nova_sudoers  README  stack

不过为了进一步限制neutron可以账户免密码执行的命令个数与种类,openstack还加上了第三步

3.在sudo 与实际要执行的受限制的系统命令之间加一层过滤,从而仅仅允许neutron用户执行过滤脚本中规定的一些命令

这个就体现在上面的glance_sudoers等文件中。neutron_sudoers实际上

eg,在执行上面的add_bridge的过程中,实际上是执行了

sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf ovs-vsctl add-br bridge_name

4.openstack root-wrap的详细使用方法

1.neutron-rootwrap

#! /usr/bin/python2

import sys
from oslo_rootwrap.cmd import main

if __name__ == "__main__":
    sys.exit(main()

 

2.oslo_rootwrap.cmd.main:

def main(run_daemon=False):
    # Split arguments, require at least a command
    execname = sys.argv.pop(0)
    
        """......此处代码已省略......"""

    configfile = sys.argv.pop(0)

    """加载紧随其后的配置文件"""
    try:
        rawconfig = moves.configparser.RawConfigParser()
        rawconfig.read(configfile)
        config = wrapper.RootwrapConfig(rawconfig)
    except ValueError as exc:
        msg = "Incorrect value in %s: %s" % (configfile, exc.args[0])
        _exit_error(execname, msg, RC_BADCONFIG, log=False)
    except moves.configparser.Error:
        _exit_error(execname, "Incorrect configuration file: %s" % configfile,
                    RC_BADCONFIG, log=False)

            """......此处代码已省略......"""
    
    """这里,config.filters_path为"""
    """filters_path=/etc/neutron/rootwrap.d,/usr/share/neutron/rootwrap"""
    """重点就是加载filters的过程"""
    
    filters = wrapper.load_filters(config.filters_path)

    if run_daemon:
        from oslo_rootwrap import daemon as daemon_mod
        daemon_mod.daemon_start(config, filters)
    else:
        """默认的run_daemon为False,会执行下面这部分代码"""
        run_one_command(execname, config, filters, sys.argv)

重点看看加载filters的过程

def load_filters(filters_path):
    """Load filters from a list of directories."""
    filterlist = []
    for filterdir in filters_path:
        if not os.path.isdir(filterdir):
            continue
        for filterfile in filter(lambda f: not f.startswith('.'),
                                 os.listdir(filterdir)):
            filterfilepath = os.path.join(filterdir, filterfile)
            if not os.path.isfile(filterfilepath):
                continue
            kwargs = {"strict": False} if six.PY3 else {}
            filterconfig = moves.configparser.RawConfigParser(**kwargs)
            filterconfig.read(filterfilepath)
            for (name, value) in filterconfig.items("Filters"):

                """position 1"""

                filterdefinition = [s.strip() for s in value.split(',')]
                
                """position 2"""
                
                newfilter = build_filter(*filterdefinition)
                if newfilter is None:
                    continue
                newfilter.name = name
                filterlist.append(newfilter)
    # And always include privsep-helper
    privsep = build_filter("CommandFilter", "privsep-helper", "root")
    privsep.name = "privsep-helper"
    filterlist.append(privsep)
    return filterlist

关键点有两个:

position1:将配置文件中Filters部分的每一行以逗号分隔开,并且将分隔开的每一个字段加入一个列表

例如,filters长这样

[Filters]

# neutron/agent/linux/iptables_firewall.py
#   "iptables-save", ...
iptables-save: CommandFilter, iptables-save, root
iptables-restore: CommandFilter, iptables-restore, root
ip6tables-save: CommandFilter, ip6tables-save, root
ip6tables-restore: CommandFilter, ip6tables-restore, root

当读取第一行的时候,filterdefinition为

filterdefinition = ["CommandFilter", "iptables-save", "root"]

position2:构建一个filter对象

def build_filter(class_name, *args):
    """Returns a filter object of class class_name."""
    """先从filters模块(oslo_rootwrap.filters)检查有没有第一个参数class_name"""
    if not hasattr(filters, class_name):
        logging.warning("Skipping unknown filter class (%s) specified "
                        "in filter definitions" % class_name)
        return None
    filterclass = getattr(filters, class_name)
    """以剩下的其他参数初始化filterclass"""
    return filterclass(*args)

由上面的

iptables-save: CommandFilter, iptables-save, root

生成了

new_class_filter = CommandFilter(name="iptables-save",exec_path=iptables-save, run_as=root)

最后,这个new_class_filter被添加到filterlist中

 

3.filters 的选择以及命令的执行

3.1把要执行的命令交给filters处理

oslo_rootwrap.cmd.main中,代码最终是在run_one_command里面执行了

def run_one_command(execname, config, filters, userargs):
    # Execute command if it matches any of the loaded filters
    try:
        obj = wrapper.start_subprocess(
            filters, userargs,
            exec_dirs=config.exec_dirs,
            log=config.use_syslog,
            stdin=sys.stdin,
            stdout=sys.stdout,
            stderr=sys.stderr)
        returncode = obj.wait()

start_subporocess:

def start_subprocess(filter_list, userargs, exec_dirs=[], log=False, **kwargs):

    """根据实际要执行的命令行选择对应的filter"""
    """如果命令行是ovs-vsctl add-br bridge_name, 就是对ovs-vsctl命令进行筛选"""
    
    filtermatch = match_filter(filter_list, userargs, exec_dirs)

    command = filtermatch.get_command(userargs, exec_dirs)

    if log:
        logging.info("(%s > %s) Executing %s (filter match = %s)" % (
            _getlogin(), pwd.getpwuid(os.getuid())[0],
            command, filtermatch.name))

    def preexec():
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
        filtermatch.preexec()

    obj = subprocess.Popen(command,
                           preexec_fn=preexec,
                           env=filtermatch.get_environment(userargs),
                           **kwargs)
    return obj

3.2filters处理的具体过程

match_filter,很简单,直接比较需要执行的linux命令行的第一个参数与filter自己的self.exec_path参数。如果一个都没有匹配到,那么返回值为空值,不会执行Linux 命令。

def match_filter(filter_list, userargs, exec_dirs=None):

    first_not_executable_filter = None
    exec_dirs = exec_dirs or []

    for f in filter_list:

        """f.exec_pth与userargs的第一个字符串相同"""
        if f.match(userargs):

            """一些命令可能受到多重限制,
             比如允许ovs-vsctl add-br但不允许ovs-vsctl del-br"""
            if isinstance(f, filters.ChainingFilter):
                # This command calls exec verify that remaining args
                # matches another filter.
                def non_chain_filter(fltr):
                    return (fltr.run_as == f.run_as
                            and not isinstance(fltr, filters.ChainingFilter))

                leaf_filters = [fltr for fltr in filter_list
                                if non_chain_filter(fltr)]
                args = f.exec_args(userargs)
                if not args:
                    continue
                try:
                    match_filter(leaf_filters, args, exec_dirs=exec_dirs)
                except (NoFilterMatched, FilterMatchNotExecutable):
                    continue

            if not f.get_exec(exec_dirs=exec_dirs):
                if not first_not_executable_filter:
                    first_not_executable_filter = f
                continue
            return f

    if first_not_executable_filter:

利用CommandFilter.get_command得到最终的参数

    def get_exec(self, exec_dirs=None):
        """找出self.exec_path的绝对路径,如找出/usr/bin/ovs-vsctl"""
        exec_dirs = exec_dirs or []
        if self.real_exec is not None:
            return 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 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:]

最后得到

command = ["/usr/bin/ovs-vsctl", "add-br", "bridge_name"]

用下面这幅图总结一下

5.利用oslo_rootwrap来给自己的代码添加执行权限控制

下面实现一个以普通用户user1执行代码读取iptables规则,并利用oslo_rootwrap进行控制的过程

为了防止sudoers.d文件夹下的文件修改出错,我们新建一个用户user1

0.新建一个用户user1

$ useradd user1

$ usermod user1 -s /bin/bash 

$ passwd user1

1.安装oslo_rootwrap

sudo apt-get install python-oslo-rootwrap
sudo pip install oslo_rootwrap

2.编写/usr/bin/user1-rootwrap

sudo vi /usr/bin/user1-rootwrap

user1-rootwrap:

#! /usr/bin/python2
import sys

from oslo_rootwrap.cmd import main

if __name__ == "__main__":
    sys.exit(main())

改变/usr/bin/user1-rootwrap的权限为755

sudo chmod 755 /usr/bin/user1-rootwrap 

3.创建 /etc/user1/rootwrap.conf

sudo mkdir /etc/user1
sudo vi /etc/user1/rootwrap.conf

rootwrap.conf

[DEFAULT]

filters_path=/etc/user1/rootwrap.d

exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin,/usr/local/sbin

use_syslog=False

syslog_log_facility=syslog

syslog_log_level=ERROR

4.创建/etc/user1/rootwrap.d文件夹以及iptables.filters

sudo mkdir /etc/user1/rootwrap.d
sudo vi /etc/user1/rootwrap.d/iptables.filters

iptables.filters:

[Filters]
iptables: CommandFilter, iptables, root

5.添加user1对sudo /usr/bin/user-root的免密执行

为了避免应修改/etc/sudoers.d失误,造成sudo不可用的问题(解决/etc/sudoers权限出错导致sudo不可用),采用visudo

visudo

然后在最后一行添加

user1 ALL=(root) NOPASSWD: /usr/bin/user1-rootwrap /etc/user1/rootwrap.conf *

6.测试

root@host:/$ su user1
密码: 
user@host:/$sudo /usr/bin/user1-rootwrap /etc/user1/rootwrap.conf iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination  

user@host:/$sudo /usr/bin/user1-rootwrap /etc/user1/rootwrap.conf ip a
/usr/bin/user1-rootwrap: Unauthorized command: ip a (no filter matched)

成功

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值