Add apparmor support

Fixes to nova-network and nova-api service restarts
Charm helpers sync to bring in the AppArmorContext class
Create specific service ApiAppArmorContexts
Add service specific templates for apparmor profiles
Add aa-profile-mode in config.yaml
Apply the apparmor profile as requested: disable, enforce, complain
Add aa-profile-mode change test to amulet
Charm-helpers sync to pull in AA Profile context changes

Change-Id: I18aff4bfe131010521ea9ff544c6bf76f888afa6
This commit is contained in:
David Ames 2016-04-21 15:14:23 -07:00 committed by James Page
parent 426f553e76
commit f2eb3bf2ec
15 changed files with 432 additions and 35 deletions

5
.gitignore vendored
View File

@ -5,3 +5,8 @@ bin
tags
*.sw[nop]
*.pyc
trusty/
xenial/
precise/
tests/cirros-*-disk.img
.unit-state.db

View File

@ -316,3 +316,9 @@ options:
description: |
Apply system hardening. Supports a space-delimited list of modules
to run. Supported modules currently include os, ssh, apache and mysql.
aa-profile-mode:
type: string
default: 'disable'
description: |
Experimental enable apparmor profile. Valid settings: 'complain', 'enforce' or 'disable'.
AA disabled by default.

View File

@ -71,7 +71,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
base_charms = {
'mysql': ['precise', 'trusty'],
'mongodb': ['precise', 'trusty'],
'nrpe': ['precise', 'trusty'],
'nrpe': ['precise', 'trusty', 'wily', 'xenial'],
}
for svc in other_services:
@ -112,7 +112,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
# Charms which should use the source config option
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
'ceph-osd', 'ceph-radosgw', 'ceph-mon']
'ceph-osd', 'ceph-radosgw', 'ceph-mon', 'ceph-proxy']
# Charms which can not use openstack-origin, ie. many subordinates
no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',

View File

