Add apparmor template for nova compute services

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

Change-Id: I886520d9e8296d2fcf0305dd021270c8530d2813
This commit is contained in:
David Ames 2016-03-30 10:00:22 -07:00
parent 0279111eeb
commit b08fe04906
10 changed files with 335 additions and 4 deletions

4
.gitignore vendored
View File

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

View File

@ -276,3 +276,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

@ -20,7 +20,7 @@ import os
import re
import time
from base64 import b64decode
from subprocess import check_call
from subprocess import check_call, CalledProcessError
import six
import yaml
@ -45,6 +45,7 @@ from charmhelpers.core.hookenv import (
INFO,
WARNING,
ERROR,
status_set,
)
from charmhelpers.core.sysctl import create as sysctl_create
@ -1491,3 +1492,92 @@ class InternalEndpointContext(OSContextGenerator):
"""
def __call__(self):
return {'use_internal_endpoints': config('use-internal-endpoints')}
class AppArmorContext(OSContextGenerator):
"""Base class for apparmor contexts."""
def __init__(self):
self._ctxt = None
self.aa_profile = None
self.aa_utils_packages = ['apparmor-utils']
@property
def ctxt(self):
if self._ctxt is not None:
return self._ctxt
self._ctxt = self._determine_ctxt()
return self._ctxt
def _determine_ctxt(self):
"""
Validate aa-profile-mode settings is disable, enforce, or complain.
: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')}
else:
ctxt = None
return ctxt
def __call__(self):
return self.ctxt
def install_aa_utils(self):
"""
Install packages required for apparmor configuration.
"""
log("Installing apparmor utils.")
ensure_packages(self.aa_utils_packages)
def manually_disable_aa_profile(self):
"""
Manually disable an apparmor profile.
If aa-profile-mode is set to disabled (default) this is required as the
template has been written but apparmor is yet unaware of the profile
and aa-disable aa-profile fails. Without this the profile would kick
into enforce mode on the next service restart.
"""
profile_path = '/etc/apparmor.d'
disable_path = '/etc/apparmor.d/disable'
if not os.path.lexists(os.path.join(disable_path, self.aa_profile)):
os.symlink(os.path.join(profile_path, self.aa_profile),
os.path.join(disable_path, self.aa_profile))
def setup_aa_profile(self):
"""
Setup an apparmor profile.
The ctxt dictionary will contain the apparmor profile mode and
the apparmor profile name.
Makes calls out to aa-disable, aa-complain, or aa-enforce to setup
the apparmor profile.
"""
self()
if not self.ctxt:
log("Not enabling apparmor Profile")
return
self.install_aa_utils()
cmd = ['aa-{}'.format(self.ctxt['aa-profile-mode'])]
cmd.append(self.ctxt['aa-profile'])
log("Setting up the apparmor profile for {} in {} mode."
"".format(self.ctxt['aa-profile'], self.ctxt['aa-profile-mode']))
try:
check_call(cmd)
except CalledProcessError as e:
# If aa-profile-mode is set to disabled (default) manual
# disabling is required as the template has been written but
# apparmor is yet unaware of the profile and aa-disable aa-profile
# fails. If aa-disable learns to read profile files first this can
# be removed.
if self.ctxt['aa-profile-mode'] == 'disable':
log("Manually disabling the apparmor profile for {}."
"".format(self.ctxt['aa-profile']))
self.manually_disable_aa_profile()
return
status_set('blocked', "Apparmor profile {} failed to be set to {}."
"".format(self.ctxt['aa-profile'],
self.ctxt['aa-profile-mode']))
raise e

View File

@ -128,6 +128,13 @@ def service(action, service_name):
return subprocess.call(cmd) == 0
def systemv_services_running():
output = subprocess.check_output(
['service', '--status-all'],
stderr=subprocess.STDOUT).decode('UTF-8')
return [row.split()[-1] for row in output.split('\n') if '[ + ]' in row]
def service_running(service_name):
"""Determine whether a system service is running"""
if init_is_systemd():
@ -140,11 +147,15 @@ def service_running(service_name):
except subprocess.CalledProcessError:
return False
else:
# This works for upstart scripts where the 'service' command
# returns a consistent string to represent running 'start/running'
if ("start/running" in output or "is running" in output or
"up and running" in output):
return True
else:
return False
# Check System V scripts init script return codes
if service_name in systemv_services_running():
return True
return False
def service_available(service_name):

View File

@ -37,6 +37,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())
@ -515,3 +519,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

