Browse Source
When running commands that require root privileges, the linuxbridge, openvswitch, and ryu agent now prepend the commands with the value of the root_helper config variable. This is set to "sudo" in the plugins' .ini files, allowing the agent to run as a non-root user with appropriate sudo privilidges. If root_helper is changed to "sudo quantum-rootwrap", then the command being run will be filtered against lists of each agent's valid commands in quantum/rootwrap. See http://wiki.openstack.org/Packager/Rootwrap for details. Fixes bug 948467. Change-Id: I549515068a4ce8ae480905ec5eaab6257445d0c3 Signed-off-by: Bob Kukura <rkukura@redhat.com>changes/93/5293/2
16 changed files with 508 additions and 38 deletions
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python |
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
"""Root wrapper for Quantum |
||||
|
||||
Uses modules in quantum.rootwrap containing filters for commands |
||||
that quantum agents are allowed to run as another user. |
||||
|
||||
To switch to using this, you should: |
||||
* Set "--root_helper=sudo quantum-rootwrap" in the agents config file. |
||||
* Allow quantum to run quantum-rootwrap as root in quantum_sudoers: |
||||
quantum ALL = (root) NOPASSWD: /usr/bin/quantum-rootwrap |
||||
(all other commands can be removed from this file) |
||||
|
||||
To make allowed commands node-specific, your packaging should only |
||||
install quantum/rootwrap/quantum-*-agent.py on compute nodes where |
||||
agents that need root privileges are run. |
||||
""" |
||||
|
||||
import os |
||||
import subprocess |
||||
import sys |
||||
|
||||
|
||||
RC_UNAUTHORIZED = 99 |
||||
RC_NOCOMMAND = 98 |
||||
|
||||
if __name__ == '__main__': |
||||
# Split arguments, require at least a command |
||||
execname = sys.argv.pop(0) |
||||
if len(sys.argv) == 0: |
||||
print "%s: %s" % (execname, "No command specified") |
||||
sys.exit(RC_NOCOMMAND) |
||||
|
||||
userargs = sys.argv[:] |
||||
|
||||
# Add ../ to sys.path to allow running from branch |
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(execname), |
||||
os.pardir, os.pardir)) |
||||
if os.path.exists(os.path.join(possible_topdir, "quantum", "__init__.py")): |
||||
sys.path.insert(0, possible_topdir) |
||||
|
||||
from quantum.rootwrap import wrapper |
||||
|
||||
# Execute command if it matches any of the loaded filters |
||||
filters = wrapper.load_filters() |
||||
filtermatch = wrapper.match_filter(filters, userargs) |
||||
if filtermatch: |
||||
obj = subprocess.Popen(filtermatch.get_command(userargs), |
||||
stdin=sys.stdin, |
||||
stdout=sys.stdout, |
||||
stderr=sys.stderr, |
||||
env=filtermatch.get_environment(userargs)) |
||||
obj.wait() |
||||
sys.exit(obj.returncode) |
||||
|
||||
print "Unauthorized command: %s" % ' '.join(userargs) |
||||
sys.exit(RC_UNAUTHORIZED) |
@ -0,0 +1,16 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
@ -0,0 +1,143 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
|
||||
import os |
||||
import re |
||||
|
||||
|
||||
class CommandFilter(object): |
||||
"""Command filter only checking that the 1st argument matches exec_path""" |
||||
|
||||
def __init__(self, exec_path, run_as, *args): |
||||
self.exec_path = exec_path |
||||
self.run_as = run_as |
||||
self.args = args |
||||
|
||||
def match(self, userargs): |
||||
"""Only check that the first argument (command) matches exec_path""" |
||||
if (os.path.basename(self.exec_path) == userargs[0]): |
||||
return True |
||||
return False |
||||
|
||||
def get_command(self, userargs): |
||||
"""Returns command to execute (with sudo -u if run_as != root).""" |
||||
if (self.run_as != 'root'): |
||||
# Used to run commands at lesser privileges |
||||
return ['sudo', '-u', self.run_as, self.exec_path] + userargs[1:] |
||||
return [self.exec_path] + userargs[1:] |
||||
|
||||
def get_environment(self, userargs): |
||||
"""Returns specific environment to set, None if none""" |
||||
return None |
||||
|
||||
|
||||
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 (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): |
||||
break |
||||
except re.error: |
||||
# DENY: Badly-formed filter |
||||
return False |
||||
else: |
||||
# ALLOW: All arguments matched |
||||
return True |
||||
|
||||
# DENY: Some arguments did not match |
||||
return False |
||||
|
||||
|
||||
class DnsmasqFilter(CommandFilter): |
||||
"""Specific filter for the dnsmasq call (which includes env)""" |
||||
|
||||
def match(self, userargs): |
||||
if (userargs[0].startswith("FLAGFILE=") and |
||||
userargs[1].startswith("NETWORK_ID=") and |
||||
userargs[2] == "dnsmasq"): |
||||
return True |
||||
return False |
||||
|
||||
def get_command(self, userargs): |
||||
return [self.exec_path] + userargs[3:] |
||||
|
||||
def get_environment(self, userargs): |
||||
env = os.environ.copy() |
||||
env['FLAGFILE'] = userargs[0].split('=')[-1] |
||||
env['NETWORK_ID'] = userargs[1].split('=')[-1] |
||||
return env |
||||
|
||||
|
||||
class KillFilter(CommandFilter): |
||||
"""Specific filter for the kill calls. |
||||
1st argument is a list of accepted signals (emptystring means no signal) |
||||
2nd argument is a list of accepted affected executables. |
||||
|
||||
This filter relies on /proc to accurately determine affected |
||||
executable, so it will only work on procfs-capable systems (not OSX). |
||||
""" |
||||
|
||||
def match(self, userargs): |
||||
if userargs[0] != "kill": |
||||
return False |
||||
args = list(userargs) |
||||
if len(args) == 3: |
||||
signal = args.pop(1) |
||||
if signal not in self.args[0]: |
||||
# Requested signal not in accepted list |
||||
return False |
||||
else: |
||||
if len(args) != 2: |
||||
# Incorrect number of arguments |
||||
return False |
||||
if '' not in self.args[0]: |
||||
# No signal, but list doesn't include empty string |
||||
return False |
||||
try: |
||||
command = os.readlink("/proc/%d/exe" % int(args[1])) |
||||
if command not in self.args[1]: |
||||
# Affected executable not in accepted list |
||||
return False |
||||
except (ValueError, OSError): |
||||
# Incorrect PID |
||||
return False |
||||
return True |
||||
|
||||
|
||||
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): |
||||
if userargs[0] != 'cat': |
||||
return False |
||||
if userargs[1] != self.file_path: |
||||
return False |
||||
if len(userargs) != 2: |
||||
return False |
||||
return True |
@ -0,0 +1,46 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
|
||||
from quantum.rootwrap import filters |
||||
|
||||
filterlist = [ |
||||
# quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py: |
||||
# 'brctl', 'addbr', bridge_name |
||||
# 'brctl', 'addif', bridge_name, interface |
||||
# 'brctl', 'addif', bridge_name, tap_device_name |
||||
# 'brctl', 'delbr', bridge_name |
||||
# 'brctl', 'delif', bridge_name, interface_name |
||||
# 'brctl', 'delif', current_bridge_name, ... |
||||
# 'brctl', 'setfd', bridge_name, ... |
||||
# 'brctl', 'stp', bridge_name, 'off' |
||||
filters.CommandFilter("/usr/sbin/brctl", "root"), |
||||
filters.CommandFilter("/sbin/brctl", "root"), |
||||
|
||||
# quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py: |
||||
# 'ip', 'link', 'add', 'link', ... |
||||
# 'ip', 'link', 'delete', interface |
||||
# 'ip', 'link', 'set', bridge_name, 'down' |
||||
# 'ip', 'link', 'set', bridge_name, 'up' |
||||
# 'ip', 'link', 'set', interface, 'down' |
||||
# 'ip', 'link', 'set', interface, 'up' |
||||
# 'ip', 'link', 'show', 'dev', device |
||||
# 'ip', 'tuntap' |
||||
# 'ip', 'tuntap' |
||||
filters.CommandFilter("/usr/sbin/ip", "root"), |
||||
filters.CommandFilter("/sbin/ip", "root"), |
||||
] |
@ -0,0 +1,36 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
|
||||
from quantum.rootwrap import filters |
||||
|
||||
filterlist = [ |
||||
# quantum/plugins/openvswitch/agent/ovs_quantum_agent.py: |
||||
# "ovs-vsctl", "--timeout=2", ... |
||||
filters.CommandFilter("/usr/bin/ovs-vsctl", "root"), |
||||
filters.CommandFilter("/bin/ovs-vsctl", "root"), |
||||
|
||||
# quantum/plugins/openvswitch/agent/ovs_quantum_agent.py: |
||||
# "ovs-ofctl", cmd, self.br_name, args |
||||
filters.CommandFilter("/usr/bin/ovs-ofctl", "root"), |
||||
filters.CommandFilter("/bin/ovs-ofctl", "root"), |
||||
|
||||
# quantum/plugins/openvswitch/agent/ovs_quantum_agent.py: |
||||
# "xe", "vif-param-get", ... |
||||
filters.CommandFilter("/usr/bin/xe", "root"), |
||||
filters.CommandFilter("/usr/sbin/xe", "root"), |
||||
] |
@ -0,0 +1,31 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
|
||||
from quantum.rootwrap import filters |
||||
|
||||
filterlist = [ |
||||
# quantum/plugins/ryu/agent/ryu_quantum_agent.py: |
||||
# "ovs-vsctl", "--timeout=2", ... |
||||
filters.CommandFilter("/usr/bin/ovs-vsctl", "root"), |
||||
filters.CommandFilter("/bin/ovs-vsctl", "root"), |
||||
|
||||
# quantum/plugins/ryu/agent/ryu_quantum_agent.py: |
||||
# "xe", "vif-param-get", ... |
||||
filters.CommandFilter("/usr/bin/xe", "root"), |
||||
filters.CommandFilter("/usr/sbin/xe", "root"), |
||||
] |
@ -0,0 +1,63 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
||||
|
||||
# Copyright (c) 2012 Openstack, LLC. |
||||
# All Rights Reserved. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
||||
# not use this file except in compliance with the License. You may obtain |
||||
# a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||||
# License for the specific language governing permissions and limitations |
||||
# under the License. |
||||
|
||||
|
||||
import os |
||||
import sys |
||||
|
||||
|
||||
FILTERS_MODULES = ['quantum.rootwrap.linuxbridge-agent', |
||||
'quantum.rootwrap.openvswitch-agent', |
||||
'quantum.rootwrap.ryu-agent', |
||||
] |
||||
|
||||
|
||||
def load_filters(): |
||||
"""Load filters from modules present in quantum.rootwrap.""" |
||||
filters = [] |
||||
for modulename in FILTERS_MODULES: |
||||
try: |
||||
__import__(modulename) |
||||
module = sys.modules[modulename] |
||||
filters = filters + module.filterlist |
||||
except ImportError: |
||||
# It's OK to have missing filters, since filter modules |
||||
# may be shipped with specific nodes |
||||
pass |
||||
return filters |
||||
|
||||
|
||||
def match_filter(filters, userargs): |
||||
""" |
||||
Checks user command and arguments through command filters and |
||||
returns the first matching filter, or None is none matched. |
||||
""" |
||||
|
||||
found_filter = None |
||||
|
||||
for f in filters: |
||||
if f.match(userargs): |
||||
# Try other filters if executable is absent |
||||
if not os.access(f.exec_path, os.X_OK): |
||||
if not found_filter: |
||||
found_filter = f |
||||
continue |
||||
# Otherwise return matching filter for execution |
||||
return f |
||||
|
||||
# No filter matched or first missing executable |
||||
return found_filter |
Loading…
Reference in new issue