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: If0d1e10b58ed506e0aca659f30120b8d5c96c04f
This commit is contained in:
parent
64394fa309
commit
6ab28b3639
@ -13,3 +13,4 @@ include:
|
|||||||
- contrib.network.ip
|
- contrib.network.ip
|
||||||
- contrib.python.packages
|
- contrib.python.packages
|
||||||
- contrib.charmsupport
|
- contrib.charmsupport
|
||||||
|
- contrib.hardening|inc=*
|
||||||
|
38
charmhelpers/contrib/hardening/README.hardening.md
Normal file
38
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
charmhelpers/contrib/hardening/__init__.py
Normal file
15
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
charmhelpers/contrib/hardening/apache/__init__.py
Normal file
19
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')
|
31
charmhelpers/contrib/hardening/apache/checks/__init__.py
Normal file
31
charmhelpers/contrib/hardening/apache/checks/__init__.py
Normal file
@ -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
charmhelpers/contrib/hardening/apache/checks/config.py
Normal file
100
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
|
31
charmhelpers/contrib/hardening/apache/templates/alias.conf
Normal file
31
charmhelpers/contrib/hardening/apache/templates/alias.conf
Normal file
@ -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
charmhelpers/contrib/hardening/audits/__init__.py
Normal file
63
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
charmhelpers/contrib/hardening/audits/apache.py
Normal file
100
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
charmhelpers/contrib/hardening/audits/apt.py
Normal file
105
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
charmhelpers/contrib/hardening/audits/file.py
Normal file
552
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)
|
0
charmhelpers/contrib/hardening/defaults/__init__.py
Normal file
0
charmhelpers/contrib/hardening/defaults/__init__.py
Normal file
13
charmhelpers/contrib/hardening/defaults/apache.yaml
Normal file
13
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
charmhelpers/contrib/hardening/defaults/mysql.yaml
Normal file
38
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
|
15
charmhelpers/contrib/hardening/defaults/mysql.yaml.schema
Normal file
15
charmhelpers/contrib/hardening/defaults/mysql.yaml.schema
Normal file
@ -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
charmhelpers/contrib/hardening/defaults/os.yaml
Normal file
67
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
charmhelpers/contrib/hardening/defaults/os.yaml.schema
Normal file
42
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
charmhelpers/contrib/hardening/defaults/ssh.yaml
Normal file
49
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
|
42
charmhelpers/contrib/hardening/defaults/ssh.yaml.schema
Normal file
42
charmhelpers/contrib/hardening/defaults/ssh.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.
|
||||||
|
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
charmhelpers/contrib/hardening/harden.py
Normal file
84
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),
|
||||||
|
('mysql', run_mysql_checks),
|
||||||
|
('apache', run_apache_checks)])
|
||||||
|
|
||||||
|
enabled = overrides or (config("harden") or "").split()
|
||||||
|
if enabled:
|
||||||
|
modules_to_run = []
|
||||||
|
# modules will always be performed in the following order
|
||||||
|
for module, func in six.iteritems(RUN_CATALOG):
|
||||||
|
if module in enabled:
|
||||||
|
enabled.remove(module)
|
||||||
|
modules_to_run.append(func)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
log("Unknown hardening modules '%s' - ignoring" %
|
||||||
|
(', '.join(enabled)), level=WARNING)
|
||||||
|
|
||||||
|
for hardener in modules_to_run:
|
||||||
|
log("Executing hardening module '%s'" %
|
||||||
|
(hardener.__name__), level=DEBUG)
|
||||||
|
hardener()
|
||||||
|
else:
|
||||||
|
log("No hardening applied to '%s'" % (f.__name__), level=DEBUG)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return _harden_inner2
|
||||||
|
|
||||||
|
return _harden_inner1
|
19
charmhelpers/contrib/hardening/host/__init__.py
Normal file
19
charmhelpers/contrib/hardening/host/__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')
|
50
charmhelpers/contrib/hardening/host/checks/__init__.py
Normal file
50
charmhelpers/contrib/hardening/host/checks/__init__.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 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.host.checks import (
|
||||||
|
apt,
|
||||||
|
limits,
|
||||||
|
login,
|
||||||
|
minimize_access,
|
||||||
|
pam,
|
||||||
|
profile,
|
||||||
|
securetty,
|
||||||
|
suid_sgid,
|
||||||
|
sysctl
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_os_checks():
|
||||||
|
log("Starting OS hardening checks.", level=DEBUG)
|
||||||
|
checks = apt.get_audits()
|
||||||
|
checks.extend(limits.get_audits())
|
||||||
|
checks.extend(login.get_audits())
|
||||||
|
checks.extend(minimize_access.get_audits())
|
||||||
|
checks.extend(pam.get_audits())
|
||||||
|
checks.extend(profile.get_audits())
|
||||||
|
checks.extend(securetty.get_audits())
|
||||||
|
checks.extend(suid_sgid.get_audits())
|
||||||
|
checks.extend(sysctl.get_audits())
|
||||||
|
|
||||||
|
for check in checks:
|
||||||
|
log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
|
||||||
|
check.ensure_compliance()
|
||||||
|
|
||||||
|
log("OS hardening checks complete.", level=DEBUG)
|
39
charmhelpers/contrib/hardening/host/checks/apt.py
Normal file
39
charmhelpers/contrib/hardening/host/checks/apt.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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.contrib.hardening.utils import get_settings
|
||||||
|
from charmhelpers.contrib.hardening.audits.apt import (
|
||||||
|
AptConfig,
|
||||||
|
RestrictedPackages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening apt audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = [AptConfig([{'key': 'APT::Get::AllowUnauthenticated',
|
||||||
|
'expected': 'false'}])]
|
||||||
|
|
||||||
|
settings = get_settings('os')
|
||||||
|
clean_packages = settings['security']['packages_clean']
|
||||||
|
if clean_packages:
|
||||||
|
security_packages = settings['security']['packages_list']
|
||||||
|
if security_packages:
|
||||||
|
audits.append(RestrictedPackages(security_packages))
|
||||||
|
|
||||||
|
return audits
|
55
charmhelpers/contrib/hardening/host/checks/limits.py
Normal file
55
charmhelpers/contrib/hardening/host/checks/limits.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# 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.contrib.hardening.audits.file import (
|
||||||
|
DirectoryPermissionAudit,
|
||||||
|
TemplatedFile,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening security limits audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
# Ensure that the /etc/security/limits.d directory is only writable
|
||||||
|
# by the root user, but others can execute and read.
|
||||||
|
audits.append(DirectoryPermissionAudit('/etc/security/limits.d',
|
||||||
|
user='root', group='root',
|
||||||
|
mode=0o755))
|
||||||
|
|
||||||
|
# If core dumps are not enabled, then don't allow core dumps to be
|
||||||
|
# created as they may contain sensitive information.
|
||||||
|
if not settings['security']['kernel_enable_core_dump']:
|
||||||
|
audits.append(TemplatedFile('/etc/security/limits.d/10.hardcore.conf',
|
||||||
|
SecurityLimitsContext(),
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
user='root', group='root', mode=0o0440))
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityLimitsContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
ctxt = {'disable_core_dump':
|
||||||
|
not settings['security']['kernel_enable_core_dump']}
|
||||||
|
return ctxt
|
67
charmhelpers/contrib/hardening/host/checks/login.py
Normal file
67
charmhelpers/contrib/hardening/host/checks/login.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# 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 six import string_types
|
||||||
|
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import TemplatedFile
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening login.defs audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = [TemplatedFile('/etc/login.defs', LoginContext(),
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
user='root', group='root', mode=0o0444)]
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class LoginContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
# Octal numbers in yaml end up being turned into decimal,
|
||||||
|
# so check if the umask is entered as a string (e.g. '027')
|
||||||
|
# or as an octal umask as we know it (e.g. 002). If its not
|
||||||
|
# a string assume it to be octal and turn it into an octal
|
||||||
|
# string.
|
||||||
|
umask = settings['environment']['umask']
|
||||||
|
if not isinstance(umask, string_types):
|
||||||
|
umask = '%s' % oct(umask)
|
||||||
|
|
||||||
|
ctxt = {
|
||||||
|
'additional_user_paths':
|
||||||
|
settings['environment']['extra_user_paths'],
|
||||||
|
'umask': umask,
|
||||||
|
'pwd_max_age': settings['auth']['pw_max_age'],
|
||||||
|
'pwd_min_age': settings['auth']['pw_min_age'],
|
||||||
|
'uid_min': settings['auth']['uid_min'],
|
||||||
|
'sys_uid_min': settings['auth']['sys_uid_min'],
|
||||||
|
'sys_uid_max': settings['auth']['sys_uid_max'],
|
||||||
|
'gid_min': settings['auth']['gid_min'],
|
||||||
|
'sys_gid_min': settings['auth']['sys_gid_min'],
|
||||||
|
'sys_gid_max': settings['auth']['sys_gid_max'],
|
||||||
|
'login_retries': settings['auth']['retries'],
|
||||||
|
'login_timeout': settings['auth']['timeout'],
|
||||||
|
'chfn_restrict': settings['auth']['chfn_restrict'],
|
||||||
|
'allow_login_without_home': settings['auth']['allow_homeless']
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctxt
|
@ -0,0 +1,52 @@
|
|||||||
|
# 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.contrib.hardening.audits.file import (
|
||||||
|
FilePermissionAudit,
|
||||||
|
ReadOnly,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening access audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
# Remove write permissions from $PATH folders for all regular users.
|
||||||
|
# This prevents changing system-wide commands from normal users.
|
||||||
|
path_folders = {'/usr/local/sbin',
|
||||||
|
'/usr/local/bin',
|
||||||
|
'/usr/sbin',
|
||||||
|
'/usr/bin',
|
||||||
|
'/bin'}
|
||||||
|
extra_user_paths = settings['environment']['extra_user_paths']
|
||||||
|
path_folders.update(extra_user_paths)
|
||||||
|
audits.append(ReadOnly(path_folders))
|
||||||
|
|
||||||
|
# Only allow the root user to have access to the shadow file.
|
||||||
|
audits.append(FilePermissionAudit('/etc/shadow', 'root', 'root', 0o0600))
|
||||||
|
|
||||||
|
if 'change_user' not in settings['security']['users_allow']:
|
||||||
|
# su should only be accessible to user and group root, unless it is
|
||||||
|
# expressly defined to allow users to change to root via the
|
||||||
|
# security_users_allow config option.
|
||||||
|
audits.append(FilePermissionAudit('/bin/su', 'root', 'root', 0o750))
|
||||||
|
|
||||||
|
return audits
|
134
charmhelpers/contrib/hardening/host/checks/pam.py
Normal file
134
charmhelpers/contrib/hardening/host/checks/pam.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# 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 subprocess import (
|
||||||
|
check_output,
|
||||||
|
CalledProcessError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
DEBUG,
|
||||||
|
ERROR,
|
||||||
|
)
|
||||||
|
from charmhelpers.fetch import (
|
||||||
|
apt_install,
|
||||||
|
apt_purge,
|
||||||
|
apt_update,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import (
|
||||||
|
TemplatedFile,
|
||||||
|
DeletedFile,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening PAM authentication audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
if settings['auth']['pam_passwdqc_enable']:
|
||||||
|
audits.append(PasswdqcPAM('/etc/passwdqc.conf'))
|
||||||
|
|
||||||
|
if settings['auth']['retries']:
|
||||||
|
audits.append(Tally2PAM('/usr/share/pam-configs/tally2'))
|
||||||
|
else:
|
||||||
|
audits.append(DeletedFile('/usr/share/pam-configs/tally2'))
|
||||||
|
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class PasswdqcPAMContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = {}
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
ctxt['auth_pam_passwdqc_options'] = \
|
||||||
|
settings['auth']['pam_passwdqc_options']
|
||||||
|
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class PasswdqcPAM(TemplatedFile):
|
||||||
|
"""The PAM Audit verifies the linux PAM settings."""
|
||||||
|
def __init__(self, path):
|
||||||
|
super(PasswdqcPAM, self).__init__(path=path,
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
context=PasswdqcPAMContext(),
|
||||||
|
user='root',
|
||||||
|
group='root',
|
||||||
|
mode=0o0640)
|
||||||
|
|
||||||
|
def pre_write(self):
|
||||||
|
# Always remove?
|
||||||
|
for pkg in ['libpam-ccreds', 'libpam-cracklib']:
|
||||||
|
log("Purging package '%s'" % pkg, level=DEBUG),
|
||||||
|
apt_purge(pkg)
|
||||||
|
|
||||||
|
apt_update(fatal=True)
|
||||||
|
for pkg in ['libpam-passwdqc']:
|
||||||
|
log("Installing package '%s'" % pkg, level=DEBUG),
|
||||||
|
apt_install(pkg)
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
"""Updates the PAM configuration after the file has been written"""
|
||||||
|
try:
|
||||||
|
check_output(['pam-auth-update', '--package'])
|
||||||
|
except CalledProcessError as e:
|
||||||
|
log('Error calling pam-auth-update: %s' % e, level=ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class Tally2PAMContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = {}
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
ctxt['auth_lockout_time'] = settings['auth']['lockout_time']
|
||||||
|
ctxt['auth_retries'] = settings['auth']['retries']
|
||||||
|
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class Tally2PAM(TemplatedFile):
|
||||||
|
"""The PAM Audit verifies the linux PAM settings."""
|
||||||
|
def __init__(self, path):
|
||||||
|
super(Tally2PAM, self).__init__(path=path,
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
context=Tally2PAMContext(),
|
||||||
|
user='root',
|
||||||
|
group='root',
|
||||||
|
mode=0o0640)
|
||||||
|
|
||||||
|
def pre_write(self):
|
||||||
|
# Always remove?
|
||||||
|
apt_purge('libpam-ccreds')
|
||||||
|
apt_update(fatal=True)
|
||||||
|
apt_install('libpam-modules')
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
"""Updates the PAM configuration after the file has been written"""
|
||||||
|
try:
|
||||||
|
check_output(['pam-auth-update', '--package'])
|
||||||
|
except CalledProcessError as e:
|
||||||
|
log('Error calling pam-auth-update: %s' % e, level=ERROR)
|
45
charmhelpers/contrib/hardening/host/checks/profile.py
Normal file
45
charmhelpers/contrib/hardening/host/checks/profile.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 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.contrib.hardening.audits.file import TemplatedFile
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening profile audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
# If core dumps are not enabled, then don't allow core dumps to be
|
||||||
|
# created as they may contain sensitive information.
|
||||||
|
if not settings['security']['kernel_enable_core_dump']:
|
||||||
|
audits.append(TemplatedFile('/etc/profile.d/pinerolo_profile.sh',
|
||||||
|
ProfileContext(),
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
mode=0o0755, user='root', group='root'))
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = {}
|
||||||
|
return ctxt
|
39
charmhelpers/contrib/hardening/host/checks/securetty.py
Normal file
39
charmhelpers/contrib/hardening/host/checks/securetty.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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.contrib.hardening.audits.file import TemplatedFile
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening Secure TTY audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
audits.append(TemplatedFile('/etc/securetty', SecureTTYContext(),
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
mode=0o0400, user='root', group='root'))
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class SecureTTYContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
ctxt = {'ttys': settings['auth']['root_ttys']}
|
||||||
|
return ctxt
|
131
charmhelpers/contrib/hardening/host/checks/suid_sgid.py
Normal file
131
charmhelpers/contrib/hardening/host/checks/suid_sgid.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# 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 subprocess
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
INFO,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
BLACKLIST = ['/usr/bin/rcp', '/usr/bin/rlogin', '/usr/bin/rsh',
|
||||||
|
'/usr/libexec/openssh/ssh-keysign',
|
||||||
|
'/usr/lib/openssh/ssh-keysign',
|
||||||
|
'/sbin/netreport',
|
||||||
|
'/usr/sbin/usernetctl',
|
||||||
|
'/usr/sbin/userisdnctl',
|
||||||
|
'/usr/sbin/pppd',
|
||||||
|
'/usr/bin/lockfile',
|
||||||
|
'/usr/bin/mail-lock',
|
||||||
|
'/usr/bin/mail-unlock',
|
||||||
|
'/usr/bin/mail-touchlock',
|
||||||
|
'/usr/bin/dotlockfile',
|
||||||
|
'/usr/bin/arping',
|
||||||
|
'/usr/sbin/uuidd',
|
||||||
|
'/usr/bin/mtr',
|
||||||
|
'/usr/lib/evolution/camel-lock-helper-1.2',
|
||||||
|
'/usr/lib/pt_chown',
|
||||||
|
'/usr/lib/eject/dmcrypt-get-device',
|
||||||
|
'/usr/lib/mc/cons.saver']
|
||||||
|
|
||||||
|
WHITELIST = ['/bin/mount', '/bin/ping', '/bin/su', '/bin/umount',
|
||||||
|
'/sbin/pam_timestamp_check', '/sbin/unix_chkpwd', '/usr/bin/at',
|
||||||
|
'/usr/bin/gpasswd', '/usr/bin/locate', '/usr/bin/newgrp',
|
||||||
|
'/usr/bin/passwd', '/usr/bin/ssh-agent',
|
||||||
|
'/usr/libexec/utempter/utempter', '/usr/sbin/lockdev',
|
||||||
|
'/usr/sbin/sendmail.sendmail', '/usr/bin/expiry',
|
||||||
|
'/bin/ping6', '/usr/bin/traceroute6.iputils',
|
||||||
|
'/sbin/mount.nfs', '/sbin/umount.nfs',
|
||||||
|
'/sbin/mount.nfs4', '/sbin/umount.nfs4',
|
||||||
|
'/usr/bin/crontab',
|
||||||
|
'/usr/bin/wall', '/usr/bin/write',
|
||||||
|
'/usr/bin/screen',
|
||||||
|
'/usr/bin/mlocate',
|
||||||
|
'/usr/bin/chage', '/usr/bin/chfn', '/usr/bin/chsh',
|
||||||
|
'/bin/fusermount',
|
||||||
|
'/usr/bin/pkexec',
|
||||||
|
'/usr/bin/sudo', '/usr/bin/sudoedit',
|
||||||
|
'/usr/sbin/postdrop', '/usr/sbin/postqueue',
|
||||||
|
'/usr/sbin/suexec',
|
||||||
|
'/usr/lib/squid/ncsa_auth', '/usr/lib/squid/pam_auth',
|
||||||
|
'/usr/kerberos/bin/ksu',
|
||||||
|
'/usr/sbin/ccreds_validate',
|
||||||
|
'/usr/bin/Xorg',
|
||||||
|
'/usr/bin/X',
|
||||||
|
'/usr/lib/dbus-1.0/dbus-daemon-launch-helper',
|
||||||
|
'/usr/lib/vte/gnome-pty-helper',
|
||||||
|
'/usr/lib/libvte9/gnome-pty-helper',
|
||||||
|
'/usr/lib/libvte-2.90-9/gnome-pty-helper']
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening suid/sgid audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
checks = []
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
if not settings['security']['suid_sgid_enforce']:
|
||||||
|
log("Skipping suid/sgid hardening", level=INFO)
|
||||||
|
return checks
|
||||||
|
|
||||||
|
# Build the blacklist and whitelist of files for suid/sgid checks.
|
||||||
|
# There are a total of 4 lists:
|
||||||
|
# 1. the system blacklist
|
||||||
|
# 2. the system whitelist
|
||||||
|
# 3. the user blacklist
|
||||||
|
# 4. the user whitelist
|
||||||
|
#
|
||||||
|
# The blacklist is the set of paths which should NOT have the suid/sgid bit
|
||||||
|
# set and the whitelist is the set of paths which MAY have the suid/sgid
|
||||||
|
# bit setl. The user whitelist/blacklist effectively override the system
|
||||||
|
# whitelist/blacklist.
|
||||||
|
u_b = settings['security']['suid_sgid_blacklist']
|
||||||
|
u_w = settings['security']['suid_sgid_whitelist']
|
||||||
|
|
||||||
|
blacklist = set(BLACKLIST) - set(u_w + u_b)
|
||||||
|
whitelist = set(WHITELIST) - set(u_b + u_w)
|
||||||
|
|
||||||
|
checks.append(NoSUIDSGIDAudit(blacklist))
|
||||||
|
|
||||||
|
dry_run = settings['security']['suid_sgid_dry_run_on_unknown']
|
||||||
|
|
||||||
|
if settings['security']['suid_sgid_remove_from_unknown'] or dry_run:
|
||||||
|
# If the policy is a dry_run (e.g. complain only) or remove unknown
|
||||||
|
# suid/sgid bits then find all of the paths which have the suid/sgid
|
||||||
|
# bit set and then remove the whitelisted paths.
|
||||||
|
root_path = settings['environment']['root_path']
|
||||||
|
unknown_paths = find_paths_with_suid_sgid(root_path) - set(whitelist)
|
||||||
|
checks.append(NoSUIDSGIDAudit(unknown_paths, unless=dry_run))
|
||||||
|
|
||||||
|
return checks
|
||||||
|
|
||||||
|
|
||||||
|
def find_paths_with_suid_sgid(root_path):
|
||||||
|
"""Finds all paths/files which have an suid/sgid bit enabled.
|
||||||
|
|
||||||
|
Starting with the root_path, this will recursively find all paths which
|
||||||
|
have an suid or sgid bit set.
|
||||||
|
"""
|
||||||
|
cmd = ['find', root_path, '-perm', '-4000', '-o', '-perm', '-2000',
|
||||||
|
'-type', 'f', '!', '-path', '/proc/*', '-print']
|
||||||
|
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
out, _ = p.communicate()
|
||||||
|
return set(out.split('\n'))
|
211
charmhelpers/contrib/hardening/host/checks/sysctl.py
Normal file
211
charmhelpers/contrib/hardening/host/checks/sysctl.py
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
# 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 platform
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import (
|
||||||
|
FilePermissionAudit,
|
||||||
|
TemplatedFile,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||||
|
|
||||||
|
|
||||||
|
SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s
|
||||||
|
net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s
|
||||||
|
net.ipv4.conf.all.rp_filter=1
|
||||||
|
net.ipv4.conf.default.rp_filter=1
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts=1
|
||||||
|
net.ipv4.icmp_ignore_bogus_error_responses=1
|
||||||
|
net.ipv4.icmp_ratelimit=100
|
||||||
|
net.ipv4.icmp_ratemask=88089
|
||||||
|
net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s
|
||||||
|
net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s
|
||||||
|
net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s
|
||||||
|
net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s
|
||||||
|
net.ipv4.tcp_rfc1337=1
|
||||||
|
net.ipv4.tcp_syncookies=1
|
||||||
|
net.ipv4.conf.all.shared_media=1
|
||||||
|
net.ipv4.conf.default.shared_media=1
|
||||||
|
net.ipv4.conf.all.accept_source_route=0
|
||||||
|
net.ipv4.conf.default.accept_source_route=0
|
||||||
|
net.ipv4.conf.all.accept_redirects=0
|
||||||
|
net.ipv4.conf.default.accept_redirects=0
|
||||||
|
net.ipv6.conf.all.accept_redirects=0
|
||||||
|
net.ipv6.conf.default.accept_redirects=0
|
||||||
|
net.ipv4.conf.all.secure_redirects=0
|
||||||
|
net.ipv4.conf.default.secure_redirects=0
|
||||||
|
net.ipv4.conf.all.send_redirects=0
|
||||||
|
net.ipv4.conf.default.send_redirects=0
|
||||||
|
net.ipv4.conf.all.log_martians=0
|
||||||
|
net.ipv6.conf.default.router_solicitations=0
|
||||||
|
net.ipv6.conf.default.accept_ra_rtr_pref=0
|
||||||
|
net.ipv6.conf.default.accept_ra_pinfo=0
|
||||||
|
net.ipv6.conf.default.accept_ra_defrtr=0
|
||||||
|
net.ipv6.conf.default.autoconf=0
|
||||||
|
net.ipv6.conf.default.dad_transmits=0
|
||||||
|
net.ipv6.conf.default.max_addresses=1
|
||||||
|
net.ipv6.conf.all.accept_ra=0
|
||||||
|
net.ipv6.conf.default.accept_ra=0
|
||||||
|
kernel.modules_disabled=%(kernel_modules_disabled)s
|
||||||
|
kernel.sysrq=%(kernel_sysrq)s
|
||||||
|
fs.suid_dumpable=%(fs_suid_dumpable)s
|
||||||
|
kernel.randomize_va_space=2
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get OS hardening sysctl audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = []
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
|
||||||
|
# Apply the sysctl settings which are configured to be applied.
|
||||||
|
audits.append(SysctlConf())
|
||||||
|
# Make sure that only root has access to the sysctl.conf file, and
|
||||||
|
# that it is read-only.
|
||||||
|
audits.append(FilePermissionAudit('/etc/sysctl.conf',
|
||||||
|
user='root',
|
||||||
|
group='root', mode=0o0440))
|
||||||
|
# If module loading is not enabled, then ensure that the modules
|
||||||
|
# file has the appropriate permissions and rebuild the initramfs
|
||||||
|
if not settings['security']['kernel_enable_module_loading']:
|
||||||
|
audits.append(ModulesTemplate())
|
||||||
|
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class ModulesContext(object):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
with open('/proc/cpuinfo', 'r') as fd:
|
||||||
|
cpuinfo = fd.readlines()
|
||||||
|
|
||||||
|
for line in cpuinfo:
|
||||||
|
match = re.search(r"^vendor_id\s+:\s+(.+)", line)
|
||||||
|
if match:
|
||||||
|
vendor = match.group(1)
|
||||||
|
|
||||||
|
if vendor == "GenuineIntel":
|
||||||
|
vendor = "intel"
|
||||||
|
elif vendor == "AuthenticAMD":
|
||||||
|
vendor = "amd"
|
||||||
|
|
||||||
|
ctxt = {'arch': platform.processor(),
|
||||||
|
'cpuVendor': vendor,
|
||||||
|
'desktop_enable': settings['general']['desktop_enable']}
|
||||||
|
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class ModulesTemplate(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ModulesTemplate, self).__init__('/etc/initramfs-tools/modules',
|
||||||
|
ModulesContext(),
|
||||||
|
templates_dir=TEMPLATES_DIR,
|
||||||
|
user='root', group='root',
|
||||||
|
mode=0o0440)
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
subprocess.check_call(['update-initramfs', '-u'])
|
||||||
|
|
||||||
|
|
||||||
|
class SysCtlHardeningContext(object):
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('os')
|
||||||
|
ctxt = {'sysctl': {}}
|
||||||
|
|
||||||
|
log("Applying sysctl settings", level=INFO)
|
||||||
|
extras = {'net_ipv4_ip_forward': 0,
|
||||||
|
'net_ipv6_conf_all_forwarding': 0,
|
||||||
|
'net_ipv6_conf_all_disable_ipv6': 1,
|
||||||
|
'net_ipv4_tcp_timestamps': 0,
|
||||||
|
'net_ipv4_conf_all_arp_ignore': 0,
|
||||||
|
'net_ipv4_conf_all_arp_announce': 0,
|
||||||
|
'kernel_sysrq': 0,
|
||||||
|
'fs_suid_dumpable': 0,
|
||||||
|
'kernel_modules_disabled': 1}
|
||||||
|
|
||||||
|
if settings['sysctl']['ipv6_enable']:
|
||||||
|
extras['net_ipv6_conf_all_disable_ipv6'] = 0
|
||||||
|
|
||||||
|
if settings['sysctl']['forwarding']:
|
||||||
|
extras['net_ipv4_ip_forward'] = 1
|
||||||
|
extras['net_ipv6_conf_all_forwarding'] = 1
|
||||||
|
|
||||||
|
if settings['sysctl']['arp_restricted']:
|
||||||
|
extras['net_ipv4_conf_all_arp_ignore'] = 1
|
||||||
|
extras['net_ipv4_conf_all_arp_announce'] = 2
|
||||||
|
|
||||||
|
if settings['security']['kernel_enable_module_loading']:
|
||||||
|
extras['kernel_modules_disabled'] = 0
|
||||||
|
|
||||||
|
if settings['sysctl']['kernel_enable_sysrq']:
|
||||||
|
sysrq_val = settings['sysctl']['kernel_secure_sysrq']
|
||||||
|
extras['kernel_sysrq'] = sysrq_val
|
||||||
|
|
||||||
|
if settings['security']['kernel_enable_core_dump']:
|
||||||
|
extras['fs_suid_dumpable'] = 1
|
||||||
|
|
||||||
|
settings.update(extras)
|
||||||
|
for d in (SYSCTL_DEFAULTS % settings).split():
|
||||||
|
d = d.strip().partition('=')
|
||||||
|
key = d[0].strip()
|
||||||
|
path = os.path.join('/proc/sys', key.replace('.', '/'))
|
||||||
|
if not os.path.exists(path):
|
||||||
|
log("Skipping '%s' since '%s' does not exist" % (key, path),
|
||||||
|
level=WARNING)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ctxt['sysctl'][key] = d[2] or None
|
||||||
|
|
||||||
|
# Translate for python3
|
||||||
|
return {'sysctl_settings':
|
||||||
|
[(k, v) for k, v in six.iteritems(ctxt['sysctl'])]}
|
||||||
|
|
||||||
|
|
||||||
|
class SysctlConf(TemplatedFile):
|
||||||
|
"""An audit check for sysctl settings."""
|
||||||
|
def __init__(self):
|
||||||
|
self.conffile = '/etc/sysctl.d/99-juju-hardening.conf'
|
||||||
|
super(SysctlConf, self).__init__(self.conffile,
|
||||||
|
SysCtlHardeningContext(),
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
user='root', group='root',
|
||||||
|
mode=0o0440)
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
try:
|
||||||
|
subprocess.check_call(['sysctl', '-p', self.conffile])
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# NOTE: on some systems if sysctl cannot apply all settings it
|
||||||
|
# will return non-zero as well.
|
||||||
|
log("sysctl command returned an error (maybe some "
|
||||||
|
"keys could not be set) - %s" % (e),
|
||||||
|
level=WARNING)
|
@ -0,0 +1,8 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
{% if disable_core_dump -%}
|
||||||
|
# Prevent core dumps for all users. These are usually only needed by developers and may contain sensitive information.
|
||||||
|
* hard core 0
|
||||||
|
{% endif %}
|
@ -0,0 +1,7 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
{% for key, value in sysctl_settings -%}
|
||||||
|
{{ key }}={{ value }}
|
||||||
|
{% endfor -%}
|
349
charmhelpers/contrib/hardening/host/templates/login.defs
Normal file
349
charmhelpers/contrib/hardening/host/templates/login.defs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
#
|
||||||
|
# /etc/login.defs - Configuration control definitions for the login package.
|
||||||
|
#
|
||||||
|
# Three items must be defined: MAIL_DIR, ENV_SUPATH, and ENV_PATH.
|
||||||
|
# If unspecified, some arbitrary (and possibly incorrect) value will
|
||||||
|
# be assumed. All other items are optional - if not specified then
|
||||||
|
# the described action or option will be inhibited.
|
||||||
|
#
|
||||||
|
# Comment lines (lines beginning with "#") and blank lines are ignored.
|
||||||
|
#
|
||||||
|
# Modified for Linux. --marekm
|
||||||
|
|
||||||
|
# REQUIRED for useradd/userdel/usermod
|
||||||
|
# Directory where mailboxes reside, _or_ name of file, relative to the
|
||||||
|
# home directory. If you _do_ define MAIL_DIR and MAIL_FILE,
|
||||||
|
# MAIL_DIR takes precedence.
|
||||||
|
#
|
||||||
|
# Essentially:
|
||||||
|
# - MAIL_DIR defines the location of users mail spool files
|
||||||
|
# (for mbox use) by appending the username to MAIL_DIR as defined
|
||||||
|
# below.
|
||||||
|
# - MAIL_FILE defines the location of the users mail spool files as the
|
||||||
|
# fully-qualified filename obtained by prepending the user home
|
||||||
|
# directory before $MAIL_FILE
|
||||||
|
#
|
||||||
|
# NOTE: This is no more used for setting up users MAIL environment variable
|
||||||
|
# which is, starting from shadow 4.0.12-1 in Debian, entirely the
|
||||||
|
# job of the pam_mail PAM modules
|
||||||
|
# See default PAM configuration files provided for
|
||||||
|
# login, su, etc.
|
||||||
|
#
|
||||||
|
# This is a temporary situation: setting these variables will soon
|
||||||
|
# move to /etc/default/useradd and the variables will then be
|
||||||
|
# no more supported
|
||||||
|
MAIL_DIR /var/mail
|
||||||
|
#MAIL_FILE .mail
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable logging and display of /var/log/faillog login failure info.
|
||||||
|
# This option conflicts with the pam_tally PAM module.
|
||||||
|
#
|
||||||
|
FAILLOG_ENAB yes
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable display of unknown usernames when login failures are recorded.
|
||||||
|
#
|
||||||
|
# WARNING: Unknown usernames may become world readable.
|
||||||
|
# See #290803 and #298773 for details about how this could become a security
|
||||||
|
# concern
|
||||||
|
LOG_UNKFAIL_ENAB no
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable logging of successful logins
|
||||||
|
#
|
||||||
|
LOG_OK_LOGINS yes
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable "syslog" logging of su activity - in addition to sulog file logging.
|
||||||
|
# SYSLOG_SG_ENAB does the same for newgrp and sg.
|
||||||
|
#
|
||||||
|
SYSLOG_SU_ENAB yes
|
||||||
|
SYSLOG_SG_ENAB yes
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, all su activity is logged to this file.
|
||||||
|
#
|
||||||
|
#SULOG_FILE /var/log/sulog
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, file which maps tty line to TERM environment parameter.
|
||||||
|
# Each line of the file is in a format something like "vt100 tty01".
|
||||||
|
#
|
||||||
|
#TTYTYPE_FILE /etc/ttytype
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, login failures will be logged here in a utmp format
|
||||||
|
# last, when invoked as lastb, will read /var/log/btmp, so...
|
||||||
|
#
|
||||||
|
FTMP_FILE /var/log/btmp
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, the command name to display when running "su -". For
|
||||||
|
# example, if this is defined as "su" then a "ps" will display the
|
||||||
|
# command is "-su". If not defined, then "ps" would display the
|
||||||
|
# name of the shell actually being run, e.g. something like "-sh".
|
||||||
|
#
|
||||||
|
SU_NAME su
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, file which inhibits all the usual chatter during the login
|
||||||
|
# sequence. If a full pathname, then hushed mode will be enabled if the
|
||||||
|
# user's name or shell are found in the file. If not a full pathname, then
|
||||||
|
# hushed mode will be enabled if the file exists in the user's home directory.
|
||||||
|
#
|
||||||
|
HUSHLOGIN_FILE .hushlogin
|
||||||
|
#HUSHLOGIN_FILE /etc/hushlogins
|
||||||
|
|
||||||
|
#
|
||||||
|
# *REQUIRED* The default PATH settings, for superuser and normal users.
|
||||||
|
#
|
||||||
|
# (they are minimal, add the rest in the shell startup files)
|
||||||
|
ENV_SUPATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
ENV_PATH PATH=/usr/local/bin:/usr/bin:/bin{% if additional_user_paths %}{{ additional_user_paths }}{% endif %}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Terminal permissions
|
||||||
|
#
|
||||||
|
# TTYGROUP Login tty will be assigned this group ownership.
|
||||||
|
# TTYPERM Login tty will be set to this permission.
|
||||||
|
#
|
||||||
|
# If you have a "write" program which is "setgid" to a special group
|
||||||
|
# which owns the terminals, define TTYGROUP to the group number and
|
||||||
|
# TTYPERM to 0620. Otherwise leave TTYGROUP commented out and assign
|
||||||
|
# TTYPERM to either 622 or 600.
|
||||||
|
#
|
||||||
|
# In Debian /usr/bin/bsd-write or similar programs are setgid tty
|
||||||
|
# However, the default and recommended value for TTYPERM is still 0600
|
||||||
|
# to not allow anyone to write to anyone else console or terminal
|
||||||
|
|
||||||
|
# Users can still allow other people to write them by issuing
|
||||||
|
# the "mesg y" command.
|
||||||
|
|
||||||
|
TTYGROUP tty
|
||||||
|
TTYPERM 0600
|
||||||
|
|
||||||
|
#
|
||||||
|
# Login configuration initializations:
|
||||||
|
#
|
||||||
|
# ERASECHAR Terminal ERASE character ('\010' = backspace).
|
||||||
|
# KILLCHAR Terminal KILL character ('\025' = CTRL/U).
|
||||||
|
# UMASK Default "umask" value.
|
||||||
|
#
|
||||||
|
# The ERASECHAR and KILLCHAR are used only on System V machines.
|
||||||
|
#
|
||||||
|
# UMASK is the default umask value for pam_umask and is used by
|
||||||
|
# useradd and newusers to set the mode of the new home directories.
|
||||||
|
# 022 is the "historical" value in Debian for UMASK
|
||||||
|
# 027, or even 077, could be considered better for privacy
|
||||||
|
# There is no One True Answer here : each sysadmin must make up his/her
|
||||||
|
# mind.
|
||||||
|
#
|
||||||
|
# If USERGROUPS_ENAB is set to "yes", that will modify this UMASK default value
|
||||||
|
# for private user groups, i. e. the uid is the same as gid, and username is
|
||||||
|
# the same as the primary group name: for these, the user permissions will be
|
||||||
|
# used as group permissions, e. g. 022 will become 002.
|
||||||
|
#
|
||||||
|
# Prefix these values with "0" to get octal, "0x" to get hexadecimal.
|
||||||
|
#
|
||||||
|
ERASECHAR 0177
|
||||||
|
KILLCHAR 025
|
||||||
|
UMASK {{ umask }}
|
||||||
|
|
||||||
|
# Enable setting of the umask group bits to be the same as owner bits (examples: `022` -> `002`, `077` -> `007`) for non-root users, if the uid is the same as gid, and username is the same as the primary group name.
|
||||||
|
# If set to yes, userdel will remove the user´s group if it contains no more members, and useradd will create by default a group with the name of the user.
|
||||||
|
USERGROUPS_ENAB yes
|
||||||
|
|
||||||
|
#
|
||||||
|
# Password aging controls:
|
||||||
|
#
|
||||||
|
# PASS_MAX_DAYS Maximum number of days a password may be used.
|
||||||
|
# PASS_MIN_DAYS Minimum number of days allowed between password changes.
|
||||||
|
# PASS_WARN_AGE Number of days warning given before a password expires.
|
||||||
|
#
|
||||||
|
PASS_MAX_DAYS {{ pwd_max_age }}
|
||||||
|
PASS_MIN_DAYS {{ pwd_min_age }}
|
||||||
|
PASS_WARN_AGE 7
|
||||||
|
|
||||||
|
#
|
||||||
|
# Min/max values for automatic uid selection in useradd
|
||||||
|
#
|
||||||
|
UID_MIN {{ uid_min }}
|
||||||
|
UID_MAX 60000
|
||||||
|
# System accounts
|
||||||
|
SYS_UID_MIN {{ sys_uid_min }}
|
||||||
|
SYS_UID_MAX {{ sys_uid_max }}
|
||||||
|
|
||||||
|
# Min/max values for automatic gid selection in groupadd
|
||||||
|
GID_MIN {{ gid_min }}
|
||||||
|
GID_MAX 60000
|
||||||
|
# System accounts
|
||||||
|
SYS_GID_MIN {{ sys_gid_min }}
|
||||||
|
SYS_GID_MAX {{ sys_gid_max }}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Max number of login retries if password is bad. This will most likely be
|
||||||
|
# overriden by PAM, since the default pam_unix module has it's own built
|
||||||
|
# in of 3 retries. However, this is a safe fallback in case you are using
|
||||||
|
# an authentication module that does not enforce PAM_MAXTRIES.
|
||||||
|
#
|
||||||
|
LOGIN_RETRIES {{ login_retries }}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Max time in seconds for login
|
||||||
|
#
|
||||||
|
LOGIN_TIMEOUT {{ login_timeout }}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Which fields may be changed by regular users using chfn - use
|
||||||
|
# any combination of letters "frwh" (full name, room number, work
|
||||||
|
# phone, home phone). If not defined, no changes are allowed.
|
||||||
|
# For backward compatibility, "yes" = "rwh" and "no" = "frwh".
|
||||||
|
#
|
||||||
|
{% if chfn_restrict %}
|
||||||
|
CHFN_RESTRICT {{ chfn_restrict }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Should login be allowed if we can't cd to the home directory?
|
||||||
|
# Default in no.
|
||||||
|
#
|
||||||
|
DEFAULT_HOME {% if allow_login_without_home %} yes {% else %} no {% endif %}
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, this command is run when removing a user.
|
||||||
|
# It should remove any at/cron/print jobs etc. owned by
|
||||||
|
# the user to be removed (passed as the first argument).
|
||||||
|
#
|
||||||
|
#USERDEL_CMD /usr/sbin/userdel_local
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable setting of the umask group bits to be the same as owner bits
|
||||||
|
# (examples: 022 -> 002, 077 -> 007) for non-root users, if the uid is
|
||||||
|
# the same as gid, and username is the same as the primary group name.
|
||||||
|
#
|
||||||
|
# If set to yes, userdel will remove the user´s group if it contains no
|
||||||
|
# more members, and useradd will create by default a group with the name
|
||||||
|
# of the user.
|
||||||
|
#
|
||||||
|
USERGROUPS_ENAB yes
|
||||||
|
|
||||||
|
#
|
||||||
|
# Instead of the real user shell, the program specified by this parameter
|
||||||
|
# will be launched, although its visible name (argv[0]) will be the shell's.
|
||||||
|
# The program may do whatever it wants (logging, additional authentification,
|
||||||
|
# banner, ...) before running the actual shell.
|
||||||
|
#
|
||||||
|
# FAKE_SHELL /bin/fakeshell
|
||||||
|
|
||||||
|
#
|
||||||
|
# If defined, either full pathname of a file containing device names or
|
||||||
|
# a ":" delimited list of device names. Root logins will be allowed only
|
||||||
|
# upon these devices.
|
||||||
|
#
|
||||||
|
# This variable is used by login and su.
|
||||||
|
#
|
||||||
|
#CONSOLE /etc/consoles
|
||||||
|
#CONSOLE console:tty01:tty02:tty03:tty04
|
||||||
|
|
||||||
|
#
|
||||||
|
# List of groups to add to the user's supplementary group set
|
||||||
|
# when logging in on the console (as determined by the CONSOLE
|
||||||
|
# setting). Default is none.
|
||||||
|
#
|
||||||
|
# Use with caution - it is possible for users to gain permanent
|
||||||
|
# access to these groups, even when not logged in on the console.
|
||||||
|
# How to do it is left as an exercise for the reader...
|
||||||
|
#
|
||||||
|
# This variable is used by login and su.
|
||||||
|
#
|
||||||
|
#CONSOLE_GROUPS floppy:audio:cdrom
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set to "yes", new passwords will be encrypted using the MD5-based
|
||||||
|
# algorithm compatible with the one used by recent releases of FreeBSD.
|
||||||
|
# It supports passwords of unlimited length and longer salt strings.
|
||||||
|
# Set to "no" if you need to copy encrypted passwords to other systems
|
||||||
|
# which don't understand the new algorithm. Default is "no".
|
||||||
|
#
|
||||||
|
# This variable is deprecated. You should use ENCRYPT_METHOD.
|
||||||
|
#
|
||||||
|
MD5_CRYPT_ENAB no
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set to MD5 , MD5-based algorithm will be used for encrypting password
|
||||||
|
# If set to SHA256, SHA256-based algorithm will be used for encrypting password
|
||||||
|
# If set to SHA512, SHA512-based algorithm will be used for encrypting password
|
||||||
|
# If set to DES, DES-based algorithm will be used for encrypting password (default)
|
||||||
|
# Overrides the MD5_CRYPT_ENAB option
|
||||||
|
#
|
||||||
|
# Note: It is recommended to use a value consistent with
|
||||||
|
# the PAM modules configuration.
|
||||||
|
#
|
||||||
|
ENCRYPT_METHOD SHA512
|
||||||
|
|
||||||
|
#
|
||||||
|
# Only used if ENCRYPT_METHOD is set to SHA256 or SHA512.
|
||||||
|
#
|
||||||
|
# Define the number of SHA rounds.
|
||||||
|
# With a lot of rounds, it is more difficult to brute forcing the password.
|
||||||
|
# But note also that it more CPU resources will be needed to authenticate
|
||||||
|
# users.
|
||||||
|
#
|
||||||
|
# If not specified, the libc will choose the default number of rounds (5000).
|
||||||
|
# The values must be inside the 1000-999999999 range.
|
||||||
|
# If only one of the MIN or MAX values is set, then this value will be used.
|
||||||
|
# If MIN > MAX, the highest value will be used.
|
||||||
|
#
|
||||||
|
# SHA_CRYPT_MIN_ROUNDS 5000
|
||||||
|
# SHA_CRYPT_MAX_ROUNDS 5000
|
||||||
|
|
||||||
|
################# OBSOLETED BY PAM ##############
|
||||||
|
# #
|
||||||
|
# These options are now handled by PAM. Please #
|
||||||
|
# edit the appropriate file in /etc/pam.d/ to #
|
||||||
|
# enable the equivelants of them.
|
||||||
|
#
|
||||||
|
###############
|
||||||
|
|
||||||
|
#MOTD_FILE
|
||||||
|
#DIALUPS_CHECK_ENAB
|
||||||
|
#LASTLOG_ENAB
|
||||||
|
#MAIL_CHECK_ENAB
|
||||||
|
#OBSCURE_CHECKS_ENAB
|
||||||
|
#PORTTIME_CHECKS_ENAB
|
||||||
|
#SU_WHEEL_ONLY
|
||||||
|
#CRACKLIB_DICTPATH
|
||||||
|
#PASS_CHANGE_TRIES
|
||||||
|
#PASS_ALWAYS_WARN
|
||||||
|
#ENVIRON_FILE
|
||||||
|
#NOLOGINS_FILE
|
||||||
|
#ISSUE_FILE
|
||||||
|
#PASS_MIN_LEN
|
||||||
|
#PASS_MAX_LEN
|
||||||
|
#ULIMIT
|
||||||
|
#ENV_HZ
|
||||||
|
#CHFN_AUTH
|
||||||
|
#CHSH_AUTH
|
||||||
|
#FAIL_DELAY
|
||||||
|
|
||||||
|
################# OBSOLETED #######################
|
||||||
|
# #
|
||||||
|
# These options are no more handled by shadow. #
|
||||||
|
# #
|
||||||
|
# Shadow utilities will display a warning if they #
|
||||||
|
# still appear. #
|
||||||
|
# #
|
||||||
|
###################################################
|
||||||
|
|
||||||
|
# CLOSE_SESSIONS
|
||||||
|
# LOGIN_STRING
|
||||||
|
# NO_PASSWORD_CONSOLE
|
||||||
|
# QMAIL_DIR
|
||||||
|
|
||||||
|
|
||||||
|
|
117
charmhelpers/contrib/hardening/host/templates/modules
Normal file
117
charmhelpers/contrib/hardening/host/templates/modules
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
# /etc/modules: kernel modules to load at boot time.
|
||||||
|
#
|
||||||
|
# This file contains the names of kernel modules that should be loaded
|
||||||
|
# at boot time, one per line. Lines beginning with "#" are ignored.
|
||||||
|
# Parameters can be specified after the module name.
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
# ----
|
||||||
|
#
|
||||||
|
# Modules for certains builds, contains support modules and some CPU-specific optimizations.
|
||||||
|
|
||||||
|
{% if arch == "x86_64" -%}
|
||||||
|
# Optimize for x86_64 cryptographic features
|
||||||
|
twofish-x86_64-3way
|
||||||
|
twofish-x86_64
|
||||||
|
aes-x86_64
|
||||||
|
salsa20-x86_64
|
||||||
|
blowfish-x86_64
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% if cpuVendor == "intel" -%}
|
||||||
|
# Intel-specific optimizations
|
||||||
|
ghash-clmulni-intel
|
||||||
|
aesni-intel
|
||||||
|
kvm-intel
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% if cpuVendor == "amd" -%}
|
||||||
|
# AMD-specific optimizations
|
||||||
|
kvm-amd
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
kvm
|
||||||
|
|
||||||
|
|
||||||
|
# Crypto
|
||||||
|
# ------
|
||||||
|
|
||||||
|
# Some core modules which comprise strong cryptography.
|
||||||
|
blowfish_common
|
||||||
|
blowfish_generic
|
||||||
|
ctr
|
||||||
|
cts
|
||||||
|
lrw
|
||||||
|
lzo
|
||||||
|
rmd160
|
||||||
|
rmd256
|
||||||
|
rmd320
|
||||||
|
serpent
|
||||||
|
sha512_generic
|
||||||
|
twofish_common
|
||||||
|
twofish_generic
|
||||||
|
xts
|
||||||
|
zlib
|
||||||
|
|
||||||
|
|
||||||
|
# Drivers
|
||||||
|
# -------
|
||||||
|
|
||||||
|
# Basics
|
||||||
|
lp
|
||||||
|
rtc
|
||||||
|
loop
|
||||||
|
|
||||||
|
# Filesystems
|
||||||
|
ext2
|
||||||
|
btrfs
|
||||||
|
|
||||||
|
{% if desktop_enable -%}
|
||||||
|
# Desktop
|
||||||
|
psmouse
|
||||||
|
snd
|
||||||
|
snd_ac97_codec
|
||||||
|
snd_intel8x0
|
||||||
|
snd_page_alloc
|
||||||
|
snd_pcm
|
||||||
|
snd_timer
|
||||||
|
soundcore
|
||||||
|
usbhid
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# Lib
|
||||||
|
# ---
|
||||||
|
xz
|
||||||
|
|
||||||
|
|
||||||
|
# Net
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# All packets needed for netfilter rules (ie iptables, ebtables).
|
||||||
|
ip_tables
|
||||||
|
x_tables
|
||||||
|
iptable_filter
|
||||||
|
iptable_nat
|
||||||
|
|
||||||
|
# Targets
|
||||||
|
ipt_LOG
|
||||||
|
ipt_REJECT
|
||||||
|
|
||||||
|
# Modules
|
||||||
|
xt_connlimit
|
||||||
|
xt_tcpudp
|
||||||
|
xt_recent
|
||||||
|
xt_limit
|
||||||
|
xt_conntrack
|
||||||
|
nf_conntrack
|
||||||
|
nf_conntrack_ipv4
|
||||||
|
nf_defrag_ipv4
|
||||||
|
xt_state
|
||||||
|
nf_nat
|
||||||
|
|
||||||
|
# Addons
|
||||||
|
xt_pknock
|
11
charmhelpers/contrib/hardening/host/templates/passwdqc.conf
Normal file
11
charmhelpers/contrib/hardening/host/templates/passwdqc.conf
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
Name: passwdqc password strength enforcement
|
||||||
|
Default: yes
|
||||||
|
Priority: 1024
|
||||||
|
Conflicts: cracklib
|
||||||
|
Password-Type: Primary
|
||||||
|
Password:
|
||||||
|
requisite pam_passwdqc.so {{ auth_pam_passwdqc_options }}
|
@ -0,0 +1,8 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
# Disable core dumps via soft limits for all users. Compliance to this setting
|
||||||
|
# is voluntary and can be modified by users up to a hard limit. This setting is
|
||||||
|
# a sane default.
|
||||||
|
ulimit -S -c 0 > /dev/null 2>&1
|
11
charmhelpers/contrib/hardening/host/templates/securetty
Normal file
11
charmhelpers/contrib/hardening/host/templates/securetty
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
# A list of TTYs, from which root can log in
|
||||||
|
# see `man securetty` for reference
|
||||||
|
{% if ttys -%}
|
||||||
|
{% for tty in ttys -%}
|
||||||
|
{{ tty }}
|
||||||
|
{% endfor -%}
|
||||||
|
{% endif -%}
|
14
charmhelpers/contrib/hardening/host/templates/tally2
Normal file
14
charmhelpers/contrib/hardening/host/templates/tally2
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
Name: tally2 lockout after failed attempts enforcement
|
||||||
|
Default: yes
|
||||||
|
Priority: 1024
|
||||||
|
Conflicts: cracklib
|
||||||
|
Auth-Type: Primary
|
||||||
|
Auth-Initial:
|
||||||
|
required pam_tally2.so deny={{ auth_retries }} onerr=fail unlock_time={{ auth_lockout_time }}
|
||||||
|
Account-Type: Primary
|
||||||
|
Account-Initial:
|
||||||
|
required pam_tally2.so
|
19
charmhelpers/contrib/hardening/mysql/__init__.py
Normal file
19
charmhelpers/contrib/hardening/mysql/__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')
|
31
charmhelpers/contrib/hardening/mysql/checks/__init__.py
Normal file
31
charmhelpers/contrib/hardening/mysql/checks/__init__.py
Normal file
@ -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.mysql.checks import config
|
||||||
|
|
||||||
|
|
||||||
|
def run_mysql_checks():
|
||||||
|
log("Starting MySQL 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("MySQL hardening checks complete.", level=DEBUG)
|
89
charmhelpers/contrib/hardening/mysql/checks/config.py
Normal file
89
charmhelpers/contrib/hardening/mysql/checks/config.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# 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
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
WARNING,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import (
|
||||||
|
FilePermissionAudit,
|
||||||
|
DirectoryPermissionAudit,
|
||||||
|
TemplatedFile,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.mysql import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get MySQL hardening config audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
if subprocess.call(['which', 'mysql'], stdout=subprocess.PIPE) != 0:
|
||||||
|
log("MySQL does not appear to be installed on this node - "
|
||||||
|
"skipping mysql hardening", level=WARNING)
|
||||||
|
return []
|
||||||
|
|
||||||
|
settings = utils.get_settings('mysql')
|
||||||
|
hardening_settings = settings['hardening']
|
||||||
|
my_cnf = hardening_settings['mysql-conf']
|
||||||
|
|
||||||
|
audits = [
|
||||||
|
FilePermissionAudit(paths=[my_cnf], user='root',
|
||||||
|
group='root', mode=0o0600),
|
||||||
|
|
||||||
|
TemplatedFile(hardening_settings['hardening-conf'],
|
||||||
|
MySQLConfContext(),
|
||||||
|
TEMPLATES_DIR,
|
||||||
|
mode=0o0750,
|
||||||
|
user='mysql',
|
||||||
|
group='root',
|
||||||
|
service_actions=[{'service': 'mysql',
|
||||||
|
'actions': ['restart']}]),
|
||||||
|
|
||||||
|
# MySQL and Percona charms do not allow configuration of the
|
||||||
|
# data directory, so use the default.
|
||||||
|
DirectoryPermissionAudit('/var/lib/mysql',
|
||||||
|
user='mysql',
|
||||||
|
group='mysql',
|
||||||
|
recursive=False,
|
||||||
|
mode=0o755),
|
||||||
|
|
||||||
|
DirectoryPermissionAudit('/etc/mysql',
|
||||||
|
user='root',
|
||||||
|
group='root',
|
||||||
|
recursive=False,
|
||||||
|
mode=0o700),
|
||||||
|
]
|
||||||
|
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLConfContext(object):
|
||||||
|
"""Defines the set of key/value pairs to set in a mysql config file.
|
||||||
|
|
||||||
|
This context, when called, will return a dictionary containing the
|
||||||
|
key/value pairs of setting to specify in the
|
||||||
|
/etc/mysql/conf.d/hardening.cnf file.
|
||||||
|
"""
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('mysql')
|
||||||
|
# Translate for python3
|
||||||
|
return {'mysql_settings':
|
||||||
|
[(k, v) for k, v in six.iteritems(settings['security'])]}
|
12
charmhelpers/contrib/hardening/mysql/templates/hardening.cnf
Normal file
12
charmhelpers/contrib/hardening/mysql/templates/hardening.cnf
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
[mysqld]
|
||||||
|
{% for setting, value in mysql_settings -%}
|
||||||
|
{% if value == 'True' -%}
|
||||||
|
{{ setting }}
|
||||||
|
{% elif value != 'None' and value != None -%}
|
||||||
|
{{ setting }} = {{ value }}
|
||||||
|
{% endif -%}
|
||||||
|
{% endfor -%}
|
19
charmhelpers/contrib/hardening/ssh/__init__.py
Normal file
19
charmhelpers/contrib/hardening/ssh/__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')
|
31
charmhelpers/contrib/hardening/ssh/checks/__init__.py
Normal file
31
charmhelpers/contrib/hardening/ssh/checks/__init__.py
Normal file
@ -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.ssh.checks import config
|
||||||
|
|
||||||
|
|
||||||
|
def run_ssh_checks():
|
||||||
|
log("Starting SSH 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("SSH hardening checks complete.", level=DEBUG)
|
394
charmhelpers/contrib/hardening/ssh/checks/config.py
Normal file
394
charmhelpers/contrib/hardening/ssh/checks/config.py
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
DEBUG,
|
||||||
|
)
|
||||||
|
from charmhelpers.fetch import (
|
||||||
|
apt_install,
|
||||||
|
apt_update,
|
||||||
|
)
|
||||||
|
from charmhelpers.core.host import lsb_release
|
||||||
|
from charmhelpers.contrib.hardening.audits.file import (
|
||||||
|
TemplatedFile,
|
||||||
|
FileContentAudit,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hardening.ssh import TEMPLATES_DIR
|
||||||
|
from charmhelpers.contrib.hardening import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_audits():
|
||||||
|
"""Get SSH hardening config audits.
|
||||||
|
|
||||||
|
:returns: dictionary of audits
|
||||||
|
"""
|
||||||
|
audits = [SSHConfig(), SSHDConfig(), SSHConfigFileContentAudit(),
|
||||||
|
SSHDConfigFileContentAudit()]
|
||||||
|
return audits
|
||||||
|
|
||||||
|
|
||||||
|
class SSHConfigContext(object):
|
||||||
|
|
||||||
|
type = 'client'
|
||||||
|
|
||||||
|
def get_macs(self, allow_weak_mac):
|
||||||
|
if allow_weak_mac:
|
||||||
|
weak_macs = 'weak'
|
||||||
|
else:
|
||||||
|
weak_macs = 'default'
|
||||||
|
|
||||||
|
default = 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160'
|
||||||
|
macs = {'default': default,
|
||||||
|
'weak': default + ',hmac-sha1'}
|
||||||
|
|
||||||
|
default = ('hmac-sha2-512-etm@openssh.com,'
|
||||||
|
'hmac-sha2-256-etm@openssh.com,'
|
||||||
|
'hmac-ripemd160-etm@openssh.com,umac-128-etm@openssh.com,'
|
||||||
|
'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160')
|
||||||
|
macs_66 = {'default': default,
|
||||||
|
'weak': default + ',hmac-sha1'}
|
||||||
|
|
||||||
|
# Use newer ciphers on Ubuntu Trusty and above
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
|
||||||
|
log("Detected Ubuntu 14.04 or newer, using new macs", level=DEBUG)
|
||||||
|
macs = macs_66
|
||||||
|
|
||||||
|
return macs[weak_macs]
|
||||||
|
|
||||||
|
def get_kexs(self, allow_weak_kex):
|
||||||
|
if allow_weak_kex:
|
||||||
|
weak_kex = 'weak'
|
||||||
|
else:
|
||||||
|
weak_kex = 'default'
|
||||||
|
|
||||||
|
default = 'diffie-hellman-group-exchange-sha256'
|
||||||
|
weak = (default + ',diffie-hellman-group14-sha1,'
|
||||||
|
'diffie-hellman-group-exchange-sha1,'
|
||||||
|
'diffie-hellman-group1-sha1')
|
||||||
|
kex = {'default': default,
|
||||||
|
'weak': weak}
|
||||||
|
|
||||||
|
default = ('curve25519-sha256@libssh.org,'
|
||||||
|
'diffie-hellman-group-exchange-sha256')
|
||||||
|
weak = (default + ',diffie-hellman-group14-sha1,'
|
||||||
|
'diffie-hellman-group-exchange-sha1,'
|
||||||
|
'diffie-hellman-group1-sha1')
|
||||||
|
kex_66 = {'default': default,
|
||||||
|
'weak': weak}
|
||||||
|
|
||||||
|
# Use newer kex on Ubuntu Trusty and above
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
|
||||||
|
log('Detected Ubuntu 14.04 or newer, using new key exchange '
|
||||||
|
'algorithms', level=DEBUG)
|
||||||
|
kex = kex_66
|
||||||
|
|
||||||
|
return kex[weak_kex]
|
||||||
|
|
||||||
|
def get_ciphers(self, cbc_required):
|
||||||
|
if cbc_required:
|
||||||
|
weak_ciphers = 'weak'
|
||||||
|
else:
|
||||||
|
weak_ciphers = 'default'
|
||||||
|
|
||||||
|
default = 'aes256-ctr,aes192-ctr,aes128-ctr'
|
||||||
|
cipher = {'default': default,
|
||||||
|
'weak': default + 'aes256-cbc,aes192-cbc,aes128-cbc'}
|
||||||
|
|
||||||
|
default = ('chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,'
|
||||||
|
'aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr')
|
||||||
|
ciphers_66 = {'default': default,
|
||||||
|
'weak': default + ',aes256-cbc,aes192-cbc,aes128-cbc'}
|
||||||
|
|
||||||
|
# Use newer ciphers on ubuntu Trusty and above
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
|
||||||
|
log('Detected Ubuntu 14.04 or newer, using new ciphers',
|
||||||
|
level=DEBUG)
|
||||||
|
cipher = ciphers_66
|
||||||
|
|
||||||
|
return cipher[weak_ciphers]
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
if settings['common']['network_ipv6_enable']:
|
||||||
|
addr_family = 'any'
|
||||||
|
else:
|
||||||
|
addr_family = 'inet'
|
||||||
|
|
||||||
|
ctxt = {
|
||||||
|
'addr_family': addr_family,
|
||||||
|
'remote_hosts': settings['common']['remote_hosts'],
|
||||||
|
'password_auth_allowed':
|
||||||
|
settings['client']['password_authentication'],
|
||||||
|
'ports': settings['common']['ports'],
|
||||||
|
'ciphers': self.get_ciphers(settings['client']['cbc_required']),
|
||||||
|
'macs': self.get_macs(settings['client']['weak_hmac']),
|
||||||
|
'kexs': self.get_kexs(settings['client']['weak_kex']),
|
||||||
|
'roaming': settings['client']['roaming'],
|
||||||
|
}
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class SSHConfig(TemplatedFile):
|
||||||
|
def __init__(self):
|
||||||
|
path = '/etc/ssh/ssh_config'
|
||||||
|
super(SSHConfig, self).__init__(path=path,
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
context=SSHConfigContext(),
|
||||||
|
user='root',
|
||||||
|
group='root',
|
||||||
|
mode=0o0644)
|
||||||
|
|
||||||
|
def pre_write(self):
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
apt_update(fatal=True)
|
||||||
|
apt_install(settings['client']['package'])
|
||||||
|
if not os.path.exists('/etc/ssh'):
|
||||||
|
os.makedir('/etc/ssh')
|
||||||
|
# NOTE: don't recurse
|
||||||
|
utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
|
||||||
|
maxdepth=0)
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
# NOTE: don't recurse
|
||||||
|
utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
|
||||||
|
maxdepth=0)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHDConfigContext(SSHConfigContext):
|
||||||
|
|
||||||
|
type = 'server'
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
if settings['common']['network_ipv6_enable']:
|
||||||
|
addr_family = 'any'
|
||||||
|
else:
|
||||||
|
addr_family = 'inet'
|
||||||
|
|
||||||
|
ctxt = {
|
||||||
|
'ssh_ip': settings['server']['listen_to'],
|
||||||
|
'password_auth_allowed':
|
||||||
|
settings['server']['password_authentication'],
|
||||||
|
'ports': settings['common']['ports'],
|
||||||
|
'addr_family': addr_family,
|
||||||
|
'ciphers': self.get_ciphers(settings['server']['cbc_required']),
|
||||||
|
'macs': self.get_macs(settings['server']['weak_hmac']),
|
||||||
|
'kexs': self.get_kexs(settings['server']['weak_kex']),
|
||||||
|
'host_key_files': settings['server']['host_key_files'],
|
||||||
|
'allow_root_with_key': settings['server']['allow_root_with_key'],
|
||||||
|
'password_authentication':
|
||||||
|
settings['server']['password_authentication'],
|
||||||
|
'use_priv_sep': settings['server']['use_privilege_separation'],
|
||||||
|
'use_pam': settings['server']['use_pam'],
|
||||||
|
'allow_x11_forwarding': settings['server']['allow_x11_forwarding'],
|
||||||
|
'print_motd': settings['server']['print_motd'],
|
||||||
|
'print_last_log': settings['server']['print_last_log'],
|
||||||
|
'client_alive_interval':
|
||||||
|
settings['server']['alive_interval'],
|
||||||
|
'client_alive_count': settings['server']['alive_count'],
|
||||||
|
'allow_tcp_forwarding': settings['server']['allow_tcp_forwarding'],
|
||||||
|
'allow_agent_forwarding':
|
||||||
|
settings['server']['allow_agent_forwarding'],
|
||||||
|
'deny_users': settings['server']['deny_users'],
|
||||||
|
'allow_users': settings['server']['allow_users'],
|
||||||
|
'deny_groups': settings['server']['deny_groups'],
|
||||||
|
'allow_groups': settings['server']['allow_groups'],
|
||||||
|
'use_dns': settings['server']['use_dns'],
|
||||||
|
'sftp_enable': settings['server']['sftp_enable'],
|
||||||
|
'sftp_group': settings['server']['sftp_group'],
|
||||||
|
'sftp_chroot': settings['server']['sftp_chroot'],
|
||||||
|
'max_auth_tries': settings['server']['max_auth_tries'],
|
||||||
|
'max_sessions': settings['server']['max_sessions'],
|
||||||
|
}
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class SSHDConfig(TemplatedFile):
|
||||||
|
def __init__(self):
|
||||||
|
path = '/etc/ssh/sshd_config'
|
||||||
|
super(SSHDConfig, self).__init__(path=path,
|
||||||
|
template_dir=TEMPLATES_DIR,
|
||||||
|
context=SSHDConfigContext(),
|
||||||
|
user='root',
|
||||||
|
group='root',
|
||||||
|
mode=0o0600,
|
||||||
|
service_actions=[{'service': 'ssh',
|
||||||
|
'actions':
|
||||||
|
['restart']}])
|
||||||
|
|
||||||
|
def pre_write(self):
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
apt_update(fatal=True)
|
||||||
|
apt_install(settings['server']['package'])
|
||||||
|
if not os.path.exists('/etc/ssh'):
|
||||||
|
os.makedir('/etc/ssh')
|
||||||
|
# NOTE: don't recurse
|
||||||
|
utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
|
||||||
|
maxdepth=0)
|
||||||
|
|
||||||
|
def post_write(self):
|
||||||
|
# NOTE: don't recurse
|
||||||
|
utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
|
||||||
|
maxdepth=0)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHConfigFileContentAudit(FileContentAudit):
|
||||||
|
def __init__(self):
|
||||||
|
self.path = '/etc/ssh/ssh_config'
|
||||||
|
super(SSHConfigFileContentAudit, self).__init__(self.path, {})
|
||||||
|
|
||||||
|
def is_compliant(self, *args, **kwargs):
|
||||||
|
self.pass_cases = []
|
||||||
|
self.fail_cases = []
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
|
||||||
|
if not settings['server']['weak_hmac']:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-sha1$')
|
||||||
|
|
||||||
|
if settings['server']['weak_kex']:
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
|
||||||
|
if settings['server']['cbc_required']:
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
if not settings['client']['weak_hmac']:
|
||||||
|
self.fail_cases.append(r'^MACs.+,hmac-sha1$')
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-sha1$')
|
||||||
|
|
||||||
|
if settings['client']['weak_kex']:
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
|
||||||
|
if settings['client']['cbc_required']:
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
|
||||||
|
if settings['client']['roaming']:
|
||||||
|
self.pass_cases.append(r'^UseRoaming yes$')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^UseRoaming yes$')
|
||||||
|
|
||||||
|
return super(SSHConfigFileContentAudit, self).is_compliant(*args,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHDConfigFileContentAudit(FileContentAudit):
|
||||||
|
def __init__(self):
|
||||||
|
self.path = '/etc/ssh/sshd_config'
|
||||||
|
super(SSHDConfigFileContentAudit, self).__init__(self.path, {})
|
||||||
|
|
||||||
|
def is_compliant(self, *args, **kwargs):
|
||||||
|
self.pass_cases = []
|
||||||
|
self.fail_cases = []
|
||||||
|
settings = utils.get_settings('ssh')
|
||||||
|
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
|
||||||
|
if not settings['server']['weak_hmac']:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-sha1$')
|
||||||
|
|
||||||
|
if settings['server']['weak_kex']:
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
|
||||||
|
if settings['server']['cbc_required']:
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
if not settings['server']['weak_hmac']:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^MACs.+,hmac-sha1$')
|
||||||
|
|
||||||
|
if settings['server']['weak_kex']:
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
else:
|
||||||
|
self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
|
||||||
|
self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
|
||||||
|
|
||||||
|
if settings['server']['cbc_required']:
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
|
||||||
|
self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
|
||||||
|
|
||||||
|
if settings['server']['sftp_enable']:
|
||||||
|
self.pass_cases.append(r'^Subsystem\ssftp')
|
||||||
|
else:
|
||||||
|
self.fail_cases.append(r'^Subsystem\ssftp')
|
||||||
|
|
||||||
|
return super(SSHDConfigFileContentAudit, self).is_compliant(*args,
|
||||||
|
**kwargs)
|
70
charmhelpers/contrib/hardening/ssh/templates/ssh_config
Normal file
70
charmhelpers/contrib/hardening/ssh/templates/ssh_config
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
# This is the ssh client system-wide configuration file. See
|
||||||
|
# ssh_config(5) for more information. This file provides defaults for
|
||||||
|
# users, and the values can be changed in per-user configuration files
|
||||||
|
# or on the command line.
|
||||||
|
|
||||||
|
# Configuration data is parsed as follows:
|
||||||
|
# 1. command line options
|
||||||
|
# 2. user-specific file
|
||||||
|
# 3. system-wide file
|
||||||
|
# Any configuration value is only changed the first time it is set.
|
||||||
|
# Thus, host-specific definitions should be at the beginning of the
|
||||||
|
# configuration file, and defaults at the end.
|
||||||
|
|
||||||
|
# Site-wide defaults for some commonly used options. For a comprehensive
|
||||||
|
# list of available options, their meanings and defaults, please see the
|
||||||
|
# ssh_config(5) man page.
|
||||||
|
|
||||||
|
# Restrict the following configuration to be limited to this Host.
|
||||||
|
{% if remote_hosts -%}
|
||||||
|
Host {{ ' '.join(remote_hosts) }}
|
||||||
|
{% endif %}
|
||||||
|
ForwardAgent no
|
||||||
|
ForwardX11 no
|
||||||
|
ForwardX11Trusted yes
|
||||||
|
RhostsRSAAuthentication no
|
||||||
|
RSAAuthentication yes
|
||||||
|
PasswordAuthentication {{ password_auth_allowed }}
|
||||||
|
HostbasedAuthentication no
|
||||||
|
GSSAPIAuthentication no
|
||||||
|
GSSAPIDelegateCredentials no
|
||||||
|
GSSAPIKeyExchange no
|
||||||
|
GSSAPITrustDNS no
|
||||||
|
BatchMode no
|
||||||
|
CheckHostIP yes
|
||||||
|
AddressFamily {{ addr_family }}
|
||||||
|
ConnectTimeout 0
|
||||||
|
StrictHostKeyChecking ask
|
||||||
|
IdentityFile ~/.ssh/identity
|
||||||
|
IdentityFile ~/.ssh/id_rsa
|
||||||
|
IdentityFile ~/.ssh/id_dsa
|
||||||
|
# The port at the destination should be defined
|
||||||
|
{% for port in ports -%}
|
||||||
|
Port {{ port }}
|
||||||
|
{% endfor %}
|
||||||
|
Protocol 2
|
||||||
|
Cipher 3des
|
||||||
|
{% if ciphers -%}
|
||||||
|
Ciphers {{ ciphers }}
|
||||||
|
{%- endif %}
|
||||||
|
{% if macs -%}
|
||||||
|
MACs {{ macs }}
|
||||||
|
{%- endif %}
|
||||||
|
{% if kexs -%}
|
||||||
|
KexAlgorithms {{ kexs }}
|
||||||
|
{%- endif %}
|
||||||
|
EscapeChar ~
|
||||||
|
Tunnel no
|
||||||
|
TunnelDevice any:any
|
||||||
|
PermitLocalCommand no
|
||||||
|
VisualHostKey no
|
||||||
|
RekeyLimit 1G 1h
|
||||||
|
SendEnv LANG LC_*
|
||||||
|
HashKnownHosts yes
|
||||||
|
{% if roaming -%}
|
||||||
|
UseRoaming {{ roaming }}
|
||||||
|
{% endif %}
|
159
charmhelpers/contrib/hardening/ssh/templates/sshd_config
Normal file
159
charmhelpers/contrib/hardening/ssh/templates/sshd_config
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
###############################################################################
|
||||||
|
# WARNING: This configuration file is maintained by Juju. Local changes may
|
||||||
|
# be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
# Package generated configuration file
|
||||||
|
# See the sshd_config(5) manpage for details
|
||||||
|
|
||||||
|
# What ports, IPs and protocols we listen for
|
||||||
|
{% for port in ports -%}
|
||||||
|
Port {{ port }}
|
||||||
|
{% endfor -%}
|
||||||
|
AddressFamily {{ addr_family }}
|
||||||
|
# Use these options to restrict which interfaces/protocols sshd will bind to
|
||||||
|
{% if ssh_ip -%}
|
||||||
|
{% for ip in ssh_ip -%}
|
||||||
|
ListenAddress {{ ip }}
|
||||||
|
{% endfor %}
|
||||||
|
{%- else -%}
|
||||||
|
ListenAddress ::
|
||||||
|
ListenAddress 0.0.0.0
|
||||||
|
{% endif -%}
|
||||||
|
Protocol 2
|
||||||
|
{% if ciphers -%}
|
||||||
|
Ciphers {{ ciphers }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if macs -%}
|
||||||
|
MACs {{ macs }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if kexs -%}
|
||||||
|
KexAlgorithms {{ kexs }}
|
||||||
|
{% endif -%}
|
||||||
|
# HostKeys for protocol version 2
|
||||||
|
{% for keyfile in host_key_files -%}
|
||||||
|
HostKey {{ keyfile }}
|
||||||
|
{% endfor -%}
|
||||||
|
|
||||||
|
# Privilege Separation is turned on for security
|
||||||
|
{% if use_priv_sep -%}
|
||||||
|
UsePrivilegeSeparation {{ use_priv_sep }}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# Lifetime and size of ephemeral version 1 server key
|
||||||
|
KeyRegenerationInterval 3600
|
||||||
|
ServerKeyBits 1024
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
SyslogFacility AUTH
|
||||||
|
LogLevel VERBOSE
|
||||||
|
|
||||||
|
# Authentication:
|
||||||
|
LoginGraceTime 30s
|
||||||
|
{% if allow_root_with_key -%}
|
||||||
|
PermitRootLogin without-password
|
||||||
|
{% else -%}
|
||||||
|
PermitRootLogin no
|
||||||
|
{% endif %}
|
||||||
|
PermitTunnel no
|
||||||
|
PermitUserEnvironment no
|
||||||
|
StrictModes yes
|
||||||
|
|
||||||
|
RSAAuthentication yes
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
AuthorizedKeysFile %h/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Don't read the user's ~/.rhosts and ~/.shosts files
|
||||||
|
IgnoreRhosts yes
|
||||||
|
# For this to work you will also need host keys in /etc/ssh_known_hosts
|
||||||
|
RhostsRSAAuthentication no
|
||||||
|
# similar for protocol version 2
|
||||||
|
HostbasedAuthentication no
|
||||||
|
# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication
|
||||||
|
IgnoreUserKnownHosts yes
|
||||||
|
|
||||||
|
# To enable empty passwords, change to yes (NOT RECOMMENDED)
|
||||||
|
PermitEmptyPasswords no
|
||||||
|
|
||||||
|
# Change to yes to enable challenge-response passwords (beware issues with
|
||||||
|
# some PAM modules and threads)
|
||||||
|
ChallengeResponseAuthentication no
|
||||||
|
|
||||||
|
# Change to no to disable tunnelled clear text passwords
|
||||||
|
PasswordAuthentication {{ password_authentication }}
|
||||||
|
|
||||||
|
# Kerberos options
|
||||||
|
KerberosAuthentication no
|
||||||
|
KerberosGetAFSToken no
|
||||||
|
KerberosOrLocalPasswd no
|
||||||
|
KerberosTicketCleanup yes
|
||||||
|
|
||||||
|
# GSSAPI options
|
||||||
|
GSSAPIAuthentication no
|
||||||
|
GSSAPICleanupCredentials yes
|
||||||
|
|
||||||
|
X11Forwarding {{ allow_x11_forwarding }}
|
||||||
|
X11DisplayOffset 10
|
||||||
|
X11UseLocalhost yes
|
||||||
|
GatewayPorts no
|
||||||
|
PrintMotd {{ print_motd }}
|
||||||
|
PrintLastLog {{ print_last_log }}
|
||||||
|
TCPKeepAlive no
|
||||||
|
UseLogin no
|
||||||
|
|
||||||
|
ClientAliveInterval {{ client_alive_interval }}
|
||||||
|
ClientAliveCountMax {{ client_alive_count }}
|
||||||
|
AllowTcpForwarding {{ allow_tcp_forwarding }}
|
||||||
|
AllowAgentForwarding {{ allow_agent_forwarding }}
|
||||||
|
|
||||||
|
MaxStartups 10:30:100
|
||||||
|
#Banner /etc/issue.net
|
||||||
|
|
||||||
|
# Allow client to pass locale environment variables
|
||||||
|
AcceptEnv LANG LC_*
|
||||||
|
|
||||||
|
# Set this to 'yes' to enable PAM authentication, account processing,
|
||||||
|
# and session processing. If this is enabled, PAM authentication will
|
||||||
|
# be allowed through the ChallengeResponseAuthentication and
|
||||||
|
# PasswordAuthentication. Depending on your PAM configuration,
|
||||||
|
# PAM authentication via ChallengeResponseAuthentication may bypass
|
||||||
|
# the setting of "PermitRootLogin without-password".
|
||||||
|
# If you just want the PAM account and session checks to run without
|
||||||
|
# PAM authentication, then enable this but set PasswordAuthentication
|
||||||
|
# and ChallengeResponseAuthentication to 'no'.
|
||||||
|
UsePAM {{ use_pam }}
|
||||||
|
|
||||||
|
{% if deny_users -%}
|
||||||
|
DenyUsers {{ deny_users }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if allow_users -%}
|
||||||
|
AllowUsers {{ allow_users }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if deny_groups -%}
|
||||||
|
DenyGroups {{ deny_groups }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if allow_groups -%}
|
||||||
|
AllowGroups allow_groups
|
||||||
|
{% endif -%}
|
||||||
|
UseDNS {{ use_dns }}
|
||||||
|
MaxAuthTries {{ max_auth_tries }}
|
||||||
|
MaxSessions {{ max_sessions }}
|
||||||
|
|
||||||
|
{% if sftp_enable -%}
|
||||||
|
# Configuration, in case SFTP is used
|
||||||
|
## override default of no subsystems
|
||||||
|
## Subsystem sftp /opt/app/openssh5/libexec/sftp-server
|
||||||
|
Subsystem sftp internal-sftp -l VERBOSE
|
||||||
|
|
||||||
|
## These lines must appear at the *end* of sshd_config
|
||||||
|
Match Group {{ sftp_group }}
|
||||||
|
ForceCommand internal-sftp -l VERBOSE
|
||||||
|
ChrootDirectory {{ sftp_chroot }}
|
||||||
|
{% else -%}
|
||||||
|
# Configuration, in case SFTP is used
|
||||||
|
## override default of no subsystems
|
||||||
|
## Subsystem sftp /opt/app/openssh5/libexec/sftp-server
|
||||||
|
## These lines must appear at the *end* of sshd_config
|
||||||
|
Match Group sftponly
|
||||||
|
ForceCommand internal-sftp -l VERBOSE
|
||||||
|
ChrootDirectory /sftpchroot/home/%u
|
||||||
|
{% endif %}
|
71
charmhelpers/contrib/hardening/templating.py
Normal file
71
charmhelpers/contrib/hardening/templating.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
DEBUG,
|
||||||
|
WARNING,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jinja2 import FileSystemLoader, Environment
|
||||||
|
except ImportError:
|
||||||
|
from charmhelpers.fetch import apt_install
|
||||||
|
from charmhelpers.fetch import apt_update
|
||||||
|
apt_update(fatal=True)
|
||||||
|
apt_install('python-jinja2', fatal=True)
|
||||||
|
from jinja2 import FileSystemLoader, Environment
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: function separated from main rendering code to facilitate easier
|
||||||
|
# mocking in unit tests.
|
||||||
|
def write(path, data):
|
||||||
|
with open(path, 'wb') as out:
|
||||||
|
out.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_path(template_dir, path):
|
||||||
|
"""Returns the template file which would be used to render the path.
|
||||||
|
|
||||||
|
The path to the template file is returned.
|
||||||
|
:param template_dir: the directory the templates are located in
|
||||||
|
:param path: the file path to be written to.
|
||||||
|
:returns: path to the template file
|
||||||
|
"""
|
||||||
|
return os.path.join(template_dir, os.path.basename(path))
|
||||||
|
|
||||||
|
|
||||||
|
def render_and_write(template_dir, path, context):
|
||||||
|
"""Renders the specified template into the file.
|
||||||
|
|
||||||
|
:param template_dir: the directory to load the template from
|
||||||
|
:param path: the path to write the templated contents to
|
||||||
|
:param context: the parameters to pass to the rendering engine
|
||||||
|
"""
|
||||||
|
env = Environment(loader=FileSystemLoader(template_dir))
|
||||||
|
template_file = os.path.basename(path)
|
||||||
|
template = env.get_template(template_file)
|
||||||
|
log('Rendering from template: %s' % template.name, level=DEBUG)
|
||||||
|
rendered_content = template.render(context)
|
||||||
|
if not rendered_content:
|
||||||
|
log("Render returned None - skipping '%s'" % path,
|
||||||
|
level=WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
|
write(path, rendered_content.encode('utf-8').strip())
|
||||||
|
log('Wrote template %s' % path, level=DEBUG)
|
157
charmhelpers/contrib/hardening/utils.py
Normal file
157
charmhelpers/contrib/hardening/utils.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# 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 glob
|
||||||
|
import grp
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import six
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Global settings cache. Since each hook fire entails a fresh module import it
|
||||||
|
# is safe to hold this in memory and not risk missing config changes (since
|
||||||
|
# they will result in a new hook fire and thus re-import).
|
||||||
|
__SETTINGS__ = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_defaults(modules):
|
||||||
|
"""Load the default config for the provided modules.
|
||||||
|
|
||||||
|
:param modules: stack modules config defaults to lookup.
|
||||||
|
:returns: modules default config dictionary.
|
||||||
|
"""
|
||||||
|
default = os.path.join(os.path.dirname(__file__),
|
||||||
|
'defaults/%s.yaml' % (modules))
|
||||||
|
return yaml.safe_load(open(default))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_schema(modules):
|
||||||
|
"""Load the config schema for the provided modules.
|
||||||
|
|
||||||
|
NOTE: this schema is intended to have 1-1 relationship with they keys in
|
||||||
|
the default config and is used a means to verify valid overrides provided
|
||||||
|
by the user.
|
||||||
|
|
||||||
|
:param modules: stack modules config schema to lookup.
|
||||||
|
:returns: modules default schema dictionary.
|
||||||
|
"""
|
||||||
|
schema = os.path.join(os.path.dirname(__file__),
|
||||||
|
'defaults/%s.yaml.schema' % (modules))
|
||||||
|
return yaml.safe_load(open(schema))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_provided_overrides(modules):
|
||||||
|
"""Load user-provided config overrides.
|
||||||
|
|
||||||
|
:param modules: stack modules to lookup in user overrides yaml file.
|
||||||
|
:returns: overrides dictionary.
|
||||||
|
"""
|
||||||
|
overrides = os.path.join(os.environ['JUJU_CHARM_DIR'],
|
||||||
|
'hardening.yaml')
|
||||||
|
if os.path.exists(overrides):
|
||||||
|
log("Found user-provided config overrides file '%s'" %
|
||||||
|
(overrides), level=DEBUG)
|
||||||
|
settings = yaml.safe_load(open(overrides))
|
||||||
|
if settings and settings.get(modules):
|
||||||
|
log("Applying '%s' overrides" % (modules), level=DEBUG)
|
||||||
|
return settings.get(modules)
|
||||||
|
|
||||||
|
log("No overrides found for '%s'" % (modules), level=DEBUG)
|
||||||
|
else:
|
||||||
|
log("No hardening config overrides file '%s' found in charm "
|
||||||
|
"root dir" % (overrides), level=DEBUG)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_overrides(settings, overrides, schema):
|
||||||
|
"""Get overrides config overlayed onto modules defaults.
|
||||||
|
|
||||||
|
:param modules: require stack modules config.
|
||||||
|
:returns: dictionary of modules config with user overrides applied.
|
||||||
|
"""
|
||||||
|
if overrides:
|
||||||
|
for k, v in six.iteritems(overrides):
|
||||||
|
if k in schema:
|
||||||
|
if schema[k] is None:
|
||||||
|
settings[k] = v
|
||||||
|
elif type(schema[k]) is dict:
|
||||||
|
settings[k] = _apply_overrides(settings[k], overrides[k],
|
||||||
|
schema[k])
|
||||||
|
else:
|
||||||
|
raise Exception("Unexpected type found in schema '%s'" %
|
||||||
|
type(schema[k]), level=ERROR)
|
||||||
|
else:
|
||||||
|
log("Unknown override key '%s' - ignoring" % (k), level=INFO)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings(modules):
|
||||||
|
global __SETTINGS__
|
||||||
|
if modules in __SETTINGS__:
|
||||||
|
return __SETTINGS__[modules]
|
||||||
|
|
||||||
|
schema = _get_schema(modules)
|
||||||
|
settings = _get_defaults(modules)
|
||||||
|
overrides = _get_user_provided_overrides(modules)
|
||||||
|
__SETTINGS__[modules] = _apply_overrides(settings, overrides, schema)
|
||||||
|
return __SETTINGS__[modules]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_permissions(path, user, group, permissions, maxdepth=-1):
|
||||||
|
"""Ensure permissions for path.
|
||||||
|
|
||||||
|
If path is a file, apply to file and return. If path is a directory,
|
||||||
|
apply recursively (if required) to directory contents and return.
|
||||||
|
|
||||||
|
:param user: user name
|
||||||
|
:param group: group name
|
||||||
|
:param permissions: octal permissions
|
||||||
|
:param maxdepth: maximum recursion depth. A negative maxdepth allows
|
||||||
|
infinite recursion and maxdepth=0 means no recursion.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
log("File '%s' does not exist - cannot set permissions" % (path),
|
||||||
|
level=WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
|
_user = pwd.getpwnam(user)
|
||||||
|
os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid)
|
||||||
|
os.chmod(path, permissions)
|
||||||
|
|
||||||
|
if maxdepth == 0:
|
||||||
|
log("Max recursion depth reached - skipping further recursion",
|
||||||
|
level=DEBUG)
|
||||||
|
return
|
||||||
|
elif maxdepth > 0:
|
||||||
|
maxdepth -= 1
|
||||||
|
|
||||||
|
if os.path.isdir(path):
|
||||||
|
contents = glob.glob("%s/*" % (path))
|
||||||
|
for c in contents:
|
||||||
|
ensure_permissions(c, user=user, group=group,
|
||||||
|
permissions=permissions, maxdepth=maxdepth)
|
@ -31,7 +31,7 @@ from keystoneclient.auth.identity import v3 as keystone_id_v3
|
|||||||
from keystoneclient import session as keystone_session
|
from keystoneclient import session as keystone_session
|
||||||
from keystoneclient.v3 import client as keystone_client_v3
|
from keystoneclient.v3 import client as keystone_client_v3
|
||||||
|
|
||||||
import novaclient.v1_1.client as nova_client
|
import novaclient.client as nova_client
|
||||||
import pika
|
import pika
|
||||||
import swiftclient
|
import swiftclient
|
||||||
|
|
||||||
@ -42,6 +42,8 @@ from charmhelpers.contrib.amulet.utils import (
|
|||||||
DEBUG = logging.DEBUG
|
DEBUG = logging.DEBUG
|
||||||
ERROR = logging.ERROR
|
ERROR = logging.ERROR
|
||||||
|
|
||||||
|
NOVA_CLIENT_VERSION = "2"
|
||||||
|
|
||||||
|
|
||||||
class OpenStackAmuletUtils(AmuletUtils):
|
class OpenStackAmuletUtils(AmuletUtils):
|
||||||
"""OpenStack amulet utilities.
|
"""OpenStack amulet utilities.
|
||||||
@ -249,7 +251,8 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
self.log.debug('Authenticating nova user ({})...'.format(user))
|
self.log.debug('Authenticating nova user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
endpoint_type='publicURL')
|
||||||
return nova_client.Client(username=user, api_key=password,
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
|
username=user, api_key=password,
|
||||||
project_id=tenant, auth_url=ep)
|
project_id=tenant, auth_url=ep)
|
||||||
|
|
||||||
def authenticate_swift_user(self, keystone, user, password, tenant):
|
def authenticate_swift_user(self, keystone, user, password, tenant):
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
{% if auth_host -%}
|
{% if auth_host -%}
|
||||||
{% if api_version == '3' -%}
|
|
||||||
[keystone_authtoken]
|
[keystone_authtoken]
|
||||||
auth_url = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
|
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
|
||||||
|
auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}
|
||||||
|
auth_plugin = password
|
||||||
|
project_domain_id = default
|
||||||
|
user_domain_id = default
|
||||||
project_name = {{ admin_tenant_name }}
|
project_name = {{ admin_tenant_name }}
|
||||||
username = {{ admin_user }}
|
username = {{ admin_user }}
|
||||||
password = {{ admin_password }}
|
password = {{ admin_password }}
|
||||||
project_domain_name = default
|
|
||||||
user_domain_name = default
|
|
||||||
auth_plugin = password
|
|
||||||
{% else -%}
|
|
||||||
[keystone_authtoken]
|
|
||||||
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
|
|
||||||
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
|
|
||||||
admin_tenant_name = {{ admin_tenant_name }}
|
|
||||||
admin_user = {{ admin_user }}
|
|
||||||
admin_password = {{ admin_password }}
|
|
||||||
signing_dir = {{ signing_dir }}
|
signing_dir = {{ signing_dir }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% endif -%}
|
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
{% if auth_host -%}
|
||||||
|
[keystone_authtoken]
|
||||||
|
# Juno specific config (Bug #1557223)
|
||||||
|
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
|
||||||
|
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
signing_dir = {{ signing_dir }}
|
||||||
|
{% endif -%}
|
@ -173,7 +173,7 @@ class Pool(object):
|
|||||||
elif mode == 'writeback':
|
elif mode == 'writeback':
|
||||||
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'forward'])
|
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'forward'])
|
||||||
# Flush the cache and wait for it to return
|
# Flush the cache and wait for it to return
|
||||||
check_call(['ceph', '--id', self.service, '-p', cache_pool, 'cache-flush-evict-all'])
|
check_call(['rados', '--id', self.service, '-p', cache_pool, 'cache-flush-evict-all'])
|
||||||
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove-overlay', self.name])
|
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove-overlay', self.name])
|
||||||
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
|
check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
|
||||||
|
|
||||||
|
@ -135,3 +135,9 @@ options:
|
|||||||
wait for you to execute the openstack-upgrade action for this charm on
|
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
|
each unit. If False it will revert to existing behavior of upgrading
|
||||||
all units on config change.
|
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
|
@ -52,6 +52,7 @@ from charmhelpers.contrib.network.ip import (
|
|||||||
get_ipv6_addr
|
get_ipv6_addr
|
||||||
)
|
)
|
||||||
from charmhelpers.contrib.charmsupport import nrpe
|
from charmhelpers.contrib.charmsupport import nrpe
|
||||||
|
from charmhelpers.contrib.hardening.harden import harden
|
||||||
|
|
||||||
from distutils.dir_util import mkpath
|
from distutils.dir_util import mkpath
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ SUDOERS_D = '/etc/sudoers.d'
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('install.real')
|
@hooks.hook('install.real')
|
||||||
|
@harden()
|
||||||
def install():
|
def install():
|
||||||
status_set('maintenance', 'Executing pre-install')
|
status_set('maintenance', 'Executing pre-install')
|
||||||
execd_preinstall()
|
execd_preinstall()
|
||||||
@ -76,6 +78,7 @@ def install():
|
|||||||
|
|
||||||
@hooks.hook('config-changed')
|
@hooks.hook('config-changed')
|
||||||
@pause_aware_restart_on_change(RESTART_MAP)
|
@pause_aware_restart_on_change(RESTART_MAP)
|
||||||
|
@harden()
|
||||||
def config_changed():
|
def config_changed():
|
||||||
if config('prefer-ipv6'):
|
if config('prefer-ipv6'):
|
||||||
status_set('maintenance', 'Configuring ipv6')
|
status_set('maintenance', 'Configuring ipv6')
|
||||||
@ -102,6 +105,7 @@ def config_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('upgrade-charm')
|
@hooks.hook('upgrade-charm')
|
||||||
|
@harden()
|
||||||
def upgrade_charm():
|
def upgrade_charm():
|
||||||
apt_install(filter_installed_packages(PACKAGES), fatal=True)
|
apt_install(filter_installed_packages(PACKAGES), fatal=True)
|
||||||
update_nrpe_config()
|
update_nrpe_config()
|
||||||
@ -179,6 +183,12 @@ def update_nrpe_config():
|
|||||||
nrpe_setup.write()
|
nrpe_setup.write()
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('update-status')
|
||||||
|
@harden()
|
||||||
|
def update_status():
|
||||||
|
log('Updating status.')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
hooks.execute(sys.argv)
|
hooks.execute(sys.argv)
|
||||||
|
1
hooks/update-status
Symbolic link
1
hooks/update-status
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
swift_storage_hooks.py
|
@ -782,15 +782,20 @@ class AmuletUtils(object):
|
|||||||
|
|
||||||
# amulet juju action helpers:
|
# amulet juju action helpers:
|
||||||
def run_action(self, unit_sentry, action,
|
def run_action(self, unit_sentry, action,
|
||||||
_check_output=subprocess.check_output):
|
_check_output=subprocess.check_output,
|
||||||
|
params=None):
|
||||||
"""Run the named action on a given unit sentry.
|
"""Run the named action on a given unit sentry.
|
||||||
|
|
||||||
|
params a dict of parameters to use
|
||||||
_check_output parameter is used for dependency injection.
|
_check_output parameter is used for dependency injection.
|
||||||
|
|
||||||
@return action_id.
|
@return action_id.
|
||||||
"""
|
"""
|
||||||
unit_id = unit_sentry.info["unit_name"]
|
unit_id = unit_sentry.info["unit_name"]
|
||||||
command = ["juju", "action", "do", "--format=json", unit_id, action]
|
command = ["juju", "action", "do", "--format=json", unit_id, action]
|
||||||
|
if params is not None:
|
||||||
|
for key, value in params.iteritems():
|
||||||
|
command.append("{}={}".format(key, value))
|
||||||
self.log.info("Running command: %s\n" % " ".join(command))
|
self.log.info("Running command: %s\n" % " ".join(command))
|
||||||
output = _check_output(command, universal_newlines=True)
|
output = _check_output(command, universal_newlines=True)
|
||||||
data = json.loads(output)
|
data = json.loads(output)
|
||||||
|
@ -31,7 +31,7 @@ from keystoneclient.auth.identity import v3 as keystone_id_v3
|
|||||||
from keystoneclient import session as keystone_session
|
from keystoneclient import session as keystone_session
|
||||||
from keystoneclient.v3 import client as keystone_client_v3
|
from keystoneclient.v3 import client as keystone_client_v3
|
||||||
|
|
||||||
import novaclient.v1_1.client as nova_client
|
import novaclient.client as nova_client
|
||||||
import pika
|
import pika
|
||||||
import swiftclient
|
import swiftclient
|
||||||
|
|
||||||
@ -42,6 +42,8 @@ from charmhelpers.contrib.amulet.utils import (
|
|||||||
DEBUG = logging.DEBUG
|
DEBUG = logging.DEBUG
|
||||||
ERROR = logging.ERROR
|
ERROR = logging.ERROR
|
||||||
|
|
||||||
|
NOVA_CLIENT_VERSION = "2"
|
||||||
|
|
||||||
|
|
||||||
class OpenStackAmuletUtils(AmuletUtils):
|
class OpenStackAmuletUtils(AmuletUtils):
|
||||||
"""OpenStack amulet utilities.
|
"""OpenStack amulet utilities.
|
||||||
@ -249,7 +251,8 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
self.log.debug('Authenticating nova user ({})...'.format(user))
|
self.log.debug('Authenticating nova user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
endpoint_type='publicURL')
|
||||||
return nova_client.Client(username=user, api_key=password,
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
|
username=user, api_key=password,
|
||||||
project_id=tenant, auth_url=ep)
|
project_id=tenant, auth_url=ep)
|
||||||
|
|
||||||
def authenticate_swift_user(self, keystone, user, password, tenant):
|
def authenticate_swift_user(self, keystone, user, password, tenant):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@ -7,10 +9,20 @@ import yaml
|
|||||||
|
|
||||||
from test_utils import CharmTestCase
|
from test_utils import CharmTestCase
|
||||||
|
|
||||||
from mock import patch
|
from mock import patch, MagicMock
|
||||||
with patch('actions.hooks.lib.misc_utils.is_paused') as is_paused:
|
|
||||||
with patch('actions.hooks.lib.swift_storage_utils.register_configs') as _:
|
# python-apt is not installed as part of test-requirements but is imported by
|
||||||
import actions.actions
|
# some charmhelpers modules so create a fake import.
|
||||||
|
sys.modules['apt'] = MagicMock()
|
||||||
|
sys.modules['apt_pkg'] = MagicMock()
|
||||||
|
|
||||||
|
with patch('actions.hooks.charmhelpers.contrib.hardening.harden.harden') as \
|
||||||
|
mock_dec:
|
||||||
|
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
|
||||||
|
lambda *args, **kwargs: f(*args, **kwargs))
|
||||||
|
with patch('actions.hooks.lib.misc_utils.is_paused') as is_paused:
|
||||||
|
with patch('actions.hooks.lib.swift_storage_utils.register_configs'):
|
||||||
|
import actions.actions
|
||||||
|
|
||||||
|
|
||||||
class PauseTestCase(CharmTestCase):
|
class PauseTestCase(CharmTestCase):
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
from mock import patch
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
os.environ['JUJU_UNIT_NAME'] = 'swift-storge'
|
os.environ['JUJU_UNIT_NAME'] = 'swift-storge'
|
||||||
|
|
||||||
with patch('lib.misc_utils.is_paused') as is_paused:
|
# python-apt is not installed as part of test-requirements but is imported by
|
||||||
with patch('lib.swift_storage_utils.register_configs') as _:
|
# some charmhelpers modules so create a fake import.
|
||||||
import actions.openstack_upgrade as openstack_upgrade
|
sys.modules['apt'] = MagicMock()
|
||||||
|
sys.modules['apt_pkg'] = MagicMock()
|
||||||
|
|
||||||
|
with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec:
|
||||||
|
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
|
||||||
|
lambda *args, **kwargs: f(*args, **kwargs))
|
||||||
|
with patch('lib.misc_utils.is_paused') as is_paused:
|
||||||
|
with patch('lib.swift_storage_utils.register_configs'):
|
||||||
|
import actions.openstack_upgrade as openstack_upgrade
|
||||||
|
|
||||||
from test_utils import (
|
from test_utils import (
|
||||||
CharmTestCase
|
CharmTestCase
|
||||||
|
@ -5,9 +5,12 @@ import uuid
|
|||||||
|
|
||||||
from test_utils import CharmTestCase, patch_open
|
from test_utils import CharmTestCase, patch_open
|
||||||
|
|
||||||
with patch('hooks.lib.misc_utils.is_paused') as is_paused:
|
with patch('hooks.charmhelpers.contrib.hardening.harden.harden') as mock_dec:
|
||||||
with patch('hooks.lib.swift_storage_utils.register_configs') as _:
|
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
|
||||||
import hooks.swift_storage_hooks as hooks
|
lambda *args, **kwargs: f(*args, **kwargs))
|
||||||
|
with patch('hooks.lib.misc_utils.is_paused') as is_paused:
|
||||||
|
with patch('hooks.lib.swift_storage_utils.register_configs') as _:
|
||||||
|
import hooks.swift_storage_hooks as hooks
|
||||||
|
|
||||||
from lib.swift_storage_utils import PACKAGES
|
from lib.swift_storage_utils import PACKAGES
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user