Improve `helper_command' config default

This option needs to capture the current oslo_config in some way, so it
can be reconstructed in the new privileged process (when using the
'sudo/rootwrap' method).  The previous version had a "$project"
placeholder default that didn't work in real usage (as expected).  This
new version generates a default value at run-time based on the values of
cfg.CONF.{config_file,config_dir}.

If the deployer has provided an explicit value for `helper_command' then
the new cfg.CONF logic is also ignored.

Note secure deployments will capture this command line in sudoers or
rootwrap filters, and it is up to sudo/rootwrap to verify that whatever
we generate here is secure and reasonable.

Note also that this and the surrounding code is ignored when using the
'fork' method.

Change-Id: I0d31bf24cac6c26f10b5d1eebaa8f475402f73d2
This commit is contained in:
Angus Lees 2016-01-11 17:39:40 +11:00
parent 4962e83a31
commit 525a028012
3 changed files with 78 additions and 24 deletions

View File

@ -257,22 +257,6 @@ class RootwrapClientChannel(_ClientChannel):
Uses sudo/rootwrap to gain privileges.
"""
# We need to be able to reconstruct the context object in the new
# python process we'll get after rootwrap/sudo. This means we
# need to construct the context object and store it somewhere
# globally accessible, and then use that python name to find it
# again in the new python interpreter. Yes, it's all a bit
# clumsy, and none of it is required when using the fork-based
# alternative above.
# These asserts here are just attempts to catch errors earlier.
# TODO(gus): Consider replacing with setuptools entry_points.
assert context.pypath is not None, (
'RootwrapClientChannel requires priv_context '
'pypath to be specified')
assert importutils.import_class(context.pypath) is context, (
'RootwrapClientChannel requires priv_context pypath '
'for context object')
listen_sock = socket.socket(socket.AF_UNIX)
# Note we listen() on the unprivileged side, and connect to it
@ -290,9 +274,7 @@ class RootwrapClientChannel(_ClientChannel):
listen_sock.bind(sockpath)
listen_sock.listen(1)
cmd = shlex.split(context.conf.helper_command) + [
'--privsep_context', context.pypath,
'--privsep_sock_path', sockpath]
cmd = self._helper_command(context, sockpath)
LOG.info(_LI('Running privsep helper: %s'), cmd)
proc = subprocess.Popen(cmd, shell=False, stderr=_fd_logger())
if proc.wait() != 0:
@ -317,6 +299,50 @@ class RootwrapClientChannel(_ClientChannel):
super(RootwrapClientChannel, self).__init__(sock)
@staticmethod
def _helper_command(context, sockpath):
# We need to be able to reconstruct the context object in the new
# python process we'll get after rootwrap/sudo. This means we
# need to construct the context object and store it somewhere
# globally accessible, and then use that python name to find it
# again in the new python interpreter. Yes, it's all a bit
# clumsy, and none of it is required when using the fork-based
# alternative above.
# These asserts here are just attempts to catch errors earlier.
# TODO(gus): Consider replacing with setuptools entry_points.
assert context.pypath is not None, (
'RootwrapClientChannel requires priv_context '
'pypath to be specified')
assert importutils.import_class(context.pypath) is context, (
'RootwrapClientChannel requires priv_context pypath '
'for context object')
# Note order is important here. Deployments will (hopefully)
# have the exact arguments in sudoers/rootwrap configs and
# reordering args will break configs!
if context.conf.helper_command:
cmd = shlex.split(context.conf.helper_command)
else:
cmd = ['sudo', 'privsep-helper']
try:
for cfg_file in cfg.CONF.config_file:
cmd.extend(['--config-file', cfg_file])
except cfg.NoSuchOptError:
pass
try:
cmd.extend(['--config-dir', cfg.CONF.config_dir])
except cfg.NoSuchOptError:
pass
cmd.extend(
['--privsep_context', context.pypath,
'--privsep_sock_path', sockpath])
return cmd
class Daemon(object):
"""NB: This doesn't fork() - do that yourself before calling run()"""

View File

@ -46,11 +46,13 @@ OPTS = [
help=_('List of Linux capabilities retained by the privsep '
'daemon.')),
cfg.StrOpt('helper_command',
default=('sudo privsep-helper'
# TODO(gus): how do I find a good config path?
' --config-file=/etc/$project/$project.conf'),
help=_('Command to invoke via sudo/rootwrap to start '
'the privsep daemon.')),
help=_('Command to invoke to start the privsep daemon if '
'not using the "fork" method. '
'If not specified, a default is generated using '
'"sudo privsep-helper" and arguments designed to '
'recreate the current configuration. '
'This command must accept suitable --privsep_context '
'and --privsep_sock_path arguments.')),
]
_ENTRYPOINT_ATTR = 'privsep_entrypoint'

View File

@ -104,3 +104,29 @@ class TestWithContext(testctx.TestContextTestCase):
self.assertRaisesRegexp(
NameError, 'undecorated not exported',
testctx.context._wrap, undecorated)
def test_helper_command(self):
self.privsep_conf.privsep.helper_command = 'foo --bar'
cmd = daemon.RootwrapClientChannel._helper_command(
testctx.context, '/tmp/sockpath')
expected = [
'foo', '--bar',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_helper_command_default(self):
self.privsep_conf.config_file = ['/bar.conf', '/baz.conf']
self.privsep_conf.config_dir = '/foo.d'
cmd = daemon.RootwrapClientChannel._helper_command(
testctx.context, '/tmp/sockpath')
expected = [
'sudo', 'privsep-helper',
'--config-file', '/bar.conf',
'--config-file', '/baz.conf',
'--config-dir', '/foo.d',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)