Add hardening support
Add charmhelpers.contrib.hardening and calls to install, config-changed, upgrade-charm and update-status hooks. Also add new config option to allow one or more hardening modules to be applied at runtime. Change-Id: I0f3035c8f8feae90ad3572297fab0ac28e7d97e2
This commit is contained in:
parent
7d7c690920
commit
7f5acef378
@ -12,3 +12,4 @@ include:
|
||||
- contrib.python
|
||||
- payload
|
||||
- contrib.charmsupport
|
||||
- contrib.hardening|inc=*
|
||||
|
@ -226,3 +226,9 @@ options:
|
||||
wait for you to execute the openstack-upgrade action for this charm on
|
||||
each unit. If False it will revert to existing behavior of upgrading
|
||||
all units on config change.
|
||||
harden:
|
||||
default:
|
||||
type: string
|
||||
description: |
|
||||
Apply system hardening. Supports a space-delimited list of modules
|
||||
to run. Supported modules currently include os, ssh, apache and mysql.
|
||||
|
5
hardening.yaml
Normal file
5
hardening.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# Overrides file for contrib.hardening. See README.hardening in
|
||||
# contrib.hardening for info on how to use this file.
|
||||
ssh:
|
||||
server:
|
||||
use_pam: 'yes' # juju requires this
|
38
hooks/charmhelpers/contrib/hardening/README.hardening.md
Normal file
38
hooks/charmhelpers/contrib/hardening/README.hardening.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Juju charm-helpers hardening library
|
||||
|
||||
## Description
|
||||
|
||||
This library provides multiple implementations of system and application
|
||||
hardening that conform to the standards of http://hardening.io/.
|
||||
|
||||
Current implementations include:
|
||||
|
||||
* OS
|
||||
* SSH
|
||||
* MySQL
|
||||
* Apache
|
||||
|
||||
## Requirements
|
||||
|
||||
* Juju Charms
|
||||
|
||||
## Usage
|
||||
|
||||
1. Synchronise this library into your charm and add the harden() decorator
|
||||
(from contrib.hardening.harden) to any functions or methods you want to use
|
||||
to trigger hardening of your application/system.
|
||||
|
||||
2. Add a config option called 'harden' to your charm config.yaml and set it to
|
||||
a space-delimited list of hardening modules you want to run e.g. "os ssh"
|
||||
|
||||
3. Override any config defaults (contrib.hardening.defaults) by adding a file
|
||||
called hardening.yaml to your charm root containing the name(s) of the
|
||||
modules whose settings you want override at root level and then any settings
|
||||
with overrides e.g.
|
||||
|
||||
os:
|
||||
general:
|
||||
desktop_enable: True
|
||||
|
||||
4. Now just run your charm as usual and hardening will be applied each time the
|
||||
hook runs.
|
15
hooks/charmhelpers/contrib/hardening/__init__.py
Normal file
15
hooks/charmhelpers/contrib/hardening/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
19
hooks/charmhelpers/contrib/hardening/apache/__init__.py
Normal file
19
hooks/charmhelpers/contrib/hardening/apache/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from os import path
|
||||
|
||||
TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
|
@ -0,0 +1,31 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
DEBUG,
|
||||
)
|
||||
from charmhelpers.contrib.hardening.apache.checks import config
|
||||
|
||||
|
||||
def run_apache_checks():
|
||||
log("Starting Apache hardening checks.", level=DEBUG)
|
||||
checks = config.get_audits()
|
||||
for check in checks:
|
||||
log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
|
||||
check.ensure_compliance()
|
||||
|
||||
log("Apache hardening checks complete.", level=DEBUG)
|
100
hooks/charmhelpers/contrib/hardening/apache/checks/config.py
Normal file
100
hooks/charmhelpers/contrib/hardening/apache/checks/config.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
INFO,
|
||||
)
|
||||
from charmhelpers.contrib.hardening.audits.file import (
|
||||
FilePermissionAudit,
|
||||
DirectoryPermissionAudit,
|
||||
NoReadWriteForOther,
|
||||
TemplatedFile,
|
||||
)
|
||||
from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
|
||||
from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
|
||||
from charmhelpers.contrib.hardening import utils
|
||||
|
||||
|
||||
def get_audits():
|
||||
"""Get Apache hardening config audits.
|
||||
|
||||
:returns: dictionary of audits
|
||||
"""
|
||||
if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
|
||||
log("Apache server does not appear to be installed on this node - "
|
||||
"skipping apache hardening", level=INFO)
|
||||
return []
|
||||
|
||||
context = ApacheConfContext()
|
||||
settings = utils.get_settings('apache')
|
||||
audits = [
|
||||
FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root',
|
||||
group='root', mode=0o0640),
|
||||
|
||||
TemplatedFile(os.path.join(settings['common']['apache_dir'],
|
||||
'mods-available/alias.conf'),
|
||||
context,
|
||||
TEMPLATES_DIR,
|
||||
mode=0o0755,
|
||||
user='root',
|
||||
service_actions=[{'service': 'apache2',
|
||||
'actions': ['restart']}]),
|
||||
|
||||
TemplatedFile(os.path.join(settings['common']['apache_dir'],
|
||||
'conf-enabled/hardening.conf'),
|
||||
context,
|
||||
TEMPLATES_DIR,
|
||||
mode=0o0640,
|
||||
user='root',
|
||||
service_actions=[{'service': 'apache2',
|
||||
'actions': ['restart']}]),
|
||||
|
||||
DirectoryPermissionAudit(settings['common']['apache_dir'],
|
||||
user='root',
|
||||
group='root',
|
||||
mode=0o640),
|
||||
|
||||
DisabledModuleAudit(settings['hardening']['modules_to_disable']),
|
||||
|
||||
NoReadWriteForOther(settings['common']['apache_dir']),
|
||||
]
|
||||
|
||||
return audits
|
||||
|
||||
|
||||
class ApacheConfContext(object):
|
||||
"""Defines the set of key/value pairs to set in a apache config file.
|
||||
|
||||
This context, when called, will return a dictionary containing the
|
||||
key/value pairs of setting to specify in the
|
||||
/etc/apache/conf-enabled/hardening.conf file.
|
||||
"""
|
||||
def __call__(self):
|
||||
settings = utils.get_settings('apache')
|
||||
ctxt = settings['hardening']
|
||||
|
||||
out = subprocess.check_output(['apache2', '-v'])
|
||||
ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
|
||||
out).group(1)
|
||||
ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
|
||||
ctxt['traceenable'] = settings['hardening']['traceenable']
|
||||
return ctxt
|
@ -0,0 +1,31 @@
|
||||
###############################################################################
|
||||
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||
# be overwritten.
|
||||
###############################################################################
|
||||
<IfModule alias_module>
|
||||
#
|
||||
# Aliases: Add here as many aliases as you need (with no limit). The format is
|
||||
# Alias fakename realname
|
||||
#
|
||||
# Note that if you include a trailing / on fakename then the server will
|
||||
# require it to be present in the URL. So "/icons" isn't aliased in this
|
||||
# example, only "/icons/". If the fakename is slash-terminated, then the
|
||||
# realname must also be slash terminated, and if the fakename omits the
|
||||
# trailing slash, the realname must also omit it.
|
||||
#
|
||||
# We include the /icons/ alias for FancyIndexed directory listings. If
|
||||
# you do not use FancyIndexing, you may comment this out.
|
||||
#
|
||||
Alias /icons/ "{{ apache_icondir }}/"
|
||||
|
||||
<Directory "{{ apache_icondir }}">
|
||||
Options -Indexes -MultiViews -FollowSymLinks
|
||||
AllowOverride None
|
||||
{% if apache_version == '2.4' -%}
|
||||
Require all granted
|
||||
{% else -%}
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
{% endif %}
|
||||
</Directory>
|
||||
</IfModule>
|
@ -0,0 +1,18 @@
|
||||
###############################################################################
|
||||
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||
# be overwritten.
|
||||
###############################################################################
|
||||
|
||||
<Location / >
|
||||
<LimitExcept {{ allowed_http_methods }} >
|
||||
# http://httpd.apache.org/docs/2.4/upgrading.html
|
||||
{% if apache_version > '2.2' -%}
|
||||
Require all granted
|
||||
{% else -%}
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
{% endif %}
|
||||
</LimitExcept>
|
||||
</Location>
|
||||
|
||||
TraceEnable {{ traceenable }}
|
63
hooks/charmhelpers/contrib/hardening/audits/__init__.py
Normal file
63
hooks/charmhelpers/contrib/hardening/audits/__init__.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class BaseAudit(object): # NO-QA
|
||||
"""Base class for hardening checks.
|
||||
|
||||
The lifecycle of a hardening check is to first check to see if the system
|
||||
is in compliance for the specified check. If it is not in compliance, the
|
||||
check method will return a value which will be supplied to the.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.unless = kwargs.get('unless', None)
|
||||
super(BaseAudit, self).__init__()
|
||||
|
||||
def ensure_compliance(self):
|
||||
"""Checks to see if the current hardening check is in compliance or
|
||||
not.
|
||||
|
||||
If the check that is performed is not in compliance, then an exception
|
||||
should be raised.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _take_action(self):
|
||||
"""Determines whether to perform the action or not.
|
||||
|
||||
Checks whether or not an action should be taken. This is determined by
|
||||
the truthy value for the unless parameter. If unless is a callback
|
||||
method, it will be invoked with no parameters in order to determine
|
||||
whether or not the action should be taken. Otherwise, the truthy value
|
||||
of the unless attribute will determine if the action should be
|
||||
performed.
|
||||
"""
|
||||
# Do the action if there isn't an unless override.
|
||||
if self.unless is None:
|
||||
return True
|
||||
|
||||
# Invoke the callback if there is one.
|
||||
if hasattr(self.unless, '__call__'):
|
||||
results = self.unless()
|
||||
if results:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
if self.unless:
|
||||
return False
|
||||
else:
|
||||
return True
|
100
hooks/charmhelpers/contrib/hardening/audits/apache.py
Normal file
100
hooks/charmhelpers/contrib/hardening/audits/apache.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from six import string_types
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
INFO,
|
||||
ERROR,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.hardening.audits import BaseAudit
|
||||
|
||||
|
||||
class DisabledModuleAudit(BaseAudit):
|
||||
"""Audits Apache2 modules.
|
||||
|
||||
Determines if the apache2 modules are enabled. If the modules are enabled
|
||||
then they are removed in the ensure_compliance.
|
||||
"""
|
||||
def __init__(self, modules):
|
||||
if modules is None:
|
||||
self.modules = []
|
||||
elif isinstance(modules, string_types):
|
||||
self.modules = [modules]
|
||||
else:
|
||||
self.modules = modules
|
||||
|
||||
def ensure_compliance(self):
|
||||
"""Ensures that the modules are not loaded."""
|
||||
if not self.modules:
|
||||
return
|
||||
|
||||
try:
|
||||
loaded_modules = self._get_loaded_modules()
|
||||
non_compliant_modules = []
|
||||
for module in self.modules:
|
||||
if module in loaded_modules:
|
||||
log("Module '%s' is enabled but should not be." %
|
||||
(module), level=INFO)
|
||||
non_compliant_modules.append(module)
|
||||
|
||||
if len(non_compliant_modules) == 0:
|
||||
return
|
||||
|
||||
for module in non_compliant_modules:
|
||||
self._disable_module(module)
|
||||
self._restart_apache()
|
||||
except subprocess.CalledProcessError as e:
|
||||
log('Error occurred auditing apache module compliance. '
|
||||
'This may have been already reported. '
|
||||
'Output is: %s' % e.output, level=ERROR)
|
||||
|
||||
@staticmethod
|
||||
def _get_loaded_modules():
|
||||
"""Returns the modules which are enabled in Apache."""
|
||||
output = subprocess.check_output(['apache2ctl', '-M'])
|
||||
modules = []
|
||||
for line in output.strip().split():
|
||||
# Each line of the enabled module output looks like:
|
||||
# module_name (static|shared)
|
||||
# Plus a header line at the top of the output which is stripped
|
||||
# out by the regex.
|
||||
matcher = re.search(r'^ (\S*)', line)
|
||||
if matcher:
|
||||
modules.append(matcher.group(1))
|
||||
return modules
|
||||
|
||||
@staticmethod
|
||||
def _disable_module(module):
|
||||
"""Disables the specified module in Apache."""
|
||||
try:
|
||||
subprocess.check_call(['a2dismod', module])
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Note: catch error here to allow the attempt of disabling
|
||||
# multiple modules in one go rather than failing after the
|
||||
# first module fails.
|
||||
log('Error occurred disabling module %s. '
|
||||
'Output is: %s' % (module, e.output), level=ERROR)
|
||||
|
||||
@staticmethod
|
||||
def _restart_apache():
|
||||
"""Restarts the apache process"""
|
||||
subprocess.check_output(['service', 'apache2', 'restart'])
|
105
hooks/charmhelpers/contrib/hardening/audits/apt.py
Normal file
105
hooks/charmhelpers/contrib/hardening/audits/apt.py
Normal file
@ -0,0 +1,105 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import # required for external apt import
|
||||
from apt import apt_pkg
|
||||
from six import string_types
|
||||
|
||||
from charmhelpers.fetch import (
|
||||
apt_cache,
|
||||
apt_purge
|
||||
)
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
DEBUG,
|
||||
WARNING,
|
||||
)
|
||||
from charmhelpers.contrib.hardening.audits import BaseAudit
|
||||
|
||||
|
||||
class AptConfig(BaseAudit):
|
||||
|
||||
def __init__(self, config, **kwargs):
|
||||
self.config = config
|
||||
|
||||
def verify_config(self):
|
||||
apt_pkg.init()
|
||||
for cfg in self.config:
|
||||
value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
|
||||
if value and value != cfg['expected']:
|
||||
log("APT config '%s' has unexpected value '%s' "
|
||||
"(expected='%s')" %
|
||||
(cfg['key'], value, cfg['expected']), level=WARNING)
|
||||
|
||||
def ensure_compliance(self):
|
||||
self.verify_config()
|
||||
|
||||
|
||||
class RestrictedPackages(BaseAudit):
|
||||
"""Class used to audit restricted packages on the system."""
|
||||
|
||||
def __init__(self, pkgs, **kwargs):
|
||||
super(RestrictedPackages, self).__init__(**kwargs)
|
||||
if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
|
||||
self.pkgs = [pkgs]
|
||||
else:
|
||||
self.pkgs = pkgs
|
||||
|
||||
def ensure_compliance(self):
|
||||
cache = apt_cache()
|
||||
|
||||
for p in self.pkgs:
|
||||
if p not in cache:
|
||||
continue
|
||||
|
||||
pkg = cache[p]
|
||||
if not self.is_virtual_package(pkg):
|
||||
if not pkg.current_ver:
|
||||
log("Package '%s' is not installed." % pkg.name,
|
||||
level=DEBUG)
|
||||
continue
|
||||
else:
|
||||
log("Restricted package '%s' is installed" % pkg.name,
|
||||
level=WARNING)
|
||||
self.delete_package(cache, pkg)
|
||||
else:
|
||||
log("Checking restricted virtual package '%s' provides" %
|
||||
pkg.name, level=DEBUG)
|
||||
self.delete_package(cache, pkg)
|
||||
|
||||
def delete_package(self, cache, pkg):
|
||||
"""Deletes the package from the system.
|
||||
|
||||
Deletes the package form the system, properly handling virtual
|
||||
packages.
|
||||
|
||||
:param cache: the apt cache
|
||||
:param pkg: the package to remove
|
||||
"""
|
||||
if self.is_virtual_package(pkg):
|
||||
log("Package '%s' appears to be virtual - purging provides" %
|
||||
pkg.name, level=DEBUG)
|
||||
for _p in pkg.provides_list:
|
||||
self.delete_package(cache, _p[2].parent_pkg)
|
||||
elif not pkg.current_ver:
|
||||
log("Package '%s' not installed" % pkg.name, level=DEBUG)
|
||||
return
|
||||
else:
|
||||
log("Purging package '%s'" % pkg.name, level=DEBUG)
|
||||
apt_purge(pkg.name)
|
||||
|
||||
def is_virtual_package(self, pkg):
|
||||
return pkg.has_provides and not pkg.has_versions
|
552
hooks/charmhelpers/contrib/hardening/audits/file.py
Normal file
552
hooks/charmhelpers/contrib/hardening/audits/file.py
Normal file
@ -0,0 +1,552 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
|
||||
from subprocess import (
|
||||
CalledProcessError,
|
||||
check_output,
|
||||
check_call,
|
||||
)
|
||||
from traceback import format_exc
|
||||
from six import string_types
|
||||
from stat import (
|
||||
S_ISGID,
|
||||
S_ISUID
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
)
|
||||
from charmhelpers.core import unitdata
|
||||
from charmhelpers.core.host import file_hash
|
||||
from charmhelpers.contrib.hardening.audits import BaseAudit
|
||||
from charmhelpers.contrib.hardening.templating import (
|
||||
get_template_path,
|
||||
render_and_write,
|
||||
)
|
||||
from charmhelpers.contrib.hardening import utils
|
||||
|
||||
|
||||
class BaseFileAudit(BaseAudit):
|
||||
"""Base class for file audits.
|
||||
|
||||
Provides api stubs for compliance check flow that must be used by any class
|
||||
that implemented this one.
|
||||
"""
|
||||
|
||||
def __init__(self, paths, always_comply=False, *args, **kwargs):
|
||||
"""
|
||||
:param paths: string path of list of paths of files we want to apply
|
||||
compliance checks are criteria to.
|
||||
:param always_comply: if true compliance criteria is always applied
|
||||
else compliance is skipped for non-existent
|
||||
paths.
|
||||
"""
|
||||
super(BaseFileAudit, self).__init__(*args, **kwargs)
|
||||
self.always_comply = always_comply
|
||||
if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
|
||||
self.paths = [paths]
|
||||
else:
|
||||
self.paths = paths
|
||||
|
||||
def ensure_compliance(self):
|
||||
"""Ensure that the all registered files comply to registered criteria.
|
||||
"""
|
||||
for p in self.paths:
|
||||
if os.path.exists(p):
|
||||
if self.is_compliant(p):
|
||||
continue
|
||||
|
||||
log('File %s is not in compliance.' % p, level=INFO)
|
||||
else:
|
||||
if not self.always_comply:
|
||||
log("Non-existent path '%s' - skipping compliance check"
|
||||
% (p), level=INFO)
|
||||
continue
|
||||
|
||||
if self._take_action():
|
||||
log("Applying compliance criteria to '%s'" % (p), level=INFO)
|
||||
self.comply(p)
|
||||
|
||||
def is_compliant(self, path):
|
||||
"""Audits the path to see if it is compliance.
|
||||
|
||||
:param path: the path to the file that should be checked.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def comply(self, path):
|
||||
"""Enforces the compliance of a path.
|
||||
|
||||
:param path: the path to the file that should be enforced.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _get_stat(cls, path):
|
||||
"""Returns the Posix st_stat information for the specified file path.
|
||||
|
||||
:param path: the path to get the st_stat information for.
|
||||
:returns: an st_stat object for the path or None if the path doesn't
|
||||
exist.
|
||||
"""
|
||||
return os.stat(path)
|
||||
|
||||
|
||||
class FilePermissionAudit(BaseFileAudit):
|
||||
"""Implements an audit for file permissions and ownership for a user.
|
||||
|
||||
This class implements functionality that ensures that a specific user/group
|
||||
will own the file(s) specified and that the permissions specified are
|
||||
applied properly to the file.
|
||||
"""
|
||||
def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
|
||||
self.user = user
|
||||
self.group = group
|
||||
self.mode = mode
|
||||
super(FilePermissionAudit, self).__init__(paths, user, group, mode,
|
||||
**kwargs)
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._user
|
||||
|
||||
@user.setter
|
||||
def user(self, name):
|
||||
try:
|
||||
user = pwd.getpwnam(name)
|
||||
except KeyError:
|
||||
log('Unknown user %s' % name, level=ERROR)
|
||||
user = None
|
||||
self._user = user
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self._group
|
||||
|
||||
@group.setter
|
||||
def group(self, name):
|
||||
try:
|
||||
group = None
|
||||
if name:
|
||||
group = grp.getgrnam(name)
|
||||
else:
|
||||
group = grp.getgrgid(self.user.pw_gid)
|
||||
except KeyError:
|
||||
log('Unknown group %s' % name, level=ERROR)
|
||||
self._group = group
|
||||
|
||||
def is_compliant(self, path):
|
||||
"""Checks if the path is in compliance.
|
||||
|
||||
Used to determine if the path specified meets the necessary
|
||||
requirements to be in compliance with the check itself.
|
||||
|
||||
:param path: the file path to check
|
||||
:returns: True if the path is compliant, False otherwise.
|
||||
"""
|
||||
stat = self._get_stat(path)
|
||||
user = self.user
|
||||
group = self.group
|
||||
|
||||
compliant = True
|
||||
if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
|
||||
log('File %s is not owned by %s:%s.' % (path, user.pw_name,
|
||||
group.gr_name),
|
||||
level=INFO)
|
||||
compliant = False
|
||||
|
||||
# POSIX refers to the st_mode bits as corresponding to both the
|
||||
# file type and file permission bits, where the least significant 12
|
||||
# bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
|
||||
# file permission bits (8-0)
|
||||
perms = stat.st_mode & 0o7777
|
||||
if perms != self.mode:
|
||||
log('File %s has incorrect permissions, currently set to %s' %
|
||||
(path, oct(stat.st_mode & 0o7777)), level=INFO)
|
||||
compliant = False
|
||||
|
||||
return compliant
|
||||
|
||||
def comply(self, path):
|
||||
"""Issues a chown and chmod to the file paths specified."""
|
||||
utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
|
||||
self.mode)
|
||||
|
||||
|
||||
class DirectoryPermissionAudit(FilePermissionAudit):
|
||||
"""Performs a permission check for the specified directory path."""
|
||||
|
||||
def __init__(self, paths, user, group=None, mode=0o600,
|
||||
recursive=True, **kwargs):
|
||||
super(DirectoryPermissionAudit, self).__init__(paths, user, group,
|
||||
mode, **kwargs)
|
||||
self.recursive = recursive
|
||||
|
||||
def is_compliant(self, path):
|
||||
"""Checks if the directory is compliant.
|
||||
|
||||
Used to determine if the path specified and all of its children
|
||||
directories are in compliance with the check itself.
|
||||
|
||||
:param path: the directory path to check
|
||||
:returns: True if the directory tree is compliant, otherwise False.
|
||||
"""
|
||||
if not os.path.isdir(path):
|
||||
log('Path specified %s is not a directory.' % path, level=ERROR)
|
||||
raise ValueError("%s is not a directory." % path)
|
||||
|
||||
if not self.recursive:
|
||||
return super(DirectoryPermissionAudit, self).is_compliant(path)
|
||||
|
||||
compliant = True
|
||||
for root, dirs, _ in os.walk(path):
|
||||
if len(dirs) > 0:
|
||||
continue
|
||||
|
||||
if not super(DirectoryPermissionAudit, self).is_compliant(root):
|
||||
compliant = False
|
||||
continue
|
||||
|
||||
return compliant
|
||||
|
||||
def comply(self, path):
|
||||
for root, dirs, _ in os.walk(path):
|
||||
if len(dirs) > 0:
|
||||
super(DirectoryPermissionAudit, self).comply(root)
|
||||
|
||||
|
||||
class ReadOnly(BaseFileAudit):
|
||||
"""Audits that files and folders are read only."""
|
||||
def __init__(self, paths, *args, **kwargs):
|
||||
super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
|
||||
|
||||
def is_compliant(self, path):
|
||||
try:
|
||||
output = check_output(['find', path, '-perm', '-go+w',
|
||||
'-type', 'f']).strip()
|
||||
|
||||
# The find above will find any files which have permission sets
|
||||
# which allow too broad of write access. As such, the path is
|
||||
# compliant if there is no output.
|
||||
if output:
|
||||
return False
|
||||
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
log('Error occurred checking finding writable files for %s. '
|
||||
'Error information is: command %s failed with returncode '
|
||||
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
|
||||
format_exc(e)), level=ERROR)
|
||||
return False
|
||||
|
||||
def comply(self, path):
|
||||
try:
|
||||
check_output(['chmod', 'go-w', '-R', path])
|
||||
except CalledProcessError as e:
|
||||
log('Error occurred removing writeable permissions for %s. '
|
||||
'Error information is: command %s failed with returncode '
|
||||
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
|
||||
format_exc(e)), level=ERROR)
|
||||
|
||||
|
||||
class NoReadWriteForOther(BaseFileAudit):
|
||||
"""Ensures that the files found under the base path are readable or
|
||||
writable by anyone other than the owner or the group.
|
||||
"""
|
||||
def __init__(self, paths):
|
||||
super(NoReadWriteForOther, self).__init__(paths)
|
||||
|
||||
def is_compliant(self, path):
|
||||
try:
|
||||
cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
|
||||
'-perm', '-o+w', '-type', 'f']
|
||||
output = check_output(cmd).strip()
|
||||
|
||||
# The find above here will find any files which have read or
|
||||
# write permissions for other, meaning there is too broad of access
|
||||
# to read/write the file. As such, the path is compliant if there's
|
||||
# no output.
|
||||
if output:
|
||||
return False
|
||||
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
log('Error occurred while finding files which are readable or '
|
||||
'writable to the world in %s. '
|
||||
'Command output is: %s.' % (path, e.output), level=ERROR)
|
||||
|
||||
def comply(self, path):
|
||||
try:
|
||||
check_output(['chmod', '-R', 'o-rw', path])
|
||||
except CalledProcessError as e:
|
||||
log('Error occurred attempting to change modes of files under '
|
||||
'path %s. Output of command is: %s' % (path, e.output))
|
||||
|
||||
|
||||
class NoSUIDSGIDAudit(BaseFileAudit):
|
||||
"""Audits that specified files do not have SUID/SGID bits set."""
|
||||
def __init__(self, paths, *args, **kwargs):
|
||||
super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
|
||||
|
||||
def is_compliant(self, path):
|
||||
stat = self._get_stat(path)
|
||||
if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def comply(self, path):
|
||||
try:
|
||||
log('Removing suid/sgid from %s.' % path, level=DEBUG)
|
||||
check_output(['chmod', '-s', path])
|
||||
except CalledProcessError as e:
|
||||
log('Error occurred removing suid/sgid from %s.'
|
||||
'Error information is: command %s failed with returncode '
|
||||
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
|
||||
format_exc(e)), level=ERROR)
|
||||
|
||||
|
||||
class TemplatedFile(BaseFileAudit):
|
||||
"""The TemplatedFileAudit audits the contents of a templated file.
|
||||
|
||||
This audit renders a file from a template, sets the appropriate file
|
||||
permissions, then generates a hashsum with which to check the content
|
||||
changed.
|
||||
"""
|
||||
def __init__(self, path, context, template_dir, mode, user='root',
|
||||
group='root', service_actions=None, **kwargs):
|
||||
self.context = context
|
||||
self.user = user
|
||||
self.group = group
|
||||
self.mode = mode
|
||||
self.template_dir = template_dir
|
||||
self.service_actions = service_actions
|
||||
super(TemplatedFile, self).__init__(paths=path, always_comply=True,
|
||||
**kwargs)
|
||||
|
||||
def is_compliant(self, path):
|
||||
"""Determines if the templated file is compliant.
|
||||
|
||||
A templated file is only compliant if it has not changed (as
|
||||
determined by its sha256 hashsum) AND its file permissions are set
|
||||
appropriately.
|
||||
|
||||
:param path: the path to check compliance.
|
||||
"""
|
||||
same_templates = self.templates_match(path)
|
||||
same_content = self.contents_match(path)
|
||||
same_permissions = self.permissions_match(path)
|
||||
|
||||
if same_content and same_permissions and same_templates:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def run_service_actions(self):
|
||||
"""Run any actions on services requested."""
|
||||
if not self.service_actions:
|
||||
return
|
||||
|
||||
for svc_action in self.service_actions:
|
||||
name = svc_action['service']
|
||||
actions = svc_action['actions']
|
||||
log("Running service '%s' actions '%s'" % (name, actions),
|
||||
level=DEBUG)
|
||||
for action in actions:
|
||||
cmd = ['service', name, action]
|
||||
try:
|
||||
check_call(cmd)
|
||||
except CalledProcessError as exc:
|
||||
log("Service name='%s' action='%s' failed - %s" %
|
||||
(name, action, exc), level=WARNING)
|
||||
|
||||
def comply(self, path):
|
||||
"""Ensures the contents and the permissions of the file.
|
||||
|
||||
:param path: the path to correct
|
||||
"""
|
||||
dirname = os.path.dirname(path)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
self.pre_write()
|
||||
render_and_write(self.template_dir, path, self.context())
|
||||
utils.ensure_permissions(path, self.user, self.group, self.mode)
|
||||
self.run_service_actions()
|
||||
self.save_checksum(path)
|
||||
self.post_write()
|
||||
|
||||
def pre_write(self):
|
||||
"""Invoked prior to writing the template."""
|
||||
pass
|
||||
|
||||
def post_write(self):
|
||||
"""Invoked after writing the template."""
|
||||
pass
|
||||
|
||||
def templates_match(self, path):
|
||||
"""Determines if the template files are the same.
|
||||
|
||||
The template file equality is determined by the hashsum of the
|
||||
template files themselves. If there is no hashsum, then the content
|
||||
cannot be sure to be the same so treat it as if they changed.
|
||||
Otherwise, return whether or not the hashsums are the same.
|
||||
|
||||
:param path: the path to check
|
||||
:returns: boolean
|
||||
"""
|
||||
template_path = get_template_path(self.template_dir, path)
|
||||
key = 'hardening:template:%s' % template_path
|
||||
template_checksum = file_hash(template_path)
|
||||
kv = unitdata.kv()
|
||||
stored_tmplt_checksum = kv.get(key)
|
||||
if not stored_tmplt_checksum:
|
||||
kv.set(key, template_checksum)
|
||||
kv.flush()
|
||||
log('Saved template checksum for %s.' % template_path,
|
||||
level=DEBUG)
|
||||
# Since we don't have a template checksum, then assume it doesn't
|
||||
# match and return that the template is different.
|
||||
return False
|
||||
elif stored_tmplt_checksum != template_checksum:
|
||||
kv.set(key, template_checksum)
|
||||
kv.flush()
|
||||
log('Updated template checksum for %s.' % template_path,
|
||||
level=DEBUG)
|
||||
return False
|
||||
|
||||
# Here the template hasn't changed based upon the calculated
|
||||
# checksum of the template and what was previously stored.
|
||||
return True
|
||||
|
||||
def contents_match(self, path):
|
||||
"""Determines if the file content is the same.
|
||||
|
||||
This is determined by comparing hashsum of the file contents and
|
||||
the saved hashsum. If there is no hashsum, then the content cannot
|
||||
be sure to be the same so treat them as if they are not the same.
|
||||
Otherwise, return True if the hashsums are the same, False if they
|
||||
are not the same.
|
||||
|
||||
:param path: the file to check.
|
||||
"""
|
||||
checksum = file_hash(path)
|
||||
|
||||
kv = unitdata.kv()
|
||||
stored_checksum = kv.get('hardening:%s' % path)
|
||||
if not stored_checksum:
|
||||
# If the checksum hasn't been generated, return False to ensure
|
||||
# the file is written and the checksum stored.
|
||||
log('Checksum for %s has not been calculated.' % path, level=DEBUG)
|
||||
return False
|
||||
elif stored_checksum != checksum:
|
||||
log('Checksum mismatch for %s.' % path, level=DEBUG)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def permissions_match(self, path):
|
||||
"""Determines if the file owner and permissions match.
|
||||
|
||||
:param path: the path to check.
|
||||
"""
|
||||
audit = FilePermissionAudit(path, self.user, self.group, self.mode)
|
||||
return audit.is_compliant(path)
|
||||
|
||||
def save_checksum(self, path):
|
||||
"""Calculates and saves the checksum for the path specified.
|
||||
|
||||
:param path: the path of the file to save the checksum.
|
||||
"""
|
||||
checksum = file_hash(path)
|
||||
kv = unitdata.kv()
|
||||
kv.set('hardening:%s' % path, checksum)
|
||||
kv.flush()
|
||||
|
||||
|
||||
class DeletedFile(BaseFileAudit):
|
||||
"""Audit to ensure that a file is deleted."""
|
||||
def __init__(self, paths):
|
||||
super(DeletedFile, self).__init__(paths)
|
||||
|
||||
def is_compliant(self, path):
|
||||
return not os.path.exists(path)
|
||||
|
||||
def comply(self, path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
class FileContentAudit(BaseFileAudit):
|
||||
"""Audit the contents of a file."""
|
||||
def __init__(self, paths, cases, **kwargs):
|
||||
# Cases we expect to pass
|
||||
self.pass_cases = cases.get('pass', [])
|
||||
# Cases we expect to fail
|
||||
self.fail_cases = cases.get('fail', [])
|
||||
super(FileContentAudit, self).__init__(paths, **kwargs)
|
||||
|
||||
def is_compliant(self, path):
|
||||
"""
|
||||
Given a set of content matching cases i.e. tuple(regex, bool) where
|
||||
bool value denotes whether or not regex is expected to match, check that
|
||||
all cases match as expected with the contents of the file. Cases can be
|
||||
expected to pass of fail.
|
||||
|
||||
:param path: Path of file to check.
|
||||
:returns: Boolean value representing whether or not all cases are
|
||||
found to be compliant.
|
||||
"""
|
||||
log("Auditing contents of file '%s'" % (path), level=DEBUG)
|
||||
with open(path, 'r') as fd:
|
||||
contents = fd.read()
|
||||
|
||||
matches = 0
|
||||
for pattern in self.pass_cases:
|
||||
key = re.compile(pattern, flags=re.MULTILINE)
|
||||
results = re.search(key, contents)
|
||||
if results:
|
||||
matches += 1
|
||||
else:
|
||||
log("Pattern '%s' was expected to pass but instead it failed"
|
||||
% (pattern), level=WARNING)
|
||||
|
||||
for pattern in self.fail_cases:
|
||||
key = re.compile(pattern, flags=re.MULTILINE)
|
||||
results = re.search(key, contents)
|
||||
if not results:
|
||||
matches += 1
|
||||
else:
|
||||
log("Pattern '%s' was expected to fail but instead it passed"
|
||||
% (pattern), level=WARNING)
|
||||
|
||||
total = len(self.pass_cases) + len(self.fail_cases)
|
||||
log("Checked %s cases and %s passed" % (total, matches), level=DEBUG)
|
||||
return matches == total
|
||||
|
||||
def comply(self, *args, **kwargs):
|
||||
"""NOOP since we just issue warnings. This is to avoid the
|
||||
NotImplememtedError.
|
||||
"""
|
||||
log("Not applying any compliance criteria, only checks.", level=INFO)
|
13
hooks/charmhelpers/contrib/hardening/defaults/apache.yaml
Normal file
13
hooks/charmhelpers/contrib/hardening/defaults/apache.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
# NOTE: this file contains the default configuration for the 'apache' hardening
|
||||
# code. If you want to override any settings you must add them to a file
|
||||
# called hardening.yaml in the root directory of your charm using the
|
||||
# name 'apache' as the root key followed by any of the following with new
|
||||
# values.
|
||||
|
||||
common:
|
||||
apache_dir: '/etc/apache2'
|
||||
|
||||
hardening:
|
||||
traceenable: 'off'
|
||||
allowed_http_methods: "GET POST"
|
||||
modules_to_disable: [ cgi, cgid ]
|
@ -0,0 +1,9 @@
|
||||
# NOTE: this schema must contain all valid keys from it's associated defaults
|
||||
# file. It is used to validate user-provided overrides.
|
||||
common:
|
||||
apache_dir:
|
||||
traceenable:
|
||||
|
||||
hardening:
|
||||
allowed_http_methods:
|
||||
modules_to_disable:
|
38
hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml
Normal file
38
hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
# NOTE: this file contains the default configuration for the 'mysql' hardening
|
||||
# code. If you want to override any settings you must add them to a file
|
||||
# called hardening.yaml in the root directory of your charm using the
|
||||
# name 'mysql' as the root key followed by any of the following with new
|
||||
# values.
|
||||
|
||||
hardening:
|
||||
mysql-conf: /etc/mysql/my.cnf
|
||||
hardening-conf: /etc/mysql/conf.d/hardening.cnf
|
||||
|
||||
security:
|
||||
# @see http://www.symantec.com/connect/articles/securing-mysql-step-step
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_chroot
|
||||
chroot: None
|
||||
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_safe-user-create
|
||||
safe-user-create: 1
|
||||
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-auth
|
||||
secure-auth: 1
|
||||
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_symbolic-links
|
||||
skip-symbolic-links: 1
|
||||
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_skip-show-database
|
||||
skip-show-database: True
|
||||
|
||||
# @see http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_local_infile
|
||||
local-infile: 0
|
||||
|
||||
# @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_allow-suspicious-udfs
|
||||
allow-suspicious-udfs: 0
|
||||
|
||||
# @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_automatic_sp_privileges
|
||||
automatic-sp-privileges: 0
|
||||
|
||||
# @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-file-priv
|
||||
secure-file-priv: /tmp
|
@ -0,0 +1,15 @@
|
||||
# NOTE: this schema must contain all valid keys from it's associated defaults
|
||||
# file. It is used to validate user-provided overrides.
|
||||
hardening:
|
||||
mysql-conf:
|
||||
hardening-conf:
|
||||
security:
|
||||
chroot:
|
||||
safe-user-create:
|
||||
secure-auth:
|
||||
skip-symbolic-links:
|
||||
skip-show-database:
|
||||
local-infile:
|
||||
allow-suspicious-udfs:
|
||||
automatic-sp-privileges:
|
||||
secure-file-priv:
|
67
hooks/charmhelpers/contrib/hardening/defaults/os.yaml
Normal file
67
hooks/charmhelpers/contrib/hardening/defaults/os.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
# NOTE: this file contains the default configuration for the 'os' hardening
|
||||
# code. If you want to override any settings you must add them to a file
|
||||
# called hardening.yaml in the root directory of your charm using the
|
||||
# name 'os' as the root key followed by any of the following with new
|
||||
# values.
|
||||
|
||||
general:
|
||||
desktop_enable: False # (type:boolean)
|
||||
|
||||
environment:
|
||||
extra_user_paths: []
|
||||
umask: 027
|
||||
root_path: /
|
||||
|
||||
auth:
|
||||
pw_max_age: 60
|
||||
# discourage password cycling
|
||||
pw_min_age: 7
|
||||
retries: 5
|
||||
lockout_time: 600
|
||||
timeout: 60
|
||||
allow_homeless: False # (type:boolean)
|
||||
pam_passwdqc_enable: True # (type:boolean)
|
||||
pam_passwdqc_options: 'min=disabled,disabled,16,12,8'
|
||||
root_ttys:
|
||||
console
|
||||
tty1
|
||||
tty2
|
||||
tty3
|
||||
tty4
|
||||
tty5
|
||||
tty6
|
||||
uid_min: 1000
|
||||
gid_min: 1000
|
||||
sys_uid_min: 100
|
||||
sys_uid_max: 999
|
||||
sys_gid_min: 100
|
||||
sys_gid_max: 999
|
||||
chfn_restrict:
|
||||
|
||||
security:
|
||||
users_allow: []
|
||||
suid_sgid_enforce: True # (type:boolean)
|
||||
# user-defined blacklist and whitelist
|
||||
suid_sgid_blacklist: []
|
||||
suid_sgid_whitelist: []
|
||||
# if this is True, remove any suid/sgid bits from files that were not in the whitelist
|
||||
suid_sgid_dry_run_on_unknown: False # (type:boolean)
|
||||
suid_sgid_remove_from_unknown: False # (type:boolean)
|
||||
# remove packages with known issues
|
||||
packages_clean: True # (type:boolean)
|
||||
packages_list:
|
||||
xinetd
|
||||
inetd
|
||||
ypserv
|
||||
telnet-server
|
||||
rsh-server
|
||||
rsync
|
||||
kernel_enable_module_loading: True # (type:boolean)
|
||||
kernel_enable_core_dump: False # (type:boolean)
|
||||
|
||||
sysctl:
|
||||
kernel_secure_sysrq: 244 # 4 + 16 + 32 + 64 + 128
|
||||
kernel_enable_sysrq: False # (type:boolean)
|
||||
forwarding: False # (type:boolean)
|
||||
ipv6_enable: False # (type:boolean)
|
||||
arp_restricted: True # (type:boolean)
|
42
hooks/charmhelpers/contrib/hardening/defaults/os.yaml.schema
Normal file
42
hooks/charmhelpers/contrib/hardening/defaults/os.yaml.schema
Normal file
@ -0,0 +1,42 @@
|
||||
# NOTE: this schema must contain all valid keys from it's associated defaults
|
||||
# file. It is used to validate user-provided overrides.
|
||||
general:
|
||||
desktop_enable:
|
||||
environment:
|
||||
extra_user_paths:
|
||||
umask:
|
||||
root_path:
|
||||
auth:
|
||||
pw_max_age:
|
||||
pw_min_age:
|
||||
retries:
|
||||
lockout_time:
|
||||
timeout:
|
||||
allow_homeless:
|
||||
pam_passwdqc_enable:
|
||||
pam_passwdqc_options:
|
||||
root_ttys:
|
||||
uid_min:
|
||||
gid_min:
|
||||
sys_uid_min:
|
||||
sys_uid_max:
|
||||
sys_gid_min:
|
||||
sys_gid_max:
|
||||
chfn_restrict:
|
||||
security:
|
||||
users_allow:
|
||||
suid_sgid_enforce:
|
||||
suid_sgid_blacklist:
|
||||
suid_sgid_whitelist:
|
||||
suid_sgid_dry_run_on_unknown:
|
||||
suid_sgid_remove_from_unknown:
|
||||
packages_clean:
|
||||
packages_list:
|
||||
kernel_enable_module_loading:
|
||||
kernel_enable_core_dump:
|
||||
sysctl:
|
||||
kernel_secure_sysrq:
|
||||
kernel_enable_sysrq:
|
||||
forwarding:
|
||||
ipv6_enable:
|
||||
arp_restricted:
|
49
hooks/charmhelpers/contrib/hardening/defaults/ssh.yaml
Normal file
49
hooks/charmhelpers/contrib/hardening/defaults/ssh.yaml
Normal file
@ -0,0 +1,49 @@
|
||||
# NOTE: this file contains the default configuration for the 'ssh' hardening
|
||||
# code. If you want to override any settings you must add them to a file
|
||||
# called hardening.yaml in the root directory of your charm using the
|
||||
# name 'ssh' as the root key followed by any of the following with new
|
||||
# values.
|
||||
|
||||
common:
|
||||
service_name: 'ssh'
|
||||
network_ipv6_enable: False # (type:boolean)
|
||||
ports: [22]
|
||||
remote_hosts: []
|
||||
|
||||
client:
|
||||
package: 'openssh-client'
|
||||
cbc_required: False # (type:boolean)
|
||||
weak_hmac: False # (type:boolean)
|
||||
weak_kex: False # (type:boolean)
|
||||
roaming: False
|
||||
password_authentication: 'no'
|
||||
|
||||
server:
|
||||
host_key_files: ['/etc/ssh/ssh_host_rsa_key', '/etc/ssh/ssh_host_dsa_key',
|
||||
'/etc/ssh/ssh_host_ecdsa_key']
|
||||
cbc_required: False # (type:boolean)
|
||||
weak_hmac: False # (type:boolean)
|
||||
weak_kex: False # (type:boolean)
|
||||
allow_root_with_key: False # (type:boolean)
|
||||
allow_tcp_forwarding: 'no'
|
||||
allow_agent_forwarding: 'no'
|
||||
allow_x11_forwarding: 'no'
|
||||
use_privilege_separation: 'sandbox'
|
||||
listen_to: ['0.0.0.0']
|
||||
use_pam: 'no'
|
||||
package: 'openssh-server'
|
||||
password_authentication: 'no'
|
||||
alive_interval: '600'
|
||||
alive_count: '3'
|
||||
sftp_enable: False # (type:boolean)
|
||||
sftp_group: 'sftponly'
|
||||
sftp_chroot: '/home/%u'
|
||||
deny_users: []
|
||||
allow_users: []
|
||||
deny_groups: []
|
||||
allow_groups: []
|
||||
print_motd: 'no'
|
||||
print_last_log: 'no'
|
||||
use_dns: 'no'
|
||||
max_auth_tries: 2
|
||||
max_sessions: 10
|
@ -0,0 +1,42 @@
|
||||
# NOTE: this schema must contain all valid keys from it's associated defaults
|
||||
# file. It is used to validate user-provided overrides.
|
||||
common:
|
||||
service_name:
|
||||
network_ipv6_enable:
|
||||
ports:
|
||||
remote_hosts:
|
||||
client:
|
||||
package:
|
||||
cbc_required:
|
||||
weak_hmac:
|
||||
weak_kex:
|
||||
roaming:
|
||||
password_authentication:
|
||||
server:
|
||||
host_key_files:
|
||||
cbc_required:
|
||||
weak_hmac:
|
||||
weak_kex:
|
||||
allow_root_with_key:
|
||||
allow_tcp_forwarding:
|
||||
allow_agent_forwarding:
|
||||
allow_x11_forwarding:
|
||||
use_privilege_separation:
|
||||
listen_to:
|
||||
use_pam:
|
||||
package:
|
||||
password_authentication:
|
||||
alive_interval:
|
||||
alive_count:
|
||||
sftp_enable:
|
||||
sftp_group:
|
||||
sftp_chroot:
|
||||
deny_users:
|
||||
allow_users:
|
||||
deny_groups:
|
||||
allow_groups:
|
||||
print_motd:
|
||||
print_last_log:
|
||||
use_dns:
|
||||
max_auth_tries:
|
||||
max_sessions:
|
84
hooks/charmhelpers/contrib/hardening/harden.py
Normal file
84
hooks/charmhelpers/contrib/hardening/harden.py
Normal file
@ -0,0 +1,84 @@
|
||||
# Copyright 2016 Canonical Limited.
|
||||
#
|
||||
# This file is part of charm-helpers.
|
||||
#
|
||||
# charm-helpers is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# charm-helpers is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import six
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
log,
|
||||
DEBUG,
|
||||
WARNING,
|
||||
)
|
||||
from charmhelpers.contrib.hardening.host.checks import run_os_checks
|
||||
from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks
|
||||
from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
|
||||
from charmhelpers.contrib.hardening.apache.checks import run_apache_checks
|
||||
|
||||
|
||||
def harden(overrides=None):
|
||||
"""Hardening decorator.
|
||||
|
||||
This is the main entry point for running the hardening stack. In order to
|
||||
run modules of the stack you must add this decorator to charm hook(s) and
|
||||
ensure that your charm config.yaml contains the 'harden' option set to
|
||||
one or more of the supported modules. Setting these will cause the
|
||||
corresponding hardening code to be run when the hook fires.
|
||||
|
||||
This decorator can and should be applied to more than one hook or function
|
||||
such that hardening modules are called multiple times. This is because
|
||||
subsequent calls will perform auditing checks that will report any changes
|
||||
to resources hardened by the first run (and possibly perform compliance
|
||||
actions as a result of any detected infractions).
|
||||
|
||||
:param overrides: Optional list of stack modules used to override those
|
||||
provided with 'harden' config.
|
||||
:returns: Returns value returned by decorated function once executed.
|
||||
"""
|
||||
def _harden_inner1(f):
|
||||
log("Hardening function '%s'" % (f.__name__), level=DEBUG)
|
||||
|
||||
def _harden_inner2(*args, **kwargs):
|
||||
RUN_CATALOG = OrderedDict([('os', run_os_checks),
|
||||
('ssh', run_ssh_checks), |