@ -57,6 +57,7 @@ from charmhelpers.core.host import (
mkdir,
write_file,
pwgen,
lsb_release,
)
from charmhelpers.contrib.hahelpers.cluster import (
determine_apache_port,
@ -1436,7 +1437,8 @@ class AppArmorContext(OSContextGenerator):
:return ctxt: Dictionary of the apparmor profile or None
"""
if config('aa-profile-mode') in ['disable', 'enforce', 'complain']:
ctxt = {'aa_profile_mode': config('aa-profile-mode')}
ctxt = {'aa_profile_mode': config('aa-profile-mode'),
'ubuntu_release': lsb_release()['DISTRIB_RELEASE']}
else:
ctxt = None
return ctxt

View File

@ -174,7 +174,7 @@ def init_is_systemd():
def adduser(username, password=None, shell='/bin/bash', system_user=False,
primary_group=None, secondary_groups=None, uid=None):
primary_group=None, secondary_groups=None, uid=None, home_dir=None):
"""Add a user to the system.
Will log but otherwise succeed if the user already exists.
@ -186,6 +186,7 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False,
:param str primary_group: Primary group for user; defaults to username
:param list secondary_groups: Optional list of additional groups
:param int uid: UID for user being created
:param str home_dir: Home directory for user
:returns: The password database entry struct, as returned by `pwd.getpwnam`
"""
@ -200,6 +201,8 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False,
cmd = ['useradd']
if uid:
cmd.extend(['--uid', str(uid)])
if home_dir:
cmd.extend(['--home', str(home_dir)])
if system_user or password is None:
cmd.append('--system')
else:

View File

@ -54,6 +54,10 @@ OVS_BRIDGE = 'br-int'
CEPH_CONF = '/etc/ceph/ceph.conf'
CHARM_CEPH_CONF = '/var/lib/charm/{}/ceph.conf'
NOVA_API_AA_PROFILE = 'usr.bin.nova-api'
NOVA_COMPUTE_AA_PROFILE = 'usr.bin.nova-compute'
NOVA_NETWORK_AA_PROFILE = 'usr.bin.nova-network'
def ceph_config_file():
return CHARM_CEPH_CONF.format(service_name())
@ -556,3 +560,45 @@ class HostIPContext(context.OSContextGenerator):
ctxt['host_ip'] = host_ip
return ctxt
class NovaAPIAppArmorContext(context.AppArmorContext):
def __init__(self):
super(NovaAPIAppArmorContext, self).__init__()
self.aa_profile = NOVA_API_AA_PROFILE
def __call__(self):
super(NovaAPIAppArmorContext, self).__call__()
if not self.ctxt:
return self.ctxt
self._ctxt.update({'aa_profile': self.aa_profile})
return self.ctxt
class NovaComputeAppArmorContext(context.AppArmorContext):
def __init__(self):
super(NovaComputeAppArmorContext, self).__init__()
self.aa_profile = NOVA_COMPUTE_AA_PROFILE
def __call__(self):
super(NovaComputeAppArmorContext, self).__call__()
if not self.ctxt:
return self.ctxt
self._ctxt.update({'aa_profile': self.aa_profile})
return self.ctxt
class NovaNetworkAppArmorContext(context.AppArmorContext):
def __init__(self):
super(NovaNetworkAppArmorContext, self).__init__()
self.aa_profile = NOVA_NETWORK_AA_PROFILE
def __call__(self):
super(NovaNetworkAppArmorContext, self).__call__()
if not self.ctxt:
return self.ctxt
self._ctxt.update({'aa_profile': self.aa_profile})
return self.ctxt

View File

@ -86,6 +86,7 @@ from nova_compute_utils import (
assess_status,
set_ppc64_cpu_smt_state,
destroy_libvirt_network,
network_manager,
)
from charmhelpers.contrib.network.ip import (
@ -97,7 +98,10 @@ from charmhelpers.core.unitdata import kv
from nova_compute_context import (
nova_metadata_requirement,
CEPH_SECRET_UUID,
assert_libvirt_rbd_imagebackend_allowed
assert_libvirt_rbd_imagebackend_allowed,
NovaAPIAppArmorContext,
NovaComputeAppArmorContext,
NovaNetworkAppArmorContext,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.core.sysctl import create as create_sysctl
@ -196,6 +200,12 @@ def config_changed():
CONFIGS.write_all()
NovaComputeAppArmorContext().setup_aa_profile()
if (network_manager() in ['flatmanager', 'flatdhcpmanager'] and
config('multi-host').lower() == 'yes'):
NovaAPIAppArmorContext().setup_aa_profile()
NovaNetworkAppArmorContext().setup_aa_profile()
@hooks.hook('amqp-relation-joined')
def amqp_joined(relation_id=None):

View File

@ -100,6 +100,12 @@ from nova_compute_context import (
HostIPContext,
DesignateContext,
NovaComputeVirtContext,
NOVA_API_AA_PROFILE,
NOVA_COMPUTE_AA_PROFILE,
NOVA_NETWORK_AA_PROFILE,
NovaAPIAppArmorContext,
NovaComputeAppArmorContext,
NovaNetworkAppArmorContext,
)
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
@ -179,6 +185,12 @@ LIBVIRT_BIN = '/etc/default/libvirt-bin'
LIBVIRT_BIN_OVERRIDES = '/etc/init/libvirt-bin.override'
NOVA_CONF = '%s/nova.conf' % NOVA_CONF_DIR
QEMU_KVM = '/etc/default/qemu-kvm'
NOVA_API_AA_PROFILE_PATH = ('/etc/apparmor.d/{}'.format(NOVA_API_AA_PROFILE))
NOVA_COMPUTE_AA_PROFILE_PATH = ('/etc/apparmor.d/{}'
''.format(NOVA_COMPUTE_AA_PROFILE))
NOVA_NETWORK_AA_PROFILE_PATH = ('/etc/apparmor.d/{}'
''.format(NOVA_NETWORK_AA_PROFILE))
BASE_RESOURCE_MAP = {
NOVA_CONF: {
@ -208,6 +220,18 @@ BASE_RESOURCE_MAP = {
context.LogLevelContext(),
context.InternalEndpointContext()],
},
NOVA_API_AA_PROFILE_PATH: {
'services': ['nova-api'],
'contexts': [NovaAPIAppArmorContext()],
},
NOVA_COMPUTE_AA_PROFILE_PATH: {
'services': ['nova-compute'],
'contexts': [NovaComputeAppArmorContext()],
},
NOVA_NETWORK_AA_PROFILE_PATH: {
'services': ['nova-network'],
'contexts': [NovaNetworkAppArmorContext()],
},
}
LIBVIRT_RESOURCE_MAP = {
@ -289,6 +313,9 @@ def resource_map():
resource_map[NOVA_CONF]['services'].extend(
['nova-api', 'nova-network']
)
else:
resource_map.pop(NOVA_API_AA_PROFILE_PATH)
resource_map.pop(NOVA_NETWORK_AA_PROFILE_PATH)
# Neutron/quantum requires additional contexts, as well as new resources
# depending on the plugin used.

View File

@ -0,0 +1,58 @@
# Last Modified: Thu Mar 31 18:53:33 2016
# Mode: {{aa_profile_mode}}
#include <tunables/global>
/usr/bin/nova-api {
#include <abstractions/authentication>
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/libvirt-qemu>
#include <abstractions/python>
#include <abstractions/wutmp>
capability audit_write,
capability net_admin,
capability net_raw,
capability sys_resource,
network inet raw,
/bin/* rix,
/etc/default/locale r,
/etc/environment r,
/etc/nova/** r,
/etc/sudoers r,
/etc/sudoers.d/ r,
/etc/sudoers.d/90-cloud-init-users r,
/etc/sudoers.d/90-cloudimg-ubuntu r,
/etc/sudoers.d/README r,
/etc/sudoers.d/nova_sudoers r,
/lib{,32,64}/** mr,
/run/lock/nova/nova-iptables wk,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/sbin/xtables-multi rix,
/tmp/ r,
/tmp/** rwk,
/usr/bin/ r,
/usr/bin/** rix,
/usr/lib{,32,64}/** mr,
/var/lib/nova/** rw,
/var/log/nova/nova-api.log w,
/var/tmp/ r,
/var/tmp/** rwk,
{% if ubuntu_release <= '12.04' %}
/proc/*/fd/ r,
/proc/*/mounts r,
/proc/*/net/ip_tables_names r,
/proc/*/stat r,
/proc/*/status r,
{% else %}
owner @{PROC}/@{pid}/fd/ r,
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/net/ip_tables_names r,
owner @{PROC}/@{pid}/stat r,
owner @{PROC}/@{pid}/status r,
{% endif %}
}

View File

@ -0,0 +1,75 @@
# Last Modified: Tue Apr 5 22:19:53 2016
# Mode: {{aa_profile_mode}}
#include <tunables/global>
/usr/bin/nova-compute {
#include <abstractions/authentication>
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/python>
#include <abstractions/wutmp>
capability audit_write,
capability chown,
capability dac_override,
capability dac_read_search,
capability fowner,
capability net_admin,
capability net_raw,
capability setgid,
capability setuid,
capability sys_admin,
capability sys_resource,
network inet raw,
network inet stream,
deny /* w,
/bin/* rix,
/dev/nbd* rw,
/dev/tty rw,
/etc/default/locale r,
/etc/environment r,
/etc/machine-id r,
/etc/mtab rw,
/etc/nova/** r,
/etc/sudoers r,
/etc/sudoers.d/ r,
/etc/sudoers.d/* r,
/proc/*/fd/ r,
/proc/*/net/ip_tables_names r,
/proc/*/net/psched r,
/proc/*/stat r,
/run/libvirt/libvirt-sock rw,
/run/lock/nova/nova-iptables wk,
/run/lock/qemu-nbd-nbd* w,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/sbin/xtables-multi rix,
/sys/block/ r,
/sys/devices/system/cpu/ r,
/sys/devices/system/node/ r,
/sys/devices/system/node/** r,
/sys/devices/virtual/block/nbd*/ r,
/tmp/* rw,
/tmp/*/ rw,
/usr/bin/ r,
/usr/bin/* rix,
/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2 rix,
/usr/lib{,32,64}/** mrw,
/usr/lib{,32,64}/python{2,3}.[34567]/**.{pyc,so} mrw,
/var/lib/nova/** rwk,
/var/log/nova/nova-compute.log w,
/var/run/libvirt/* rw,
/var/run/libvirt/libvirt-sock rw,
/var/tmp/* w,
{% if ubuntu_release <= '12.04' %}
/proc/*/mounts r,
/proc/*/status r,
{% else %}
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/status r,
{% endif %}
}

View File

@ -0,0 +1,55 @@
# Last Modified: Thu Mar 31 18:21:05 2016
# Mode: {{aa_profile_mode}}
#include <tunables/global>
/usr/bin/nova-network {
#include <abstractions/authentication>
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/python>
#include <abstractions/wutmp>
capability audit_write,
capability dac_override,
capability net_admin,
capability net_raw,
capability setgid,
capability setuid,
capability sys_resource,
network inet raw,
/bin/** rix,
/dev/tty rw,
/etc/default/locale r,
/etc/environment r,
/etc/iproute2/rt_scopes r,
/etc/nova/** r,
/etc/pam.d/* r,
/etc/sudoers r,
/etc/sudoers.d/ r,
/etc/sudoers.d/* r,
/proc/*/fd/ r,
/proc/*/net/ip_tables_names r,
/proc/*/stat r,
/run/lock/nova/nova-iptables wk,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/sbin/xtables-multi rix,
/tmp/** rw,
/usr/bin/ r,
/usr/bin/** rix,
/usr/lib{,32,64}/** mr,
/usr/lib{,32,64}/python{2,3}.[34567]/**.{pyc,so} mra,
/var/lib/nova/** rwk,
/var/log/nova/nova-network.log w,
/var/tmp/* a,
{% if ubuntu_release <= '12.04' %}
/proc/*/mounts r,
/proc/*/status r,
{% else %}
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/status r,
{% endif %}
}

View File

@ -45,8 +45,8 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
self._deploy()
u.log.info('Waiting on extended status checks...')
exclude_services = ['mysql']
self._auto_wait_for_status(exclude_services=exclude_services)
self.exclude_services = ['mysql']
self._auto_wait_for_status(exclude_services=self.exclude_services)
self._initialize_tests()
@ -88,8 +88,10 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
def _configure_services(self):
"""Configure all of the services."""
u.log.debug("Running all tests in Apparmor enforce mode.")
nova_config = {'config-flags': 'auto_assign_floating_ip=False',
'enable-live-migration': 'False'}
'enable-live-migration': 'False',
'aa-profile-mode': 'enforce'}
nova_cc_config = {}
if self.git:
amulet_http_proxy = os.environ.get('AMULET_HTTP_PROXY')
@ -135,12 +137,12 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
def _initialize_tests(self):
"""Perform final initialization before tests get run."""
# Access the sentries for inspecting service units
self.mysql_sentry = self.d.sentry.unit['mysql/0']
self.keystone_sentry = self.d.sentry.unit['keystone/0']
self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0']
self.nova_cc_sentry = self.d.sentry.unit['nova-cloud-controller/0']
self.glance_sentry = self.d.sentry.unit['glance/0']
self.mysql_sentry = self.d.sentry['mysql'][0]
self.keystone_sentry = self.d.sentry['keystone'][0]
self.rabbitmq_sentry = self.d.sentry['rabbitmq-server'][0]
self.nova_compute_sentry = self.d.sentry['nova-compute'][0]
self.nova_cc_sentry = self.d.sentry['nova-cloud-controller'][0]
self.glance_sentry = self.d.sentry['glance'][0]
u.log.debug('openstack release val: {}'.format(
self._get_openstack_release()))
@ -444,7 +446,7 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
'flat_interface': 'eth1',
'network_manager': 'nova.network.manager.FlatDHCPManager',
'volume_api_class': 'nova.volume.cinder.API',
'auth_strategy': 'keystone'
'auth_strategy': 'keystone',
}
}
@ -554,6 +556,7 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
u.log.debug('Making config change on {}...'.format(juju_service))
mtime = u.get_sentry_time(sentry)
self.d.configure(juju_service, set_alternate)
self._auto_wait_for_status(exclude_services=self.exclude_services)
sleep_time = 30
for s, conf_file in services.iteritems():
@ -584,3 +587,43 @@ class NovaBasicDeployment(OpenStackAmuletDeployment):
assert u.wait_on_action(action_id), "Resume action failed."
assert u.status_get(sentry_unit)[0] == "active"
u.log.debug('OK')
def test_920_change_aa_profile(self):
"""Test changing the Apparmor profile mode"""
# Services which are expected to restart upon config change,
# and corresponding config files affected by the change
services = {
'nova-compute': '/etc/apparmor.d/usr.bin.nova-compute',
'nova-network': '/etc/apparmor.d/usr.bin.nova-network',
'nova-api': '/etc/apparmor.d/usr.bin.nova-api',
}
sentry = self.nova_compute_sentry
juju_service = 'nova-compute'
mtime = u.get_sentry_time(sentry)
set_default = {'aa-profile-mode': 'enforce'}
set_alternate = {'aa-profile-mode': 'complain'}
sleep_time = 60
# Change to complain mode
self.d.configure(juju_service, set_alternate)
self._auto_wait_for_status(exclude_services=self.exclude_services)
for s, conf_file in services.iteritems():
u.log.debug("Checking that service restarted: {}".format(s))
if not u.validate_service_config_changed(sentry, mtime, s,
conf_file,
sleep_time=sleep_time):
self.d.configure(juju_service, set_default)
msg = "service {} didn't restart after config change".format(s)
amulet.raise_status(amulet.FAIL, msg=msg)
sleep_time = 0
output, code = sentry.run('aa-status '
'--complaining')
u.log.info("Assert output of aa-status --complaining >= 3. Result: {} "
"Exit Code: {}".format(output, code))
assert int(output) >= 3

View File

@ -71,7 +71,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
base_charms = {
'mysql': ['precise', 'trusty'],
'mongodb': ['precise', 'trusty'],
'nrpe': ['precise', 'trusty'],
'nrpe': ['precise', 'trusty', 'wily', 'xenial'],
}
for svc in other_services:
@ -112,7 +112,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
# Charms which should use the source config option
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
'ceph-osd', 'ceph-radosgw', 'ceph-mon']
'ceph-osd', 'ceph-radosgw', 'ceph-mon', 'ceph-proxy']
# Charms which can not use openstack-origin, ie. many subordinates
no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',

View File

@ -76,6 +76,7 @@ TO_PATCH = [
'update_nrpe_config',
'git_install',
'git_install_requested',
'network_manager',
# misc_utils
'ensure_ceph_keyring',
'execd_preinstall',

View File

@ -137,11 +137,12 @@ class NovaComputeUtilsTests(CharmTestCase):
result = utils.determine_packages()
self.assertTrue('nova-api-metadata' in result)
@patch.object(utils, 'nova_metadata_requirement')
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network_no_multihost(self, net_man):
self.skipTest('skipped until contexts are properly mocked')
def test_resource_map_nova_network_no_multihost(self, net_man, en_meta):
self.test_config.set('multi-host', 'no')
net_man.return_value = 'FlatDHCPManager'
en_meta.return_value = (False, None)
net_man.return_value = 'flatdhcpmanager'
result = utils.resource_map()
ex = {
'/etc/default/libvirt-bin': {
@ -152,41 +153,106 @@ class NovaComputeUtilsTests(CharmTestCase):
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/nova/nova-compute.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/nova/nova.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/ceph/secret.xml': {
'contexts': [],
'services': []
},
'/var/lib/charm/nova_compute/ceph.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/default/qemu-kvm': {
'contexts': [],
'services': ['qemu-kvm']
},
'/etc/init/libvirt-bin.override': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/libvirt/libvirtd.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/apparmor.d/usr.bin.nova-compute': {
'contexts': [],
'services': ['nova-compute']
},
}
self.assertEquals(ex, result)
# Mocking contexts is tricky but we can still test that
# the correct files are monitored and the correct services
# will be started
self.assertEquals(set(ex.keys()), set(result.keys()))
for k in ex.keys():
self.assertEquals(set(ex[k]['services']),
set(result[k]['services']))
@patch.object(utils, 'nova_metadata_requirement')
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network(self, net_man):
def test_resource_map_nova_network(self, net_man, en_meta):
self.skipTest('skipped until contexts are properly mocked')
net_man.return_value = 'FlatDHCPManager'
en_meta.return_value = (False, None)
self.test_config.set('multi-host', 'yes')
net_man.return_value = 'flatdhcpmanager'
result = utils.resource_map()
ex = {
'/etc/default/libvirt-bin': {
'contexts': [], 'services': ['libvirt-bin']
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/libvirt/qemu.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/nova/nova-compute.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/nova/nova.conf': {
'contexts': [],
'services': ['nova-compute', 'nova-api', 'nova-network']
}
},
'/etc/ceph/secret.xml': {
'contexts': [],
'services': []
},
'/var/lib/charm/nova_compute/ceph.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/default/qemu-kvm': {
'contexts': [],
'services': ['qemu-kvm']
},
'/etc/init/libvirt-bin.override': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/libvirt/libvirtd.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/apparmor.d/usr.bin.nova-network': {
'contexts': [],
'services': ['nova-network']
},
'/etc/apparmor.d/usr.bin.nova-compute': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/apparmor.d/usr.bin.nova-api': {
'contexts': [],
'services': ['nova-api']
},
}
self.assertEquals(ex, result)
# Mocking contexts is tricky but we can still test that
# the correct files are monitored and the correct services
# will be started
self.assertEquals(set(ex.keys()), set(result.keys()))
for k in ex.keys():
self.assertEquals(set(ex[k]['services']),
set(result[k]['services']))
@patch.object(utils, 'nova_metadata_requirement')
@patch.object(utils, 'neutron_plugin')