Allow metadata proxy running with nobody user/group

Currently metadata proxy cannot run with nobody user/group as metadata
proxy requires to connect to metadata_proxy_socket when queried.

This change allows to run metadata proxy with nobody user/group by
allowing to choose the metadata_proxy_socket mode with the new option
metadata_proxy_socket_mode (4 choices) in order to adapt socket
permissions to metadata proxy user/group.

This change refactors also where options are defined to enable
metadata_proxy_user/group options in the metadata agent.

In practice:
* if metadata_proxy_user is agent effective user or root, then:
  * metadata proxy is allowed to use rootwrap (unsecure)
  * set metadata_proxy_socket_mode = user (0o644)
* else if metadata_proxy_group is agent effective group, then:
  * metadata proxy is not allowed to use rootwrap (secure)
  * set metadata_proxy_socket_mode = group (0o664)
  * set metadata_proxy_log_watch = false
* else:
  * metadata proxy has lowest permissions (securest) but metadata proxy
    socket can be opened by everyone
  * set metadata_proxy_socket_mode = all (0o666)
  * set metadata_proxy_log_watch = false

An alternative is to set metadata_proxy_socket_mode = deduce, in such
case metadata agent uses previous rules to choose the correct mode.

DocImpact
Closes-Bug: #1427228
Change-Id: I235a0cc4f0cbd55ae4ec1570daf2ebbb6a72441d
This commit is contained in:
Cedric Brandily 2015-03-17 15:20:07 +00:00
parent aa943f930d
commit 80bea7a386
13 changed files with 221 additions and 41 deletions

View File

@ -45,6 +45,15 @@ admin_password = %SERVICE_PASSWORD%
# Location of Metadata Proxy UNIX domain socket # Location of Metadata Proxy UNIX domain socket
# metadata_proxy_socket = $state_path/metadata_proxy # metadata_proxy_socket = $state_path/metadata_proxy
# Metadata Proxy UNIX domain socket mode, 3 values allowed:
# 'deduce': deduce mode from metadata_proxy_user/group values,
# 'user': set metadata proxy socket mode to 0o644, to use when
# metadata_proxy_user is agent effective user or root,
# 'group': set metadata proxy socket mode to 0o664, to use when
# metadata_proxy_group is agent effective group,
# 'all': set metadata proxy socket mode to 0o666, to use otherwise.
# metadata_proxy_socket_mode = deduce
# Number of separate worker processes for metadata server. Defaults to # Number of separate worker processes for metadata server. Defaults to
# half the number of CPU cores # half the number of CPU cores
# metadata_workers = # metadata_workers =

View File

@ -21,7 +21,7 @@ from oslo_config import cfg
from neutron.agent.common import config from neutron.agent.common import config
from neutron.agent.dhcp import config as dhcp_config from neutron.agent.dhcp import config as dhcp_config
from neutron.agent.linux import interface from neutron.agent.linux import interface
from neutron.agent.metadata import driver as metadata_driver from neutron.agent.metadata import config as metadata_config
from neutron.common import config as common_config from neutron.common import config as common_config
from neutron.common import topics from neutron.common import topics
from neutron.openstack.common import service from neutron.openstack.common import service
@ -35,7 +35,8 @@ def register_options():
cfg.CONF.register_opts(dhcp_config.DHCP_AGENT_OPTS) cfg.CONF.register_opts(dhcp_config.DHCP_AGENT_OPTS)
cfg.CONF.register_opts(dhcp_config.DHCP_OPTS) cfg.CONF.register_opts(dhcp_config.DHCP_OPTS)
cfg.CONF.register_opts(dhcp_config.DNSMASQ_OPTS) cfg.CONF.register_opts(dhcp_config.DNSMASQ_OPTS)
cfg.CONF.register_opts(metadata_driver.MetadataDriver.OPTS) cfg.CONF.register_opts(metadata_config.DRIVER_OPTS)
cfg.CONF.register_opts(metadata_config.SHARED_OPTS)
cfg.CONF.register_opts(interface.OPTS) cfg.CONF.register_opts(interface.OPTS)

View File