@ -82,7 +82,10 @@ from charmhelpers.core.unitdata import kv
from nova_compute_context import (
CEPH_SECRET_UUID,
assert_libvirt_imagebackend_allowed
assert_libvirt_imagebackend_allowed,
NovaAPIAppArmorContext,
NovaComputeAppArmorContext,
NovaNetworkAppArmorContext,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.core.sysctl import create as create_sysctl
@ -175,6 +178,9 @@ def config_changed():
for unit in related_units(rid):
ceph_changed(rid=rid, unit=unit)
NovaAPIAppArmorContext().setup_aa_profile()
NovaComputeAppArmorContext().setup_aa_profile()
NovaNetworkAppArmorContext().setup_aa_profile()
CONFIGS.write_all()

View File

@ -81,6 +81,12 @@ from nova_compute_context import (
ceph_config_file,
HostIPContext,
DesignateContext,
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'
@ -159,6 +165,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: {
@ -186,6 +198,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 = {

View File

@ -0,0 +1,58 @@
# Last Modified: Thu Mar 31 18:53:33 2016
#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/dash ix,
/etc/nova/api-paste.ini r,
/etc/nova/nova.conf r,
/etc/nova/rootwrap.conf r,
/etc/nova/rootwrap.d/ r,
/etc/nova/rootwrap.d/api-metadata.filters r,
/etc/nova/rootwrap.d/compute.filters r,
/etc/nova/rootwrap.d/network.filters r,
/etc/sudoers r,
/etc/sudoers.d/ r,
/etc/sudoers.d/90-cloud-init-users r,
/etc/sudoers.d/README r,
/etc/sudoers.d/nova_sudoers r,
/lib{,32,64}/** mr,
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,
/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/nova-api r,
/usr/bin/nova-rootwrap rix,
/usr/bin/python2.7 ix,
/usr/bin/python3.4 ix,
/usr/bin/sudo ix,
/usr/lib{,32,64}/** mr,
/var/lib/nova/ r,
/var/lib/nova/** rw,
/var/log/nova/nova-api.log w,
/var/tmp/ r,
/var/tmp/** rwk,
}

View File

@ -0,0 +1,57 @@
# Last Modified: Tue Apr 5 22:19:53 2016
#include <tunables/global>
/usr/bin/nova-compute {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/python>
capability dac_override,
capability dac_read_search,
network inet dgram,
network inet stream,
deny /* w,
/bin/dash rix,
/bin/mv rix,
/bin/uname rix,
/etc/nova/nova-compute.conf r,
/etc/nova/nova.conf r,
/etc/nova/policy.json r,
/etc/nsswitch.conf r,
/etc/passwd r,
/proc/*/net/psched r,
/run/libvirt/libvirt-sock rw,
/run/lock/nova/nova-iptables wk,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/sys/devices/system/cpu/ r,
/sys/devices/system/node/ r,
/sys/devices/system/node/** r,
/tmp/* rw,
/tmp/*/ rw,
/usr/bin/ r,
/usr/bin/env rix,
/usr/bin/gcc-4.8 rix,
/usr/bin/nova-compute r,
/usr/bin/python2.7 ix,
/usr/bin/python3.4 ix,
/usr/bin/qemu-img rix,
/usr/bin/sudo rix,
/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2 rix,
/usr/lib{,32,64}/** rw,
/usr/lib{,32,64}/python{2,3}.[34567]/**.{pyc,so} mrw,
/var/lib/nova/* rwk,
/var/lib/nova/instances/ r,
/var/lib/nova/instances/** rwk,
/var/lib/nova/instances/locks/nova-storage-registry-lock k,
/var/log/nova/nova-compute.log w,
/var/run/libvirt/* rw,
/var/run/libvirt/libvirt-sock rw,
/var/tmp/* w,
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/net/psched r,
owner @{PROC}/@{pid}/status r,
}

View File

@ -0,0 +1,29 @@
# Last Modified: Thu Mar 31 18:21:05 2016
#include <tunables/global>
/usr/bin/nova-network {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/python>
deny /usr/bin/sudo x,
/bin/dash rix,
/bin/uname rix,
/etc/nova/nova.conf r,
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/status r,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/tmp/** rw,
/usr/bin/ r,
/usr/bin/nova-network r,
/usr/bin/python2.7 ix,
/usr/bin/python3.4 ix,
/usr/lib{,32,64}/python{2,3}.[34567]/**.{pyc,so} mra,
/var/lib/nova/* a,
/var/log/nova/nova-network.log w,
/var/tmp/* a,
}