@ -23,7 +23,7 @@ from neutron.agent.l3 import config as l3_config
from neutron.agent.l3 import ha from neutron.agent.l3 import ha
from neutron.agent.linux import external_process from neutron.agent.linux import external_process
from neutron.agent.linux import interface from neutron.agent.linux import interface
from neutron.agent.metadata import driver as metadata_driver from neutron.agent.metadata import config as metadata_config
from neutron.common import config as common_config from neutron.common import config as common_config
from neutron.common import topics from neutron.common import topics
from neutron.openstack.common import service from neutron.openstack.common import service
@ -32,7 +32,8 @@ from neutron import service as neutron_service
def register_opts(conf): def register_opts(conf):
conf.register_opts(l3_config.OPTS) conf.register_opts(l3_config.OPTS)
conf.register_opts(metadata_driver.MetadataDriver.OPTS) conf.register_opts(metadata_config.DRIVER_OPTS)
conf.register_opts(metadata_config.SHARED_OPTS)
conf.register_opts(ha.OPTS) conf.register_opts(ha.OPTS)
config.register_interface_driver_opts_helper(conf) config.register_interface_driver_opts_helper(conf)
config.register_use_namespaces_opts_helper(conf) config.register_use_namespaces_opts_helper(conf)

View File

@ -15,6 +15,7 @@
import fcntl import fcntl
import glob import glob
import grp
import httplib import httplib
import os import os
import pwd import pwd
@ -344,6 +345,15 @@ def is_effective_user(user_id_or_name):
return user_id_or_name == effective_user_name return user_id_or_name == effective_user_name
def is_effective_group(group_id_or_name):
"""Returns True if group_id_or_name is effective group (id/name)."""
egid = os.getegid()
if str(group_id_or_name) == str(egid):
return True
effective_group_name = grp.getgrgid(egid).gr_name
return group_id_or_name == effective_group_name
class UnixDomainHTTPConnection(httplib.HTTPConnection): class UnixDomainHTTPConnection(httplib.HTTPConnection):
"""Connection class for HTTP over UNIX domain socket.""" """Connection class for HTTP over UNIX domain socket."""
def __init__(self, host, port=None, strict=None, timeout=None, def __init__(self, host, port=None, strict=None, timeout=None,
@ -375,10 +385,12 @@ class UnixDomainWSGIServer(wsgi.Server):
self._server = None self._server = None
super(UnixDomainWSGIServer, self).__init__(name) super(UnixDomainWSGIServer, self).__init__(name)
def start(self, application, file_socket, workers, backlog): def start(self, application, file_socket, workers, backlog, mode=None):
self._socket = eventlet.listen(file_socket, self._socket = eventlet.listen(file_socket,
family=socket.AF_UNIX, family=socket.AF_UNIX,
backlog=backlog) backlog=backlog)
if mode is not None:
os.chmod(file_socket, mode)
self._launch(application, workers=workers) self._launch(application, workers=workers)

View File

@ -24,6 +24,7 @@ import six.moves.urllib.parse as urlparse
import webob import webob
from neutron.agent.linux import utils as agent_utils from neutron.agent.linux import utils as agent_utils
from neutron.agent.metadata import config
from neutron.agent import rpc as agent_rpc from neutron.agent import rpc as agent_rpc
from neutron.common import constants as n_const from neutron.common import constants as n_const
from neutron.common import rpc as n_rpc from neutron.common import rpc as n_rpc
@ -36,6 +37,12 @@ from neutron.openstack.common import loopingcall
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
MODE_MAP = {
config.USER_MODE: 0o644,
config.GROUP_MODE: 0o664,
config.ALL_MODE: 0o666,
}
class MetadataPluginAPI(object): class MetadataPluginAPI(object):
"""Agent-side RPC for metadata agent-to-plugin interaction. """Agent-side RPC for metadata agent-to-plugin interaction.
@ -305,10 +312,29 @@ class UnixDomainMetadataProxy(object):
return return
self.agent_state.pop('start_flag', None) self.agent_state.pop('start_flag', None)
def _get_socket_mode(self):
mode = self.conf.metadata_proxy_socket_mode
if mode == config.DEDUCE_MODE:
user = self.conf.metadata_proxy_user
if (not user or user == '0' or user == 'root'
or agent_utils.is_effective_user(user)):
# user is agent effective user or root => USER_MODE
mode = config.USER_MODE
else:
group = self.conf.metadata_proxy_group
if not group or agent_utils.is_effective_group(group):
# group is agent effective group => GROUP_MODE
mode = config.GROUP_MODE
else:
# otherwise => ALL_MODE
mode = config.ALL_MODE
return MODE_MAP[mode]
def run(self): def run(self):
server = agent_utils.UnixDomainWSGIServer('neutron-metadata-agent') server = agent_utils.UnixDomainWSGIServer('neutron-metadata-agent')
server.start(MetadataProxyHandler(self.conf), server.start(MetadataProxyHandler(self.conf),
self.conf.metadata_proxy_socket, self.conf.metadata_proxy_socket,
workers=self.conf.metadata_workers, workers=self.conf.metadata_workers,
backlog=self.conf.metadata_backlog) backlog=self.conf.metadata_backlog,
mode=self._get_socket_mode())
server.wait() server.wait()

View File

@ -17,6 +17,38 @@ from oslo_config import cfg
from neutron.common import utils from neutron.common import utils
SHARED_OPTS = [
cfg.StrOpt('metadata_proxy_socket',
default='$state_path/metadata_proxy',
help=_('Location for Metadata Proxy UNIX domain socket.')),
cfg.StrOpt('metadata_proxy_user',
default='',
help=_("User (uid or name) running metadata proxy after "
"its initialization (if empty: agent effective "
"user).")),
cfg.StrOpt('metadata_proxy_group',
default='',
help=_("Group (gid or name) running metadata proxy after "
"its initialization (if empty: agent effective "
"group)."))
]
DRIVER_OPTS = [
cfg.BoolOpt('metadata_proxy_watch_log',
default=None,
help=_("Enable/Disable log watch by metadata proxy. It "
"should be disabled when metadata_proxy_user/group "
"is not allowed to read/write its log file and "
"copytruncate logrotate option must be used if "
"logrotate is enabled on metadata proxy log "
"files. Option default value is deduced from "
"metadata_proxy_user: watch log is enabled if "
"metadata_proxy_user is agent effective user "
"id/name.")),
]
METADATA_PROXY_HANDLER_OPTS = [ METADATA_PROXY_HANDLER_OPTS = [
cfg.StrOpt('admin_user', cfg.StrOpt('admin_user',
help=_("Admin user")), help=_("Admin user")),
@ -66,11 +98,29 @@ METADATA_PROXY_HANDLER_OPTS = [
help=_("Private key of client certificate.")) help=_("Private key of client certificate."))
] ]
DEDUCE_MODE = 'deduce'
USER_MODE = 'user'
GROUP_MODE = 'group'
ALL_MODE = 'all'
SOCKET_MODES = (DEDUCE_MODE, USER_MODE, GROUP_MODE, ALL_MODE)
UNIX_DOMAIN_METADATA_PROXY_OPTS = [ UNIX_DOMAIN_METADATA_PROXY_OPTS = [
cfg.StrOpt('metadata_proxy_socket', cfg.StrOpt('metadata_proxy_socket_mode',
default='$state_path/metadata_proxy', default=DEDUCE_MODE,
help=_('Location for Metadata Proxy UNIX domain socket')), choices=SOCKET_MODES,
help=_("Metadata Proxy UNIX domain socket mode, 3 values "
"allowed: "
"'deduce': deduce mode from metadata_proxy_user/group "
"values, "
"'user': set metadata proxy socket mode to 0o644, to "
"use when metadata_proxy_user is agent effective user "
"or root, "
"'group': set metadata proxy socket mode to 0o664, to "
"use when metadata_proxy_group is agent effective "
"group or root, "
"'all': set metadata proxy socket mode to 0o666, to use "
"otherwise.")),
cfg.IntOpt('metadata_workers', cfg.IntOpt('metadata_workers',
default=utils.cpu_count() // 2, default=utils.cpu_count() // 2,
help=_('Number of separate worker processes for metadata ' help=_('Number of separate worker processes for metadata '

View File

@ -15,7 +15,6 @@
import os import os
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from neutron.agent.common import config from neutron.agent.common import config
@ -36,34 +35,6 @@ METADATA_SERVICE_NAME = 'metadata-proxy'
class MetadataDriver(object): class MetadataDriver(object):
OPTS = [
cfg.StrOpt('metadata_proxy_socket',
default='$state_path/metadata_proxy',
help=_('Location of Metadata Proxy UNIX domain '
'socket')),
cfg.StrOpt('metadata_proxy_user',
default='',
help=_("User (uid or name) running metadata proxy after "
"its initialization (if empty: agent effective "
"user)")),
cfg.StrOpt('metadata_proxy_group',
default='',
help=_("Group (gid or name) running metadata proxy after "
"its initialization (if empty: agent effective "
"group)")),
cfg.BoolOpt('metadata_proxy_watch_log',
default=None,
help=_("Enable/Disable log watch by metadata proxy. It "
"should be disabled when metadata_proxy_user/group "
"is not allowed to read/write its log file and "
"copytruncate logrotate option must be used if "
"logrotate is enabled on metadata proxy log "
"files. Option default value is deduced from "
"metadata_proxy_user: watch log is enabled if "
"metadata_proxy_user is agent effective user "
"id/name.")),
]
def __init__(self, l3_agent): def __init__(self, l3_agent):
self.metadata_port = l3_agent.conf.metadata_port self.metadata_port = l3_agent.conf.metadata_port
self.metadata_access_mark = l3_agent.conf.metadata_access_mark self.metadata_access_mark = l3_agent.conf.metadata_access_mark

View File

@ -28,6 +28,7 @@ LOG = logging.getLogger(__name__)
def main(): def main():
cfg.CONF.register_opts(metadata_conf.SHARED_OPTS)
cfg.CONF.register_opts(metadata_conf.UNIX_DOMAIN_METADATA_PROXY_OPTS) cfg.CONF.register_opts(metadata_conf.UNIX_DOMAIN_METADATA_PROXY_OPTS)
cfg.CONF.register_opts(metadata_conf.METADATA_PROXY_HANDLER_OPTS) cfg.CONF.register_opts(metadata_conf.METADATA_PROXY_HANDLER_OPTS)
cache.register_oslo_configs(cfg.CONF) cache.register_oslo_configs(cfg.CONF)

View File

@ -20,6 +20,8 @@ import select
import shlex import shlex
import subprocess import subprocess
import fixtures
from neutron.agent.common import config from neutron.agent.common import config
from neutron.agent.linux import ip_lib from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils from neutron.agent.linux import utils
@ -33,6 +35,33 @@ SS_SOURCE_PORT_PATTERN = re.compile(
r'^.*\s+\d+\s+.*:(?P<port>\d+)\s+[0-9:].*') r'^.*\s+\d+\s+.*:(?P<port>\d+)\s+[0-9:].*')
class RecursivePermDirFixture(fixtures.Fixture):
"""Ensure at least perms permissions on directory and ancestors."""
def __init__(self, directory, perms):
super(RecursivePermDirFixture, self).__init__()
self.directory = directory
self.least_perms = perms
def setUp(self):
super(RecursivePermDirFixture, self).setUp()
previous_directory = None
current_directory = self.directory
while previous_directory != current_directory:
perms = os.stat(current_directory).st_mode
if perms & self.least_perms != self.least_perms:
os.chmod(current_directory, perms | self.least_perms)
self.addCleanup(self.safe_chmod, current_directory, perms)
previous_directory = current_directory
current_directory = os.path.dirname(current_directory)
def safe_chmod(self, path, mode):
try:
os.chmod(path, mode)
except OSError:
pass
def get_free_namespace_port(tcp=True, namespace=None): def get_free_namespace_port(tcp=True, namespace=None):
"""Return an unused port from given namespace """Return an unused port from given namespace

View File

@ -15,6 +15,7 @@
import copy import copy
import functools import functools
import os.path
import mock import mock
import netaddr import netaddr
@ -776,12 +777,21 @@ class MetadataFakeProxyHandler(object):
class MetadataL3AgentTestCase(L3AgentTestFramework): class MetadataL3AgentTestCase(L3AgentTestFramework):
SOCKET_MODE = 0o644
def _create_metadata_fake_server(self, status): def _create_metadata_fake_server(self, status):
server = utils.UnixDomainWSGIServer('metadata-fake-server') server = utils.UnixDomainWSGIServer('metadata-fake-server')
self.addCleanup(server.stop) self.addCleanup(server.stop)
# NOTE(cbrandily): TempDir fixture creates a folder with 0o700
# permissions but metadata_proxy_socket folder must be readable by all
# users
self.useFixture(
helpers.RecursivePermDirFixture(
os.path.dirname(self.agent.conf.metadata_proxy_socket), 0o555))
server.start(MetadataFakeProxyHandler(status), server.start(MetadataFakeProxyHandler(status),
self.agent.conf.metadata_proxy_socket, self.agent.conf.metadata_proxy_socket,
workers=0, backlog=4096) workers=0, backlog=4096, mode=self.SOCKET_MODE)
def test_access_to_metadata_proxy(self): def test_access_to_metadata_proxy(self):
"""Test access to the l3-agent metadata proxy. """Test access to the l3-agent metadata proxy.
@ -830,6 +840,39 @@ class MetadataL3AgentTestCase(L3AgentTestFramework):
self.assertIn(str(webob.exc.HTTPOk.code), firstline.split()) self.assertIn(str(webob.exc.HTTPOk.code), firstline.split())
class UnprivilegedUserMetadataL3AgentTestCase(MetadataL3AgentTestCase):
"""Test metadata proxy with least privileged user.
The least privileged user has uid=65534 and is commonly named 'nobody' but
not always, that's why we use its uid.
"""
SOCKET_MODE = 0o664
def setUp(self):
super(UnprivilegedUserMetadataL3AgentTestCase, self).setUp()
self.agent.conf.set_override('metadata_proxy_user', '65534')
self.agent.conf.set_override('metadata_proxy_watch_log', False)
class UnprivilegedUserGroupMetadataL3AgentTestCase(MetadataL3AgentTestCase):
"""Test metadata proxy with least privileged user/group.
The least privileged user has uid=65534 and is commonly named 'nobody' but
not always, that's why we use its uid.
Its group has gid=65534 and is commonly named 'nobody' or 'nogroup', that's
why we use its gid.
"""
SOCKET_MODE = 0o666
def setUp(self):
super(UnprivilegedUserGroupMetadataL3AgentTestCase, self).setUp()
self.agent.conf.set_override('metadata_proxy_user', '65534')
self.agent.conf.set_override('metadata_proxy_group', '65534')
self.agent.conf.set_override('metadata_proxy_watch_log', False)
class TestDvrRouter(L3AgentTestFramework): class TestDvrRouter(L3AgentTestFramework):
def test_dvr_router_lifecycle_without_ha_without_snat_with_fips(self): def test_dvr_router_lifecycle_without_ha_without_snat_with_fips(self):
self._dvr_router_lifecycle(enable_ha=False, enable_snat=False) self._dvr_router_lifecycle(enable_ha=False, enable_snat=False)

View File

@ -215,6 +215,11 @@ class FakeUser(object):
self.pw_name = name self.pw_name = name
class FakeGroup(object):
def __init__(self, name):
self.gr_name = name
class TestBaseOSUtils(base.BaseTestCase): class TestBaseOSUtils(base.BaseTestCase):
EUID = 123 EUID = 123
@ -264,6 +269,34 @@ class TestBaseOSUtils(base.BaseTestCase):
geteuid.assert_called_once_with() geteuid.assert_called_once_with()
getpwuid.assert_called_once_with(self.EUID) getpwuid.assert_called_once_with(self.EUID)
@mock.patch('os.getegid', return_value=EGID)
@mock.patch('grp.getgrgid', return_value=FakeGroup(EGNAME))
def test_is_effective_group_id(self, getgrgid, getegid):
self.assertTrue(utils.is_effective_group(self.EGID))
getegid.assert_called_once_with()
self.assertFalse(getgrgid.called)
@mock.patch('os.getegid', return_value=EGID)
@mock.patch('grp.getgrgid', return_value=FakeGroup(EGNAME))
def test_is_effective_group_str_id(self, getgrgid, getegid):
self.assertTrue(utils.is_effective_group(str(self.EGID)))
getegid.assert_called_once_with()
self.assertFalse(getgrgid.called)
@mock.patch('os.getegid', return_value=EGID)
@mock.patch('grp.getgrgid', return_value=FakeGroup(EGNAME))
def test_is_effective_group_name(self, getgrgid, getegid):
self.assertTrue(utils.is_effective_group(self.EGNAME))
getegid.assert_called_once_with()
getgrgid.assert_called_once_with(self.EGID)
@mock.patch('os.getegid', return_value=EGID)
@mock.patch('grp.getgrgid', return_value=FakeGroup(EGNAME))
def test_is_not_effective_group(self, getgrgid, getegid):
self.assertFalse(utils.is_effective_group('wrong'))
getegid.assert_called_once_with()
getgrgid.assert_called_once_with(self.EGID)
class TestUnixDomainHttpConnection(base.BaseTestCase): class TestUnixDomainHttpConnection(base.BaseTestCase):
def test_connect(self): def test_connect(self):

View File

@ -23,6 +23,7 @@ from neutron.agent.common import config as agent_config
from neutron.agent.l3 import agent as l3_agent from neutron.agent.l3 import agent as l3_agent
from neutron.agent.l3 import config as l3_config from neutron.agent.l3 import config as l3_config
from neutron.agent.l3 import ha as l3_ha_agent from neutron.agent.l3 import ha as l3_ha_agent
from neutron.agent.metadata import config
from neutron.agent.metadata import driver as metadata_driver from neutron.agent.metadata import driver as metadata_driver
from neutron.openstack.common import uuidutils from neutron.openstack.common import uuidutils
from neutron.tests import base from neutron.tests import base
@ -77,7 +78,8 @@ class TestMetadataDriverProcess(base.BaseTestCase):
cfg.CONF.register_opts(l3_config.OPTS) cfg.CONF.register_opts(l3_config.OPTS)
cfg.CONF.register_opts(l3_ha_agent.OPTS) cfg.CONF.register_opts(l3_ha_agent.OPTS)
cfg.CONF.register_opts(metadata_driver.MetadataDriver.OPTS) cfg.CONF.register_opts(config.SHARED_OPTS)
cfg.CONF.register_opts(config.DRIVER_OPTS)
def _test_spawn_metadata_proxy(self, expected_user, expected_group, def _test_spawn_metadata_proxy(self, expected_user, expected_group,
user='', group='', watch_log=True): user='', group='', watch_log=True):

View File

@ -20,6 +20,7 @@ import webob
from neutron.agent.linux import utils as agent_utils from neutron.agent.linux import utils as agent_utils
from neutron.agent.metadata import agent from neutron.agent.metadata import agent
from neutron.agent.metadata import config
from neutron.agent import metadata_agent from neutron.agent import metadata_agent
from neutron.common import constants from neutron.common import constants
from neutron.common import utils from neutron.common import utils
@ -521,6 +522,7 @@ class TestUnixDomainMetadataProxy(base.BaseTestCase):
self.cfg.CONF.metadata_proxy_socket = '/the/path' self.cfg.CONF.metadata_proxy_socket = '/the/path'
self.cfg.CONF.metadata_workers = 0 self.cfg.CONF.metadata_workers = 0
self.cfg.CONF.metadata_backlog = 128 self.cfg.CONF.metadata_backlog = 128
self.cfg.CONF.metadata_proxy_socket_mode = config.USER_MODE
@mock.patch.object(agent_utils, 'ensure_dir') @mock.patch.object(agent_utils, 'ensure_dir')
def test_init_doesnot_exists(self, ensure_dir): def test_init_doesnot_exists(self, ensure_dir):
@ -569,7 +571,7 @@ class TestUnixDomainMetadataProxy(base.BaseTestCase):
mock.call('neutron-metadata-agent'), mock.call('neutron-metadata-agent'),
mock.call().start(handler.return_value, mock.call().start(handler.return_value,
'/the/path', workers=0, '/the/path', workers=0,
backlog=128), backlog=128, mode=0o644),
mock.call().wait()] mock.call().wait()]
) )