summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunaid Ali <junaidali@plumgrid.com>2016-04-22 04:15:33 -0400
committerJunaid Ali <junaidali@plumgrid.com>2016-04-22 04:15:33 -0400
commitc0d2d9c303a9d15e6b50678e709ae08b8296a891 (patch)
treee7057571cbffb887b47cfb5f5c6696ada2ab2dd7
parent1dee6f9daa01a6931191020465cc5968ab218fe2 (diff)
make sync
-rw-r--r--bin/charm_helpers_sync.py253
-rw-r--r--hooks/charmhelpers/contrib/amulet/utils.py33
-rw-r--r--hooks/charmhelpers/contrib/hardening/__init__.py15
-rw-r--r--hooks/charmhelpers/contrib/hardening/apache/__init__.py19
-rw-r--r--hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py31
-rw-r--r--hooks/charmhelpers/contrib/hardening/apache/checks/config.py100
-rw-r--r--hooks/charmhelpers/contrib/hardening/audits/__init__.py63
-rw-r--r--hooks/charmhelpers/contrib/hardening/audits/apache.py100
-rw-r--r--hooks/charmhelpers/contrib/hardening/audits/apt.py105
-rw-r--r--hooks/charmhelpers/contrib/hardening/audits/file.py552
-rw-r--r--hooks/charmhelpers/contrib/hardening/harden.py84
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/__init__.py19
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/__init__.py50
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/apt.py39
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/limits.py55
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/login.py67
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py52
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/pam.py134
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/profile.py45
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/securetty.py39
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py131
-rw-r--r--hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py211
-rw-r--r--hooks/charmhelpers/contrib/hardening/mysql/__init__.py19
-rw-r--r--hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py31
-rw-r--r--hooks/charmhelpers/contrib/hardening/mysql/checks/config.py89
-rw-r--r--hooks/charmhelpers/contrib/hardening/ssh/__init__.py19
-rw-r--r--hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py31
-rw-r--r--hooks/charmhelpers/contrib/hardening/ssh/checks/config.py394
-rw-r--r--hooks/charmhelpers/contrib/hardening/templating.py71
-rw-r--r--hooks/charmhelpers/contrib/hardening/utils.py157
-rw-r--r--hooks/charmhelpers/contrib/network/ip.py24
-rw-r--r--hooks/charmhelpers/contrib/network/ovs/__init__.py8
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/deployment.py4
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/utils.py53
-rw-r--r--hooks/charmhelpers/contrib/openstack/context.py110
-rw-r--r--hooks/charmhelpers/contrib/openstack/ip.py42
-rw-r--r--hooks/charmhelpers/contrib/openstack/neutron.py14
-rw-r--r--hooks/charmhelpers/contrib/openstack/utils.py673
-rw-r--r--hooks/charmhelpers/contrib/python/packages.py29
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/ceph.py199
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/utils.py10
-rw-r--r--hooks/charmhelpers/core/hookenv.py31
-rw-r--r--hooks/charmhelpers/core/host.py78
43 files changed, 4127 insertions, 156 deletions
diff --git a/bin/charm_helpers_sync.py b/bin/charm_helpers_sync.py
new file mode 100644
index 0000000..f67fdb9
--- /dev/null
+++ b/bin/charm_helpers_sync.py
@@ -0,0 +1,253 @@
1#!/usr/bin/python
2
3# Copyright 2014-2015 Canonical Limited.
4#
5# This file is part of charm-helpers.
6#
7# charm-helpers is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3 as
9# published by the Free Software Foundation.
10#
11# charm-helpers is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
19# Authors:
20# Adam Gandelman <adamg@ubuntu.com>
21
22import logging
23import optparse
24import os
25import subprocess
26import shutil
27import sys
28import tempfile
29import yaml
30from fnmatch import fnmatch
31
32import six
33
34CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
35
36
37def parse_config(conf_file):
38 if not os.path.isfile(conf_file):
39 logging.error('Invalid config file: %s.' % conf_file)
40 return False
41 return yaml.load(open(conf_file).read())
42
43
44def clone_helpers(work_dir, branch):
45 dest = os.path.join(work_dir, 'charm-helpers')
46 logging.info('Checking out %s to %s.' % (branch, dest))
47 cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
48 subprocess.check_call(cmd)
49 return dest
50
51
52def _module_path(module):
53 return os.path.join(*module.split('.'))
54
55
56def _src_path(src, module):
57 return os.path.join(src, 'charmhelpers', _module_path(module))
58
59
60def _dest_path(dest, module):
61 return os.path.join(dest, _module_path(module))
62
63
64def _is_pyfile(path):
65 return os.path.isfile(path + '.py')
66
67
68def ensure_init(path):
69 '''
70 ensure directories leading up to path are importable, omitting
71 parent directory, eg path='/hooks/helpers/foo'/:
72 hooks/
73 hooks/helpers/__init__.py
74 hooks/helpers/foo/__init__.py
75 '''
76 for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
77 _i = os.path.join(d, '__init__.py')
78 if not os.path.exists(_i):
79 logging.info('Adding missing __init__.py: %s' % _i)
80 open(_i, 'wb').close()
81
82
83def sync_pyfile(src, dest):
84 src = src + '.py'
85 src_dir = os.path.dirname(src)
86 logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
87 if not os.path.exists(dest):
88 os.makedirs(dest)
89 shutil.copy(src, dest)
90 if os.path.isfile(os.path.join(src_dir, '__init__.py')):
91 shutil.copy(os.path.join(src_dir, '__init__.py'),
92 dest)
93 ensure_init(dest)
94
95
96def get_filter(opts=None):
97 opts = opts or []
98 if 'inc=*' in opts:
99 # do not filter any files, include everything
100 return None
101
102 def _filter(dir, ls):
103 incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
104 _filter = []
105 for f in ls:
106 _f = os.path.join(dir, f)
107
108 if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
109 if True not in [fnmatch(_f, inc) for inc in incs]:
110 logging.debug('Not syncing %s, does not match include '
111 'filters (%s)' % (_f, incs))
112 _filter.append(f)
113 else:
114 logging.debug('Including file, which matches include '
115 'filters (%s): %s' % (incs, _f))
116 elif (os.path.isfile(_f) and not _f.endswith('.py')):
117 logging.debug('Not syncing file: %s' % f)
118 _filter.append(f)
119 elif (os.path.isdir(_f) and not
120 os.path.isfile(os.path.join(_f, '__init__.py'))):
121 logging.debug('Not syncing directory: %s' % f)
122 _filter.append(f)
123 return _filter
124 return _filter
125
126
127def sync_directory(src, dest, opts=None):
128 if os.path.exists(dest):
129 logging.debug('Removing existing directory: %s' % dest)
130 shutil.rmtree(dest)
131 logging.info('Syncing directory: %s -> %s.' % (src, dest))
132
133 shutil.copytree(src, dest, ignore=get_filter(opts))
134 ensure_init(dest)
135
136
137def sync(src, dest, module, opts=None):
138
139 # Sync charmhelpers/__init__.py for bootstrap code.
140 sync_pyfile(_src_path(src, '__init__'), dest)
141
142 # Sync other __init__.py files in the path leading to module.
143 m = []
144 steps = module.split('.')[:-1]
145 while steps:
146 m.append(steps.pop(0))
147 init = '.'.join(m + ['__init__'])
148 sync_pyfile(_src_path(src, init),
149 os.path.dirname(_dest_path(dest, init)))
150
151 # Sync the module, or maybe a .py file.
152 if os.path.isdir(_src_path(src, module)):
153 sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
154 elif _is_pyfile(_src_path(src, module)):
155 sync_pyfile(_src_path(src, module),
156 os.path.dirname(_dest_path(dest, module)))
157 else:
158 logging.warn('Could not sync: %s. Neither a pyfile or directory, '
159 'does it even exist?' % module)
160
161
162def parse_sync_options(options):
163 if not options:
164 return []
165 return options.split(',')
166
167
168def extract_options(inc, global_options=None):
169 global_options = global_options or []
170 if global_options and isinstance(global_options, six.string_types):
171 global_options = [global_options]
172 if '|' not in inc:
173 return (inc, global_options)
174 inc, opts = inc.split('|')
175 return (inc, parse_sync_options(opts) + global_options)
176
177
178def sync_helpers(include, src, dest, options=None):
179 if not os.path.isdir(dest):
180 os.makedirs(dest)
181
182 global_options = parse_sync_options(options)
183
184 for inc in include:
185 if isinstance(inc, str):
186 inc, opts = extract_options(inc, global_options)
187 sync(src, dest, inc, opts)
188 elif isinstance(inc, dict):
189 # could also do nested dicts here.
190 for k, v in six.iteritems(inc):
191 if isinstance(v, list):
192 for m in v:
193 inc, opts = extract_options(m, global_options)
194 sync(src, dest, '%s.%s' % (k, inc), opts)
195
196if __name__ == '__main__':
197 parser = optparse.OptionParser()
198 parser.add_option('-c', '--config', action='store', dest='config',
199 default=None, help='helper config file')
200 parser.add_option('-D', '--debug', action='store_true', dest='debug',
201 default=False, help='debug')
202 parser.add_option('-b', '--branch', action='store', dest='branch',
203 help='charm-helpers bzr branch (overrides config)')
204 parser.add_option('-d', '--destination', action='store', dest='dest_dir',
205 help='sync destination dir (overrides config)')
206 (opts, args) = parser.parse_args()
207
208 if opts.debug:
209 logging.basicConfig(level=logging.DEBUG)
210 else:
211 logging.basicConfig(level=logging.INFO)
212
213 if opts.config:
214 logging.info('Loading charm helper config from %s.' % opts.config)
215 config = parse_config(opts.config)
216 if not config:
217 logging.error('Could not parse config from %s.' % opts.config)
218 sys.exit(1)
219 else:
220 config = {}
221
222 if 'branch' not in config:
223 config['branch'] = CHARM_HELPERS_BRANCH
224 if opts.branch:
225 config['branch'] = opts.branch
226 if opts.dest_dir:
227 config['destination'] = opts.dest_dir
228
229 if 'destination' not in config:
230 logging.error('No destination dir. specified as option or config.')
231 sys.exit(1)
232
233 if 'include' not in config:
234 if not args:
235 logging.error('No modules to sync specified as option or config.')
236 sys.exit(1)
237 config['include'] = []
238 [config['include'].append(a) for a in args]
239
240 sync_options = None
241 if 'options' in config:
242 sync_options = config['options']
243 tmpd = tempfile.mkdtemp()
244 try:
245 checkout = clone_helpers(tmpd, config['branch'])
246 sync_helpers(config['include'], checkout, config['destination'],
247 options=sync_options)
248 except Exception as e:
249 logging.error("Could not sync: %s" % e)
250 raise e
251 finally:
252 logging.debug('Cleaning up %s' % tmpd)
253 shutil.rmtree(tmpd)
diff --git a/hooks/charmhelpers/contrib/amulet/utils.py b/hooks/charmhelpers/contrib/amulet/utils.py
index 2591a9b..7e5c25a 100644
--- a/hooks/charmhelpers/contrib/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/amulet/utils.py
@@ -601,7 +601,7 @@ class AmuletUtils(object):
601 return ('Process name count mismatch. expected, actual: {}, ' 601 return ('Process name count mismatch. expected, actual: {}, '
602 '{}'.format(len(expected), len(actual))) 602 '{}'.format(len(expected), len(actual)))
603 603
604 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \ 604 for (e_proc_name, e_pids), (a_proc_name, a_pids) in \
605 zip(e_proc_names.items(), a_proc_names.items()): 605 zip(e_proc_names.items(), a_proc_names.items()):
606 if e_proc_name != a_proc_name: 606 if e_proc_name != a_proc_name:
607 return ('Process name mismatch. expected, actual: {}, ' 607 return ('Process name mismatch. expected, actual: {}, '
@@ -610,25 +610,31 @@ class AmuletUtils(object):
610 a_pids_length = len(a_pids) 610 a_pids_length = len(a_pids)
611 fail_msg = ('PID count mismatch. {} ({}) expected, actual: ' 611 fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
612 '{}, {} ({})'.format(e_sentry_name, e_proc_name, 612 '{}, {} ({})'.format(e_sentry_name, e_proc_name,
613 e_pids_length, a_pids_length, 613 e_pids, a_pids_length,
614 a_pids)) 614 a_pids))
615 615
616 # If expected is not bool, ensure PID quantities match 616 # If expected is a list, ensure at least one PID quantity match
617 if not isinstance(e_pids_length, bool) and \ 617 if isinstance(e_pids, list) and \
618 a_pids_length != e_pids_length: 618 a_pids_length not in e_pids:
619 return fail_msg
620 # If expected is not bool and not list,
621 # ensure PID quantities match
622 elif not isinstance(e_pids, bool) and \
623 not isinstance(e_pids, list) and \
624 a_pids_length != e_pids:
619 return fail_msg 625 return fail_msg
620 # If expected is bool True, ensure 1 or more PIDs exist 626 # If expected is bool True, ensure 1 or more PIDs exist
621 elif isinstance(e_pids_length, bool) and \ 627 elif isinstance(e_pids, bool) and \
622 e_pids_length is True and a_pids_length < 1: 628 e_pids is True and a_pids_length < 1:
623 return fail_msg 629 return fail_msg
624 # If expected is bool False, ensure 0 PIDs exist 630 # If expected is bool False, ensure 0 PIDs exist
625 elif isinstance(e_pids_length, bool) and \ 631 elif isinstance(e_pids, bool) and \
626 e_pids_length is False and a_pids_length != 0: 632 e_pids is False and a_pids_length != 0:
627 return fail_msg 633 return fail_msg
628 else: 634 else:
629 self.log.debug('PID check OK: {} {} {}: ' 635 self.log.debug('PID check OK: {} {} {}: '
630 '{}'.format(e_sentry_name, e_proc_name, 636 '{}'.format(e_sentry_name, e_proc_name,
631 e_pids_length, a_pids)) 637 e_pids, a_pids))
632 return None 638 return None
633 639
634 def validate_list_of_identical_dicts(self, list_of_dicts): 640 def validate_list_of_identical_dicts(self, list_of_dicts):
@@ -782,15 +788,20 @@ class AmuletUtils(object):
782 788
783# amulet juju action helpers: 789# amulet juju action helpers:
784 def run_action(self, unit_sentry, action, 790 def run_action(self, unit_sentry, action,
785 _check_output=subprocess.check_output): 791 _check_output=subprocess.check_output,
792 params=None):
786 """Run the named action on a given unit sentry. 793 """Run the named action on a given unit sentry.
787 794
795 params a dict of parameters to use
788 _check_output parameter is used for dependency injection. 796 _check_output parameter is used for dependency injection.
789 797
790 @return action_id. 798 @return action_id.
791 """ 799 """
792 unit_id = unit_sentry.info["unit_name"] 800 unit_id = unit_sentry.info["unit_name"]
793 command = ["juju", "action", "do", "--format=json", unit_id, action] 801 command = ["juju", "action", "do", "--format=json", unit_id, action]
802 if params is not None:
803 for key, value in params.iteritems():
804 command.append("{}={}".format(key, value))
794 self.log.info("Running command: %s\n" % " ".join(command)) 805 self.log.info("Running command: %s\n" % " ".join(command))
795 output = _check_output(command, universal_newlines=True) 806 output = _check_output(command, universal_newlines=True)
796 data = json.loads(output) 807 data = json.loads(output)
diff --git a/hooks/charmhelpers/contrib/hardening/__init__.py b/hooks/charmhelpers/contrib/hardening/__init__.py
new file mode 100644
index 0000000..a133532
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/__init__.py
@@ -0,0 +1,15 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
diff --git a/hooks/charmhelpers/contrib/hardening/apache/__init__.py b/hooks/charmhelpers/contrib/hardening/apache/__init__.py
new file mode 100644
index 0000000..277b8c7
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/apache/__init__.py
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
diff --git a/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py b/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
new file mode 100644
index 0000000..d130479
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.apache.checks import config
22
23
24def run_apache_checks():
25 log("Starting Apache hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("Apache hardening checks complete.", level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/apache/checks/config.py b/hooks/charmhelpers/contrib/hardening/apache/checks/config.py
new file mode 100644
index 0000000..8249ca0
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/apache/checks/config.py
@@ -0,0 +1,100 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import re
19import subprocess
20
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO,
25)
26from charmhelpers.contrib.hardening.audits.file import (
27 FilePermissionAudit,
28 DirectoryPermissionAudit,
29 NoReadWriteForOther,
30 TemplatedFile,
31)
32from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
33from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
34from charmhelpers.contrib.hardening import utils
35
36
37def get_audits():
38 """Get Apache hardening config audits.
39
40 :returns: dictionary of audits
41 """
42 if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
43 log("Apache server does not appear to be installed on this node - "
44 "skipping apache hardening", level=INFO)
45 return []
46
47 context = ApacheConfContext()
48 settings = utils.get_settings('apache')
49 audits = [
50 FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root',
51 group='root', mode=0o0640),
52
53 TemplatedFile(os.path.join(settings['common']['apache_dir'],
54 'mods-available/alias.conf'),
55 context,
56 TEMPLATES_DIR,
57 mode=0o0755,
58 user='root',
59 service_actions=[{'service': 'apache2',
60 'actions': ['restart']}]),
61
62 TemplatedFile(os.path.join(settings['common']['apache_dir'],
63 'conf-enabled/hardening.conf'),
64 context,
65 TEMPLATES_DIR,
66 mode=0o0640,
67 user='root',
68 service_actions=[{'service': 'apache2',
69 'actions': ['restart']}]),
70
71 DirectoryPermissionAudit(settings['common']['apache_dir'],
72 user='root',
73 group='root',
74 mode=0o640),
75
76 DisabledModuleAudit(settings['hardening']['modules_to_disable']),
77
78 NoReadWriteForOther(settings['common']['apache_dir']),
79 ]
80
81 return audits
82
83
84class ApacheConfContext(object):
85 """Defines the set of key/value pairs to set in a apache config file.
86
87 This context, when called, will return a dictionary containing the
88 key/value pairs of setting to specify in the
89 /etc/apache/conf-enabled/hardening.conf file.
90 """
91 def __call__(self):
92 settings = utils.get_settings('apache')
93 ctxt = settings['hardening']
94
95 out = subprocess.check_output(['apache2', '-v'])
96 ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
97 out).group(1)
98 ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
99 ctxt['traceenable'] = settings['hardening']['traceenable']
100 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/audits/__init__.py b/hooks/charmhelpers/contrib/hardening/audits/__init__.py
new file mode 100644
index 0000000..6a7057b
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/audits/__init__.py
@@ -0,0 +1,63 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17
18class BaseAudit(object): # NO-QA
19 """Base class for hardening checks.
20
21 The lifecycle of a hardening check is to first check to see if the system
22 is in compliance for the specified check. If it is not in compliance, the
23 check method will return a value which will be supplied to the.
24 """
25 def __init__(self, *args, **kwargs):
26 self.unless = kwargs.get('unless', None)
27 super(BaseAudit, self).__init__()
28
29 def ensure_compliance(self):
30 """Checks to see if the current hardening check is in compliance or
31 not.
32
33 If the check that is performed is not in compliance, then an exception
34 should be raised.
35 """
36 pass
37
38 def _take_action(self):
39 """Determines whether to perform the action or not.
40
41 Checks whether or not an action should be taken. This is determined by
42 the truthy value for the unless parameter. If unless is a callback
43 method, it will be invoked with no parameters in order to determine
44 whether or not the action should be taken. Otherwise, the truthy value
45 of the unless attribute will determine if the action should be
46 performed.
47 """
48 # Do the action if there isn't an unless override.
49 if self.unless is None:
50 return True
51
52 # Invoke the callback if there is one.
53 if hasattr(self.unless, '__call__'):
54 results = self.unless()
55 if results:
56 return False
57 else:
58 return True
59
60 if self.unless:
61 return False
62 else:
63 return True
diff --git a/hooks/charmhelpers/contrib/hardening/audits/apache.py b/hooks/charmhelpers/contrib/hardening/audits/apache.py
new file mode 100644
index 0000000..cf3c987
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/audits/apache.py
@@ -0,0 +1,100 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import re
18import subprocess
19
20from six import string_types
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO,
25 ERROR,
26)
27
28from charmhelpers.contrib.hardening.audits import BaseAudit
29
30
31class DisabledModuleAudit(BaseAudit):
32 """Audits Apache2 modules.
33
34 Determines if the apache2 modules are enabled. If the modules are enabled
35 then they are removed in the ensure_compliance.
36 """
37 def __init__(self, modules):
38 if modules is None:
39 self.modules = []
40 elif isinstance(modules, string_types):
41 self.modules = [modules]
42 else:
43 self.modules = modules
44
45 def ensure_compliance(self):
46 """Ensures that the modules are not loaded."""
47 if not self.modules:
48 return
49
50 try:
51 loaded_modules = self._get_loaded_modules()
52 non_compliant_modules = []
53 for module in self.modules:
54 if module in loaded_modules:
55 log("Module '%s' is enabled but should not be." %
56 (module), level=INFO)
57 non_compliant_modules.append(module)
58
59 if len(non_compliant_modules) == 0:
60 return
61
62 for module in non_compliant_modules:
63 self._disable_module(module)
64 self._restart_apache()
65 except subprocess.CalledProcessError as e:
66 log('Error occurred auditing apache module compliance. '
67 'This may have been already reported. '
68 'Output is: %s' % e.output, level=ERROR)
69
70 @staticmethod
71 def _get_loaded_modules():
72 """Returns the modules which are enabled in Apache."""
73 output = subprocess.check_output(['apache2ctl', '-M'])
74 modules = []
75 for line in output.strip().split():
76 # Each line of the enabled module output looks like:
77 # module_name (static|shared)
78 # Plus a header line at the top of the output which is stripped
79 # out by the regex.
80 matcher = re.search(r'^ (\S*)', line)
81 if matcher:
82 modules.append(matcher.group(1))
83 return modules
84
85 @staticmethod
86 def _disable_module(module):
87 """Disables the specified module in Apache."""
88 try:
89 subprocess.check_call(['a2dismod', module])
90 except subprocess.CalledProcessError as e:
91 # Note: catch error here to allow the attempt of disabling
92 # multiple modules in one go rather than failing after the
93 # first module fails.
94 log('Error occurred disabling module %s. '
95 'Output is: %s' % (module, e.output), level=ERROR)
96
97 @staticmethod
98 def _restart_apache():
99 """Restarts the apache process"""
100 subprocess.check_output(['service', 'apache2', 'restart'])
diff --git a/hooks/charmhelpers/contrib/hardening/audits/apt.py b/hooks/charmhelpers/contrib/hardening/audits/apt.py
new file mode 100644
index 0000000..e94af03
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/audits/apt.py
@@ -0,0 +1,105 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from __future__ import absolute_import # required for external apt import
18from apt import apt_pkg
19from six import string_types
20
21from charmhelpers.fetch import (
22 apt_cache,
23 apt_purge
24)
25from charmhelpers.core.hookenv import (
26 log,
27 DEBUG,
28 WARNING,
29)
30from charmhelpers.contrib.hardening.audits import BaseAudit
31
32
33class AptConfig(BaseAudit):
34
35 def __init__(self, config, **kwargs):
36 self.config = config
37
38 def verify_config(self):
39 apt_pkg.init()
40 for cfg in self.config:
41 value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
42 if value and value != cfg['expected']:
43 log("APT config '%s' has unexpected value '%s' "
44 "(expected='%s')" %
45 (cfg['key'], value, cfg['expected']), level=WARNING)
46
47 def ensure_compliance(self):
48 self.verify_config()
49
50
51class RestrictedPackages(BaseAudit):
52 """Class used to audit restricted packages on the system."""
53
54 def __init__(self, pkgs, **kwargs):
55 super(RestrictedPackages, self).__init__(**kwargs)
56 if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
57 self.pkgs = [pkgs]
58 else:
59 self.pkgs = pkgs
60
61 def ensure_compliance(self):
62 cache = apt_cache()
63
64 for p in self.pkgs:
65 if p not in cache:
66 continue
67
68 pkg = cache[p]
69 if not self.is_virtual_package(pkg):
70 if not pkg.current_ver:
71 log("Package '%s' is not installed." % pkg.name,
72 level=DEBUG)
73 continue
74 else:
75 log("Restricted package '%s' is installed" % pkg.name,
76 level=WARNING)
77 self.delete_package(cache, pkg)
78 else:
79 log("Checking restricted virtual package '%s' provides" %
80 pkg.name, level=DEBUG)
81 self.delete_package(cache, pkg)
82
83 def delete_package(self, cache, pkg):
84 """Deletes the package from the system.
85
86 Deletes the package form the system, properly handling virtual
87 packages.
88
89 :param cache: the apt cache
90 :param pkg: the package to remove
91 """
92 if self.is_virtual_package(pkg):
93 log("Package '%s' appears to be virtual - purging provides" %
94 pkg.name, level=DEBUG)
95 for _p in pkg.provides_list:
96 self.delete_package(cache, _p[2].parent_pkg)
97 elif not pkg.current_ver:
98 log("Package '%s' not installed" % pkg.name, level=DEBUG)
99 return
100 else:
101 log("Purging package '%s'" % pkg.name, level=DEBUG)
102 apt_purge(pkg.name)
103
104 def is_virtual_package(self, pkg):
105 return pkg.has_provides and not pkg.has_versions
diff --git a/hooks/charmhelpers/contrib/hardening/audits/file.py b/hooks/charmhelpers/contrib/hardening/audits/file.py
new file mode 100644
index 0000000..0fb545a
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/audits/file.py
@@ -0,0 +1,552 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import grp
18import os
19import pwd
20import re
21
22from subprocess import (
23 CalledProcessError,
24 check_output,
25 check_call,
26)
27from traceback import format_exc
28from six import string_types
29from stat import (
30 S_ISGID,
31 S_ISUID
32)
33
34from charmhelpers.core.hookenv import (
35 log,
36 DEBUG,
37 INFO,
38 WARNING,
39 ERROR,
40)
41from charmhelpers.core import unitdata
42from charmhelpers.core.host import file_hash
43from charmhelpers.contrib.hardening.audits import BaseAudit
44from charmhelpers.contrib.hardening.templating import (
45 get_template_path,
46 render_and_write,
47)
48from charmhelpers.contrib.hardening import utils
49
50
51class BaseFileAudit(BaseAudit):
52 """Base class for file audits.
53
54 Provides api stubs for compliance check flow that must be used by any class
55 that implemented this one.
56 """
57
58 def __init__(self, paths, always_comply=False, *args, **kwargs):
59 """
60 :param paths: string path of list of paths of files we want to apply
61 compliance checks are criteria to.
62 :param always_comply: if true compliance criteria is always applied
63 else compliance is skipped for non-existent
64 paths.
65 """
66 super(BaseFileAudit, self).__init__(*args, **kwargs)
67 self.always_comply = always_comply
68 if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
69 self.paths = [paths]
70 else:
71 self.paths = paths
72
73 def ensure_compliance(self):
74 """Ensure that the all registered files comply to registered criteria.
75 """
76 for p in self.paths:
77 if os.path.exists(p):
78 if self.is_compliant(p):
79 continue
80
81 log('File %s is not in compliance.' % p, level=INFO)
82 else:
83 if not self.always_comply:
84 log("Non-existent path '%s' - skipping compliance check"
85 % (p), level=INFO)
86 continue
87
88 if self._take_action():
89 log("Applying compliance criteria to '%s'" % (p), level=INFO)
90 self.comply(p)
91
92 def is_compliant(self, path):
93 """Audits the path to see if it is compliance.
94
95 :param path: the path to the file that should be checked.
96 """
97 raise NotImplementedError
98
99 def comply(self, path):
100 """Enforces the compliance of a path.
101
102 :param path: the path to the file that should be enforced.
103 """
104 raise NotImplementedError
105
106 @classmethod
107 def _get_stat(cls, path):
108 """Returns the Posix st_stat information for the specified file path.
109
110 :param path: the path to get the st_stat information for.
111 :returns: an st_stat object for the path or None if the path doesn't
112 exist.
113 """
114 return os.stat(path)
115
116
117class FilePermissionAudit(BaseFileAudit):
118 """Implements an audit for file permissions and ownership for a user.
119
120 This class implements functionality that ensures that a specific user/group
121 will own the file(s) specified and that the permissions specified are
122 applied properly to the file.
123 """
124 def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
125 self.user = user
126 self.group = group
127 self.mode = mode
128 super(FilePermissionAudit, self).__init__(paths, user, group, mode,
129 **kwargs)
130
131 @property
132 def user(self):
133 return self._user
134
135 @user.setter
136 def user(self, name):
137 try:
138 user = pwd.getpwnam(name)
139 except KeyError:
140 log('Unknown user %s' % name, level=ERROR)
141 user = None
142 self._user = user
143
144 @property
145 def group(self):
146 return self._group
147
148 @group.setter
149 def group(self, name):
150 try:
151 group = None
152 if name:
153 group = grp.getgrnam(name)
154 else:
155 group = grp.getgrgid(self.user.pw_gid)
156 except KeyError:
157 log('Unknown group %s' % name, level=ERROR)
158 self._group = group
159
160 def is_compliant(self, path):
161 """Checks if the path is in compliance.
162
163 Used to determine if the path specified meets the necessary
164 requirements to be in compliance with the check itself.
165
166 :param path: the file path to check
167 :returns: True if the path is compliant, False otherwise.
168 """
169 stat = self._get_stat(path)
170 user = self.user
171 group = self.group
172
173 compliant = True
174 if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
175 log('File %s is not owned by %s:%s.' % (path, user.pw_name,
176 group.gr_name),
177 level=INFO)
178 compliant = False
179
180 # POSIX refers to the st_mode bits as corresponding to both the
181 # file type and file permission bits, where the least significant 12
182 # bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
183 # file permission bits (8-0)
184 perms = stat.st_mode & 0o7777
185 if perms != self.mode:
186 log('File %s has incorrect permissions, currently set to %s' %
187 (path, oct(stat.st_mode & 0o7777)), level=INFO)
188 compliant = False
189
190 return compliant
191
192 def comply(self, path):
193 """Issues a chown and chmod to the file paths specified."""
194 utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
195 self.mode)
196
197
198class DirectoryPermissionAudit(FilePermissionAudit):
199 """Performs a permission check for the specified directory path."""
200
201 def __init__(self, paths, user, group=None, mode=0o600,
202 recursive=True, **kwargs):
203 super(DirectoryPermissionAudit, self).__init__(paths, user, group,
204 mode, **kwargs)
205 self.recursive = recursive
206
207 def is_compliant(self, path):
208 """Checks if the directory is compliant.
209
210 Used to determine if the path specified and all of its children
211 directories are in compliance with the check itself.
212
213 :param path: the directory path to check
214 :returns: True if the directory tree is compliant, otherwise False.
215 """
216 if not os.path.isdir(path):
217 log('Path specified %s is not a directory.' % path, level=ERROR)
218 raise ValueError("%s is not a directory." % path)
219
220 if not self.recursive:
221 return super(DirectoryPermissionAudit, self).is_compliant(path)
222
223 compliant = True
224 for root, dirs, _ in os.walk(path):
225 if len(dirs) > 0:
226 continue
227
228 if not super(DirectoryPermissionAudit, self).is_compliant(root):
229 compliant = False
230 continue
231
232 return compliant
233
234 def comply(self, path):
235 for root, dirs, _ in os.walk(path):
236 if len(dirs) > 0:
237 super(DirectoryPermissionAudit, self).comply(root)
238
239
240class ReadOnly(BaseFileAudit):
241 """Audits that files and folders are read only."""
242 def __init__(self, paths, *args, **kwargs):
243 super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
244
245 def is_compliant(self, path):
246 try:
247 output = check_output(['find', path, '-perm', '-go+w',
248 '-type', 'f']).strip()
249
250 # The find above will find any files which have permission sets
251 # which allow too broad of write access. As such, the path is
252 # compliant if there is no output.
253 if output:
254 return False
255
256 return True
257 except CalledProcessError as e:
258 log('Error occurred checking finding writable files for %s. '
259 'Error information is: command %s failed with returncode '
260 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
261 format_exc(e)), level=ERROR)
262 return False
263
264 def comply(self, path):
265 try:
266 check_output(['chmod', 'go-w', '-R', path])
267 except CalledProcessError as e:
268 log('Error occurred removing writeable permissions for %s. '
269 'Error information is: command %s failed with returncode '
270 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
271 format_exc(e)), level=ERROR)
272
273
274class NoReadWriteForOther(BaseFileAudit):
275 """Ensures that the files found under the base path are readable or
276 writable by anyone other than the owner or the group.
277 """
278 def __init__(self, paths):
279 super(NoReadWriteForOther, self).__init__(paths)
280
281 def is_compliant(self, path):
282 try:
283 cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
284 '-perm', '-o+w', '-type', 'f']
285 output = check_output(cmd).strip()
286
287 # The find above here will find any files which have read or
288 # write permissions for other, meaning there is too broad of access
289 # to read/write the file. As such, the path is compliant if there's
290 # no output.
291 if output:
292 return False
293
294 return True
295 except CalledProcessError as e:
296 log('Error occurred while finding files which are readable or '
297 'writable to the world in %s. '
298 'Command output is: %s.' % (path, e.output), level=ERROR)
299
300 def comply(self, path):
301 try:
302 check_output(['chmod', '-R', 'o-rw', path])
303 except CalledProcessError as e:
304 log('Error occurred attempting to change modes of files under '
305 'path %s. Output of command is: %s' % (path, e.output))
306
307
308class NoSUIDSGIDAudit(BaseFileAudit):
309 """Audits that specified files do not have SUID/SGID bits set."""
310 def __init__(self, paths, *args, **kwargs):
311 super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
312
313 def is_compliant(self, path):
314 stat = self._get_stat(path)
315 if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
316 return False
317
318 return True
319
320 def comply(self, path):
321 try:
322 log('Removing suid/sgid from %s.' % path, level=DEBUG)
323 check_output(['chmod', '-s', path])
324 except CalledProcessError as e:
325 log('Error occurred removing suid/sgid from %s.'
326 'Error information is: command %s failed with returncode '
327 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
328 format_exc(e)), level=ERROR)
329
330
331class TemplatedFile(BaseFileAudit):
332 """The TemplatedFileAudit audits the contents of a templated file.
333
334 This audit renders a file from a template, sets the appropriate file
335 permissions, then generates a hashsum with which to check the content
336 changed.
337 """
338 def __init__(self, path, context, template_dir, mode, user='root',
339 group='root', service_actions=None, **kwargs):
340 self.context = context
341 self.user = user
342 self.group = group
343 self.mode = mode
344 self.template_dir = template_dir
345 self.service_actions = service_actions
346 super(TemplatedFile, self).__init__(paths=path, always_comply=True,
347 **kwargs)
348
349 def is_compliant(self, path):
350 """Determines if the templated file is compliant.
351
352 A templated file is only compliant if it has not changed (as
353 determined by its sha256 hashsum) AND its file permissions are set
354 appropriately.
355
356 :param path: the path to check compliance.
357 """
358 same_templates = self.templates_match(path)
359 same_content = self.contents_match(path)
360 same_permissions = self.permissions_match(path)
361
362 if same_content and same_permissions and same_templates:
363 return True
364
365 return False
366
367 def run_service_actions(self):
368 """Run any actions on services requested."""
369 if not self.service_actions:
370 return
371
372 for svc_action in self.service_actions:
373 name = svc_action['service']
374 actions = svc_action['actions']
375 log("Running service '%s' actions '%s'" % (name, actions),
376 level=DEBUG)
377 for action in actions:
378 cmd = ['service', name, action]
379 try:
380 check_call(cmd)
381 except CalledProcessError as exc:
382 log("Service name='%s' action='%s' failed - %s" %
383 (name, action, exc), level=WARNING)
384
385 def comply(self, path):
386 """Ensures the contents and the permissions of the file.
387
388 :param path: the path to correct
389 """
390 dirname = os.path.dirname(path)
391 if not os.path.exists(dirname):
392 os.makedirs(dirname)
393
394 self.pre_write()
395 render_and_write(self.template_dir, path, self.context())
396 utils.ensure_permissions(path, self.user, self.group, self.mode)
397 self.run_service_actions()
398 self.save_checksum(path)
399 self.post_write()
400
401 def pre_write(self):
402 """Invoked prior to writing the template."""
403 pass
404
405 def post_write(self):
406 """Invoked after writing the template."""
407 pass
408
409 def templates_match(self, path):
410 """Determines if the template files are the same.
411
412 The template file equality is determined by the hashsum of the
413 template files themselves. If there is no hashsum, then the content
414 cannot be sure to be the same so treat it as if they changed.
415 Otherwise, return whether or not the hashsums are the same.
416
417 :param path: the path to check
418 :returns: boolean
419 """
420 template_path = get_template_path(self.template_dir, path)
421 key = 'hardening:template:%s' % template_path
422 template_checksum = file_hash(template_path)
423 kv = unitdata.kv()
424 stored_tmplt_checksum = kv.get(key)
425 if not stored_tmplt_checksum:
426 kv.set(key, template_checksum)
427 kv.flush()
428 log('Saved template checksum for %s.' % template_path,
429 level=DEBUG)
430 # Since we don't have a template checksum, then assume it doesn't
431 # match and return that the template is different.
432 return False
433 elif stored_tmplt_checksum != template_checksum:
434 kv.set(key, template_checksum)
435 kv.flush()
436 log('Updated template checksum for %s.' % template_path,
437 level=DEBUG)
438 return False
439
440 # Here the template hasn't changed based upon the calculated
441 # checksum of the template and what was previously stored.
442 return True
443
444 def contents_match(self, path):
445 """Determines if the file content is the same.
446
447 This is determined by comparing hashsum of the file contents and
448 the saved hashsum. If there is no hashsum, then the content cannot
449 be sure to be the same so treat them as if they are not the same.
450 Otherwise, return True if the hashsums are the same, False if they
451 are not the same.
452
453 :param path: the file to check.
454 """
455 checksum = file_hash(path)
456
457 kv = unitdata.kv()
458 stored_checksum = kv.get('hardening:%s' % path)
459 if not stored_checksum:
460 # If the checksum hasn't been generated, return False to ensure
461 # the file is written and the checksum stored.
462 log('Checksum for %s has not been calculated.' % path, level=DEBUG)
463 return False
464 elif stored_checksum != checksum:
465 log('Checksum mismatch for %s.' % path, level=DEBUG)
466 return False
467
468 return True
469
470 def permissions_match(self, path):
471 """Determines if the file owner and permissions match.
472
473 :param path: the path to check.
474 """
475 audit = FilePermissionAudit(path, self.user, self.group, self.mode)
476 return audit.is_compliant(path)
477
478 def save_checksum(self, path):
479 """Calculates and saves the checksum for the path specified.
480
481 :param path: the path of the file to save the checksum.
482 """
483 checksum = file_hash(path)
484 kv = unitdata.kv()
485 kv.set('hardening:%s' % path, checksum)
486 kv.flush()
487
488
489class DeletedFile(BaseFileAudit):
490 """Audit to ensure that a file is deleted."""
491 def __init__(self, paths):
492 super(DeletedFile, self).__init__(paths)
493
494 def is_compliant(self, path):
495 return not os.path.exists(path)
496
497 def comply(self, path):
498 os.remove(path)
499
500
501class FileContentAudit(BaseFileAudit):
502 """Audit the contents of a file."""
503 def __init__(self, paths, cases, **kwargs):
504 # Cases we expect to pass
505 self.pass_cases = cases.get('pass', [])
506 # Cases we expect to fail
507 self.fail_cases = cases.get('fail', [])
508 super(FileContentAudit, self).__init__(paths, **kwargs)
509
510 def is_compliant(self, path):
511 """
512 Given a set of content matching cases i.e. tuple(regex, bool) where
513 bool value denotes whether or not regex is expected to match, check that
514 all cases match as expected with the contents of the file. Cases can be
515 expected to pass of fail.
516
517 :param path: Path of file to check.
518 :returns: Boolean value representing whether or not all cases are
519 found to be compliant.
520 """
521 log("Auditing contents of file '%s'" % (path), level=DEBUG)
522 with open(path, 'r') as fd:
523 contents = fd.read()
524
525 matches = 0
526 for pattern in self.pass_cases:
527 key = re.compile(pattern, flags=re.MULTILINE)
528 results = re.search(key, contents)
529 if results:
530 matches += 1
531 else:
532 log("Pattern '%s' was expected to pass but instead it failed"
533 % (pattern), level=WARNING)
534
535 for pattern in self.fail_cases:
536 key = re.compile(pattern, flags=re.MULTILINE)
537 results = re.search(key, contents)
538 if not results:
539 matches += 1
540 else:
541 log("Pattern '%s' was expected to fail but instead it passed"
542 % (pattern), level=WARNING)
543
544 total = len(self.pass_cases) + len(self.fail_cases)
545 log("Checked %s cases and %s passed" % (total, matches), level=DEBUG)
546 return matches == total
547
548 def comply(self, *args, **kwargs):
549 """NOOP since we just issue warnings. This is to avoid the
550 NotImplememtedError.
551 """
552 log("Not applying any compliance criteria, only checks.", level=INFO)
diff --git a/hooks/charmhelpers/contrib/hardening/harden.py b/hooks/charmhelpers/contrib/hardening/harden.py
new file mode 100644
index 0000000..ac7568d
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/harden.py
@@ -0,0 +1,84 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18
19from collections import OrderedDict
20
21from charmhelpers.core.hookenv import (
22 config,
23 log,
24 DEBUG,
25 WARNING,
26)
27from charmhelpers.contrib.hardening.host.checks import run_os_checks
28from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks
29from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
30from charmhelpers.contrib.hardening.apache.checks import run_apache_checks
31
32
33def harden(overrides=None):
34 """Hardening decorator.
35
36 This is the main entry point for running the hardening stack. In order to
37 run modules of the stack you must add this decorator to charm hook(s) and
38 ensure that your charm config.yaml contains the 'harden' option set to
39 one or more of the supported modules. Setting these will cause the
40 corresponding hardening code to be run when the hook fires.
41
42 This decorator can and should be applied to more than one hook or function
43 such that hardening modules are called multiple times. This is because
44 subsequent calls will perform auditing checks that will report any changes
45 to resources hardened by the first run (and possibly perform compliance
46 actions as a result of any detected infractions).
47
48 :param overrides: Optional list of stack modules used to override those
49 provided with 'harden' config.
50 :returns: Returns value returned by decorated function once executed.
51 """
52 def _harden_inner1(f):
53 log("Hardening function '%s'" % (f.__name__), level=DEBUG)
54
55 def _harden_inner2(*args, **kwargs):
56 RUN_CATALOG = OrderedDict([('os', run_os_checks),
57 ('ssh', run_ssh_checks),
58 ('mysql', run_mysql_checks),
59 ('apache', run_apache_checks)])
60
61 enabled = overrides or (config("harden") or "").split()
62 if enabled:
63 modules_to_run = []
64 # modules will always be performed in the following order
65 for module, func in six.iteritems(RUN_CATALOG):
66 if module in enabled:
67 enabled.remove(module)
68 modules_to_run.append(func)
69
70 if enabled:
71 log("Unknown hardening modules '%s' - ignoring" %
72 (', '.join(enabled)), level=WARNING)
73
74 for hardener in modules_to_run:
75 log("Executing hardening module '%s'" %
76 (hardener.__name__), level=DEBUG)
77 hardener()
78 else:
79 log("No hardening applied to '%s'" % (f.__name__), level=DEBUG)
80
81 return f(*args, **kwargs)
82 return _harden_inner2
83
84 return _harden_inner1
diff --git a/hooks/charmhelpers/contrib/hardening/host/__init__.py b/hooks/charmhelpers/contrib/hardening/host/__init__.py
new file mode 100644
index 0000000..277b8c7
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/__init__.py
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/__init__.py b/hooks/charmhelpers/contrib/hardening/host/checks/__init__.py
new file mode 100644
index 0000000..c3bd598
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/__init__.py
@@ -0,0 +1,50 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.host.checks import (
22 apt,
23 limits,
24 login,
25 minimize_access,
26 pam,
27 profile,
28 securetty,
29 suid_sgid,
30 sysctl
31)
32
33
34def run_os_checks():
35 log("Starting OS hardening checks.", level=DEBUG)
36 checks = apt.get_audits()
37 checks.extend(limits.get_audits())
38 checks.extend(login.get_audits())
39 checks.extend(minimize_access.get_audits())
40 checks.extend(pam.get_audits())
41 checks.extend(profile.get_audits())
42 checks.extend(securetty.get_audits())
43 checks.extend(suid_sgid.get_audits())
44 checks.extend(sysctl.get_audits())
45
46 for check in checks:
47 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
48 check.ensure_compliance()
49
50 log("OS hardening checks complete.", level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/apt.py b/hooks/charmhelpers/contrib/hardening/host/checks/apt.py
new file mode 100644
index 0000000..2c221cd
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/apt.py
@@ -0,0 +1,39 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.utils import get_settings
18from charmhelpers.contrib.hardening.audits.apt import (
19 AptConfig,
20 RestrictedPackages,
21)
22
23
24def get_audits():
25 """Get OS hardening apt audits.
26
27 :returns: dictionary of audits
28 """
29 audits = [AptConfig([{'key': 'APT::Get::AllowUnauthenticated',
30 'expected': 'false'}])]
31
32 settings = get_settings('os')
33 clean_packages = settings['security']['packages_clean']
34 if clean_packages:
35 security_packages = settings['security']['packages_list']
36 if security_packages:
37 audits.append(RestrictedPackages(security_packages))
38
39 return audits
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/limits.py b/hooks/charmhelpers/contrib/hardening/host/checks/limits.py
new file mode 100644
index 0000000..8ce9dc2
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/limits.py
@@ -0,0 +1,55 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import (
18 DirectoryPermissionAudit,
19 TemplatedFile,
20)
21from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
22from charmhelpers.contrib.hardening import utils
23
24
25def get_audits():
26 """Get OS hardening security limits audits.
27
28 :returns: dictionary of audits
29 """
30 audits = []
31 settings = utils.get_settings('os')
32
33 # Ensure that the /etc/security/limits.d directory is only writable
34 # by the root user, but others can execute and read.
35 audits.append(DirectoryPermissionAudit('/etc/security/limits.d',
36 user='root', group='root',
37 mode=0o755))
38
39 # If core dumps are not enabled, then don't allow core dumps to be
40 # created as they may contain sensitive information.
41 if not settings['security']['kernel_enable_core_dump']:
42 audits.append(TemplatedFile('/etc/security/limits.d/10.hardcore.conf',
43 SecurityLimitsContext(),
44 template_dir=TEMPLATES_DIR,
45 user='root', group='root', mode=0o0440))
46 return audits
47
48
49class SecurityLimitsContext(object):
50
51 def __call__(self):
52 settings = utils.get_settings('os')
53 ctxt = {'disable_core_dump':
54 not settings['security']['kernel_enable_core_dump']}
55 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/login.py b/hooks/charmhelpers/contrib/hardening/host/checks/login.py
new file mode 100644
index 0000000..d32c4f6
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/login.py
@@ -0,0 +1,67 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from six import string_types
18
19from charmhelpers.contrib.hardening.audits.file import TemplatedFile
20from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
21from charmhelpers.contrib.hardening import utils
22
23
24def get_audits():
25 """Get OS hardening login.defs audits.
26
27 :returns: dictionary of audits
28 """
29 audits = [TemplatedFile('/etc/login.defs', LoginContext(),
30 template_dir=TEMPLATES_DIR,
31 user='root', group='root', mode=0o0444)]
32 return audits
33
34
35class LoginContext(object):
36
37 def __call__(self):
38 settings = utils.get_settings('os')
39
40 # Octal numbers in yaml end up being turned into decimal,
41 # so check if the umask is entered as a string (e.g. '027')
42 # or as an octal umask as we know it (e.g. 002). If its not
43 # a string assume it to be octal and turn it into an octal
44 # string.
45 umask = settings['environment']['umask']
46 if not isinstance(umask, string_types):
47 umask = '%s' % oct(umask)
48
49 ctxt = {
50 'additional_user_paths':
51 settings['environment']['extra_user_paths'],
52 'umask': umask,
53 'pwd_max_age': settings['auth']['pw_max_age'],
54 'pwd_min_age': settings['auth']['pw_min_age'],
55 'uid_min': settings['auth']['uid_min'],
56 'sys_uid_min': settings['auth']['sys_uid_min'],
57 'sys_uid_max': settings['auth']['sys_uid_max'],
58 'gid_min': settings['auth']['gid_min'],
59 'sys_gid_min': settings['auth']['sys_gid_min'],
60 'sys_gid_max': settings['auth']['sys_gid_max'],
61 'login_retries': settings['auth']['retries'],
62 'login_timeout': settings['auth']['timeout'],
63 'chfn_restrict': settings['auth']['chfn_restrict'],
64 'allow_login_without_home': settings['auth']['allow_homeless']
65 }
66
67 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py b/hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py
new file mode 100644
index 0000000..c471064
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py
@@ -0,0 +1,52 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import (
18 FilePermissionAudit,
19 ReadOnly,
20)
21from charmhelpers.contrib.hardening import utils
22
23
24def get_audits():
25 """Get OS hardening access audits.
26
27 :returns: dictionary of audits
28 """
29 audits = []
30 settings = utils.get_settings('os')
31
32 # Remove write permissions from $PATH folders for all regular users.
33 # This prevents changing system-wide commands from normal users.
34 path_folders = {'/usr/local/sbin',
35 '/usr/local/bin',
36 '/usr/sbin',
37 '/usr/bin',
38 '/bin'}
39 extra_user_paths = settings['environment']['extra_user_paths']
40 path_folders.update(extra_user_paths)
41 audits.append(ReadOnly(path_folders))
42
43 # Only allow the root user to have access to the shadow file.
44 audits.append(FilePermissionAudit('/etc/shadow', 'root', 'root', 0o0600))
45
46 if 'change_user' not in settings['security']['users_allow']:
47 # su should only be accessible to user and group root, unless it is
48 # expressly defined to allow users to change to root via the
49 # security_users_allow config option.
50 audits.append(FilePermissionAudit('/bin/su', 'root', 'root', 0o750))
51
52 return audits
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/pam.py b/hooks/charmhelpers/contrib/hardening/host/checks/pam.py
new file mode 100644
index 0000000..383fe28
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/pam.py
@@ -0,0 +1,134 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from subprocess import (
18 check_output,
19 CalledProcessError,
20)
21
22from charmhelpers.core.hookenv import (
23 log,
24 DEBUG,
25 ERROR,
26)
27from charmhelpers.fetch import (
28 apt_install,
29 apt_purge,
30 apt_update,
31)
32from charmhelpers.contrib.hardening.audits.file import (
33 TemplatedFile,
34 DeletedFile,
35)
36from charmhelpers.contrib.hardening import utils
37from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
38
39
40def get_audits():
41 """Get OS hardening PAM authentication audits.
42
43 :returns: dictionary of audits
44 """
45 audits = []
46
47 settings = utils.get_settings('os')
48
49 if settings['auth']['pam_passwdqc_enable']:
50 audits.append(PasswdqcPAM('/etc/passwdqc.conf'))
51
52 if settings['auth']['retries']:
53 audits.append(Tally2PAM('/usr/share/pam-configs/tally2'))
54 else:
55 audits.append(DeletedFile('/usr/share/pam-configs/tally2'))
56
57 return audits
58
59
60class PasswdqcPAMContext(object):
61
62 def __call__(self):
63 ctxt = {}
64 settings = utils.get_settings('os')
65
66 ctxt['auth_pam_passwdqc_options'] = \
67 settings['auth']['pam_passwdqc_options']
68
69 return ctxt
70
71
72class PasswdqcPAM(TemplatedFile):
73 """The PAM Audit verifies the linux PAM settings."""
74 def __init__(self, path):
75 super(PasswdqcPAM, self).__init__(path=path,
76 template_dir=TEMPLATES_DIR,
77 context=PasswdqcPAMContext(),
78 user='root',
79 group='root',
80 mode=0o0640)
81
82 def pre_write(self):
83 # Always remove?
84 for pkg in ['libpam-ccreds', 'libpam-cracklib']:
85 log("Purging package '%s'" % pkg, level=DEBUG),
86 apt_purge(pkg)
87
88 apt_update(fatal=True)
89 for pkg in ['libpam-passwdqc']:
90 log("Installing package '%s'" % pkg, level=DEBUG),
91 apt_install(pkg)
92
93 def post_write(self):
94 """Updates the PAM configuration after the file has been written"""
95 try:
96 check_output(['pam-auth-update', '--package'])
97 except CalledProcessError as e:
98 log('Error calling pam-auth-update: %s' % e, level=ERROR)
99
100
101class Tally2PAMContext(object):
102
103 def __call__(self):
104 ctxt = {}
105 settings = utils.get_settings('os')
106
107 ctxt['auth_lockout_time'] = settings['auth']['lockout_time']
108 ctxt['auth_retries'] = settings['auth']['retries']
109
110 return ctxt
111
112
113class Tally2PAM(TemplatedFile):
114 """The PAM Audit verifies the linux PAM settings."""
115 def __init__(self, path):
116 super(Tally2PAM, self).__init__(path=path,
117 template_dir=TEMPLATES_DIR,
118 context=Tally2PAMContext(),
119 user='root',
120 group='root',
121 mode=0o0640)
122
123 def pre_write(self):
124 # Always remove?
125 apt_purge('libpam-ccreds')
126 apt_update(fatal=True)
127 apt_install('libpam-modules')
128
129 def post_write(self):
130 """Updates the PAM configuration after the file has been written"""
131 try:
132 check_output(['pam-auth-update', '--package'])
133 except CalledProcessError as e:
134 log('Error calling pam-auth-update: %s' % e, level=ERROR)
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/profile.py b/hooks/charmhelpers/contrib/hardening/host/checks/profile.py
new file mode 100644
index 0000000..f744335
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/profile.py
@@ -0,0 +1,45 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19from charmhelpers.contrib.hardening import utils
20
21
22def get_audits():
23 """Get OS hardening profile audits.
24
25 :returns: dictionary of audits
26 """
27 audits = []
28
29 settings = utils.get_settings('os')
30
31 # If core dumps are not enabled, then don't allow core dumps to be
32 # created as they may contain sensitive information.
33 if not settings['security']['kernel_enable_core_dump']:
34 audits.append(TemplatedFile('/etc/profile.d/pinerolo_profile.sh',
35 ProfileContext(),
36 template_dir=TEMPLATES_DIR,
37 mode=0o0755, user='root', group='root'))
38 return audits
39
40
41class ProfileContext(object):
42
43 def __call__(self):
44 ctxt = {}
45 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/securetty.py b/hooks/charmhelpers/contrib/hardening/host/checks/securetty.py
new file mode 100644
index 0000000..e33c73c
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/securetty.py
@@ -0,0 +1,39 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19from charmhelpers.contrib.hardening import utils
20
21
22def get_audits():
23 """Get OS hardening Secure TTY audits.
24
25 :returns: dictionary of audits
26 """
27 audits = []
28 audits.append(TemplatedFile('/etc/securetty', SecureTTYContext(),
29 template_dir=TEMPLATES_DIR,
30 mode=0o0400, user='root', group='root'))
31 return audits
32
33
34class SecureTTYContext(object):
35
36 def __call__(self):
37 settings = utils.get_settings('os')
38 ctxt = {'ttys': settings['auth']['root_ttys']}
39 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py b/hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py
new file mode 100644
index 0000000..0534689
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py
@@ -0,0 +1,131 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import subprocess
18
19from charmhelpers.core.hookenv import (
20 log,
21 INFO,
22)
23from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit
24from charmhelpers.contrib.hardening import utils
25
26
27BLACKLIST = ['/usr/bin/rcp', '/usr/bin/rlogin', '/usr/bin/rsh',
28 '/usr/libexec/openssh/ssh-keysign',
29 '/usr/lib/openssh/ssh-keysign',
30 '/sbin/netreport',
31 '/usr/sbin/usernetctl',
32 '/usr/sbin/userisdnctl',
33 '/usr/sbin/pppd',
34 '/usr/bin/lockfile',
35 '/usr/bin/mail-lock',
36 '/usr/bin/mail-unlock',
37 '/usr/bin/mail-touchlock',
38 '/usr/bin/dotlockfile',
39 '/usr/bin/arping',
40 '/usr/sbin/uuidd',
41 '/usr/bin/mtr',
42 '/usr/lib/evolution/camel-lock-helper-1.2',
43 '/usr/lib/pt_chown',
44 '/usr/lib/eject/dmcrypt-get-device',
45 '/usr/lib/mc/cons.saver']
46
47WHITELIST = ['/bin/mount', '/bin/ping', '/bin/su', '/bin/umount',
48 '/sbin/pam_timestamp_check', '/sbin/unix_chkpwd', '/usr/bin/at',
49 '/usr/bin/gpasswd', '/usr/bin/locate', '/usr/bin/newgrp',
50 '/usr/bin/passwd', '/usr/bin/ssh-agent',
51 '/usr/libexec/utempter/utempter', '/usr/sbin/lockdev',
52 '/usr/sbin/sendmail.sendmail', '/usr/bin/expiry',
53 '/bin/ping6', '/usr/bin/traceroute6.iputils',
54 '/sbin/mount.nfs', '/sbin/umount.nfs',
55 '/sbin/mount.nfs4', '/sbin/umount.nfs4',
56 '/usr/bin/crontab',
57 '/usr/bin/wall', '/usr/bin/write',
58 '/usr/bin/screen',
59 '/usr/bin/mlocate',
60 '/usr/bin/chage', '/usr/bin/chfn', '/usr/bin/chsh',
61 '/bin/fusermount',
62 '/usr/bin/pkexec',
63 '/usr/bin/sudo', '/usr/bin/sudoedit',
64 '/usr/sbin/postdrop', '/usr/sbin/postqueue',
65 '/usr/sbin/suexec',
66 '/usr/lib/squid/ncsa_auth', '/usr/lib/squid/pam_auth',
67 '/usr/kerberos/bin/ksu',
68 '/usr/sbin/ccreds_validate',
69 '/usr/bin/Xorg',
70 '/usr/bin/X',
71 '/usr/lib/dbus-1.0/dbus-daemon-launch-helper',
72 '/usr/lib/vte/gnome-pty-helper',
73 '/usr/lib/libvte9/gnome-pty-helper',
74 '/usr/lib/libvte-2.90-9/gnome-pty-helper']
75
76
77def get_audits():
78 """Get OS hardening suid/sgid audits.
79
80 :returns: dictionary of audits
81 """
82 checks = []
83 settings = utils.get_settings('os')
84 if not settings['security']['suid_sgid_enforce']:
85 log("Skipping suid/sgid hardening", level=INFO)
86 return checks
87
88 # Build the blacklist and whitelist of files for suid/sgid checks.
89 # There are a total of 4 lists:
90 # 1. the system blacklist
91 # 2. the system whitelist
92 # 3. the user blacklist
93 # 4. the user whitelist
94 #
95 # The blacklist is the set of paths which should NOT have the suid/sgid bit
96 # set and the whitelist is the set of paths which MAY have the suid/sgid
97 # bit setl. The user whitelist/blacklist effectively override the system
98 # whitelist/blacklist.
99 u_b = settings['security']['suid_sgid_blacklist']
100 u_w = settings['security']['suid_sgid_whitelist']
101
102 blacklist = set(BLACKLIST) - set(u_w + u_b)
103 whitelist = set(WHITELIST) - set(u_b + u_w)
104
105 checks.append(NoSUIDSGIDAudit(blacklist))
106
107 dry_run = settings['security']['suid_sgid_dry_run_on_unknown']
108
109 if settings['security']['suid_sgid_remove_from_unknown'] or dry_run:
110 # If the policy is a dry_run (e.g. complain only) or remove unknown
111 # suid/sgid bits then find all of the paths which have the suid/sgid
112 # bit set and then remove the whitelisted paths.
113 root_path = settings['environment']['root_path']
114 unknown_paths = find_paths_with_suid_sgid(root_path) - set(whitelist)
115 checks.append(NoSUIDSGIDAudit(unknown_paths, unless=dry_run))
116
117 return checks
118
119
120def find_paths_with_suid_sgid(root_path):
121 """Finds all paths/files which have an suid/sgid bit enabled.
122
123 Starting with the root_path, this will recursively find all paths which
124 have an suid or sgid bit set.
125 """
126 cmd = ['find', root_path, '-perm', '-4000', '-o', '-perm', '-2000',
127 '-type', 'f', '!', '-path', '/proc/*', '-print']
128
129 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
130 out, _ = p.communicate()
131 return set(out.split('\n'))
diff --git a/hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py b/hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py
new file mode 100644
index 0000000..4a76d74
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py
@@ -0,0 +1,211 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import platform
19import re
20import six
21import subprocess
22
23from charmhelpers.core.hookenv import (
24 log,
25 INFO,
26 WARNING,
27)
28from charmhelpers.contrib.hardening import utils
29from charmhelpers.contrib.hardening.audits.file import (
30 FilePermissionAudit,
31 TemplatedFile,
32)
33from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
34
35
36SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s
37net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s
38net.ipv4.conf.all.rp_filter=1
39net.ipv4.conf.default.rp_filter=1
40net.ipv4.icmp_echo_ignore_broadcasts=1
41net.ipv4.icmp_ignore_bogus_error_responses=1
42net.ipv4.icmp_ratelimit=100
43net.ipv4.icmp_ratemask=88089
44net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s
45net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s
46net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s
47net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s
48net.ipv4.tcp_rfc1337=1
49net.ipv4.tcp_syncookies=1
50net.ipv4.conf.all.shared_media=1
51net.ipv4.conf.default.shared_media=1
52net.ipv4.conf.all.accept_source_route=0
53net.ipv4.conf.default.accept_source_route=0
54net.ipv4.conf.all.accept_redirects=0
55net.ipv4.conf.default.accept_redirects=0
56net.ipv6.conf.all.accept_redirects=0
57net.ipv6.conf.default.accept_redirects=0
58net.ipv4.conf.all.secure_redirects=0
59net.ipv4.conf.default.secure_redirects=0
60net.ipv4.conf.all.send_redirects=0
61net.ipv4.conf.default.send_redirects=0
62net.ipv4.conf.all.log_martians=0
63net.ipv6.conf.default.router_solicitations=0
64net.ipv6.conf.default.accept_ra_rtr_pref=0
65net.ipv6.conf.default.accept_ra_pinfo=0
66net.ipv6.conf.default.accept_ra_defrtr=0
67net.ipv6.conf.default.autoconf=0
68net.ipv6.conf.default.dad_transmits=0
69net.ipv6.conf.default.max_addresses=1
70net.ipv6.conf.all.accept_ra=0
71net.ipv6.conf.default.accept_ra=0
72kernel.modules_disabled=%(kernel_modules_disabled)s
73kernel.sysrq=%(kernel_sysrq)s
74fs.suid_dumpable=%(fs_suid_dumpable)s
75kernel.randomize_va_space=2
76"""
77
78
79def get_audits():
80 """Get OS hardening sysctl audits.
81
82 :returns: dictionary of audits
83 """
84 audits = []
85 settings = utils.get_settings('os')
86
87 # Apply the sysctl settings which are configured to be applied.
88 audits.append(SysctlConf())
89 # Make sure that only root has access to the sysctl.conf file, and
90 # that it is read-only.
91 audits.append(FilePermissionAudit('/etc/sysctl.conf',
92 user='root',
93 group='root', mode=0o0440))
94 # If module loading is not enabled, then ensure that the modules
95 # file has the appropriate permissions and rebuild the initramfs
96 if not settings['security']['kernel_enable_module_loading']:
97 audits.append(ModulesTemplate())
98
99 return audits
100
101
102class ModulesContext(object):
103
104 def __call__(self):
105 settings = utils.get_settings('os')
106 with open('/proc/cpuinfo', 'r') as fd:
107 cpuinfo = fd.readlines()
108
109 for line in cpuinfo:
110 match = re.search(r"^vendor_id\s+:\s+(.+)", line)
111 if match:
112 vendor = match.group(1)
113
114 if vendor == "GenuineIntel":
115 vendor = "intel"
116 elif vendor == "AuthenticAMD":
117 vendor = "amd"
118
119 ctxt = {'arch': platform.processor(),
120 'cpuVendor': vendor,
121 'desktop_enable': settings['general']['desktop_enable']}
122
123 return ctxt
124
125
126class ModulesTemplate(object):
127
128 def __init__(self):
129 super(ModulesTemplate, self).__init__('/etc/initramfs-tools/modules',
130 ModulesContext(),
131 templates_dir=TEMPLATES_DIR,
132 user='root', group='root',
133 mode=0o0440)
134
135 def post_write(self):
136 subprocess.check_call(['update-initramfs', '-u'])
137
138
139class SysCtlHardeningContext(object):
140 def __call__(self):
141 settings = utils.get_settings('os')
142 ctxt = {'sysctl': {}}
143
144 log("Applying sysctl settings", level=INFO)
145 extras = {'net_ipv4_ip_forward': 0,
146 'net_ipv6_conf_all_forwarding': 0,
147 'net_ipv6_conf_all_disable_ipv6': 1,
148 'net_ipv4_tcp_timestamps': 0,
149 'net_ipv4_conf_all_arp_ignore': 0,
150 'net_ipv4_conf_all_arp_announce': 0,
151 'kernel_sysrq': 0,
152 'fs_suid_dumpable': 0,
153 'kernel_modules_disabled': 1}
154
155 if settings['sysctl']['ipv6_enable']:
156 extras['net_ipv6_conf_all_disable_ipv6'] = 0
157
158 if settings['sysctl']['forwarding']:
159 extras['net_ipv4_ip_forward'] = 1
160 extras['net_ipv6_conf_all_forwarding'] = 1
161
162 if settings['sysctl']['arp_restricted']:
163 extras['net_ipv4_conf_all_arp_ignore'] = 1
164 extras['net_ipv4_conf_all_arp_announce'] = 2
165
166 if settings['security']['kernel_enable_module_loading']:
167 extras['kernel_modules_disabled'] = 0
168
169 if settings['sysctl']['kernel_enable_sysrq']:
170 sysrq_val = settings['sysctl']['kernel_secure_sysrq']
171 extras['kernel_sysrq'] = sysrq_val
172
173 if settings['security']['kernel_enable_core_dump']:
174 extras['fs_suid_dumpable'] = 1
175
176 settings.update(extras)
177 for d in (SYSCTL_DEFAULTS % settings).split():
178 d = d.strip().partition('=')
179 key = d[0].strip()
180 path = os.path.join('/proc/sys', key.replace('.', '/'))
181 if not os.path.exists(path):
182 log("Skipping '%s' since '%s' does not exist" % (key, path),
183 level=WARNING)
184 continue
185
186 ctxt['sysctl'][key] = d[2] or None
187
188 # Translate for python3
189 return {'sysctl_settings':
190 [(k, v) for k, v in six.iteritems(ctxt['sysctl'])]}
191
192
193class SysctlConf(TemplatedFile):
194 """An audit check for sysctl settings."""
195 def __init__(self):
196 self.conffile = '/etc/sysctl.d/99-juju-hardening.conf'
197 super(SysctlConf, self).__init__(self.conffile,
198 SysCtlHardeningContext(),
199 template_dir=TEMPLATES_DIR,
200 user='root', group='root',
201 mode=0o0440)
202
203 def post_write(self):
204 try:
205 subprocess.check_call(['sysctl', '-p', self.conffile])
206 except subprocess.CalledProcessError as e:
207 # NOTE: on some systems if sysctl cannot apply all settings it
208 # will return non-zero as well.
209 log("sysctl command returned an error (maybe some "
210 "keys could not be set) - %s" % (e),
211 level=WARNING)
diff --git a/hooks/charmhelpers/contrib/hardening/mysql/__init__.py b/hooks/charmhelpers/contrib/hardening/mysql/__init__.py
new file mode 100644
index 0000000..277b8c7
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/mysql/__init__.py
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
diff --git a/hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py b/hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py
new file mode 100644
index 0000000..d4f0ec1
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.mysql.checks import config
22
23
24def run_mysql_checks():
25 log("Starting MySQL hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("MySQL hardening checks complete.", level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/mysql/checks/config.py b/hooks/charmhelpers/contrib/hardening/mysql/checks/config.py
new file mode 100644
index 0000000..3af8b89
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/mysql/checks/config.py
@@ -0,0 +1,89 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18import subprocess
19
20from charmhelpers.core.hookenv import (
21 log,
22 WARNING,
23)
24from charmhelpers.contrib.hardening.audits.file import (
25 FilePermissionAudit,
26 DirectoryPermissionAudit,
27 TemplatedFile,
28)
29from charmhelpers.contrib.hardening.mysql import TEMPLATES_DIR
30from charmhelpers.contrib.hardening import utils
31
32
33def get_audits():
34 """Get MySQL hardening config audits.
35
36 :returns: dictionary of audits
37 """
38 if subprocess.call(['which', 'mysql'], stdout=subprocess.PIPE) != 0:
39 log("MySQL does not appear to be installed on this node - "
40 "skipping mysql hardening", level=WARNING)
41 return []
42
43 settings = utils.get_settings('mysql')
44 hardening_settings = settings['hardening']
45 my_cnf = hardening_settings['mysql-conf']
46
47 audits = [
48 FilePermissionAudit(paths=[my_cnf], user='root',
49 group='root', mode=0o0600),
50
51 TemplatedFile(hardening_settings['hardening-conf'],
52 MySQLConfContext(),
53 TEMPLATES_DIR,
54 mode=0o0750,
55 user='mysql',
56 group='root',
57 service_actions=[{'service': 'mysql',
58 'actions': ['restart']}]),
59
60 # MySQL and Percona charms do not allow configuration of the
61 # data directory, so use the default.
62 DirectoryPermissionAudit('/var/lib/mysql',
63 user='mysql',
64 group='mysql',
65 recursive=False,
66 mode=0o755),
67
68 DirectoryPermissionAudit('/etc/mysql',
69 user='root',
70 group='root',
71 recursive=False,
72 mode=0o700),
73 ]
74
75 return audits
76
77
78class MySQLConfContext(object):
79 """Defines the set of key/value pairs to set in a mysql config file.
80
81 This context, when called, will return a dictionary containing the
82 key/value pairs of setting to specify in the
83 /etc/mysql/conf.d/hardening.cnf file.
84 """
85 def __call__(self):
86 settings = utils.get_settings('mysql')
87 # Translate for python3
88 return {'mysql_settings':
89 [(k, v) for k, v in six.iteritems(settings['security'])]}
diff --git a/hooks/charmhelpers/contrib/hardening/ssh/__init__.py b/hooks/charmhelpers/contrib/hardening/ssh/__init__.py
new file mode 100644
index 0000000..277b8c7
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/ssh/__init__.py
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
diff --git a/hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py b/hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py
new file mode 100644
index 0000000..b85150d
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.ssh.checks import config
22
23
24def run_ssh_checks():
25 log("Starting SSH hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("SSH hardening checks complete.", level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/ssh/checks/config.py b/hooks/charmhelpers/contrib/hardening/ssh/checks/config.py
new file mode 100644
index 0000000..3fb6ae8
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/ssh/checks/config.py
@@ -0,0 +1,394 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18
19from charmhelpers.core.hookenv import (
20 log,
21 DEBUG,
22)
23from charmhelpers.fetch import (
24 apt_install,
25 apt_update,
26)
27from charmhelpers.core.host import lsb_release
28from charmhelpers.contrib.hardening.audits.file import (
29 TemplatedFile,
30 FileContentAudit,
31)
32from charmhelpers.contrib.hardening.ssh import TEMPLATES_DIR
33from charmhelpers.contrib.hardening import utils
34
35
36def get_audits():
37 """Get SSH hardening config audits.
38
39 :returns: dictionary of audits
40 """
41 audits = [SSHConfig(), SSHDConfig(), SSHConfigFileContentAudit(),
42 SSHDConfigFileContentAudit()]
43 return audits
44
45
46class SSHConfigContext(object):
47
48 type = 'client'
49
50 def get_macs(self, allow_weak_mac):
51 if allow_weak_mac:
52 weak_macs = 'weak'
53 else:
54 weak_macs = 'default'
55
56 default = 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160'
57 macs = {'default': default,
58 'weak': default + ',hmac-sha1'}
59
60 default = ('hmac-sha2-512-etm@openssh.com,'
61 'hmac-sha2-256-etm@openssh.com,'
62 'hmac-ripemd160-etm@openssh.com,umac-128-etm@openssh.com,'
63 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160')
64 macs_66 = {'default': default,
65 'weak': default + ',hmac-sha1'}
66
67 # Use newer ciphers on Ubuntu Trusty and above
68 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
69 log("Detected Ubuntu 14.04 or newer, using new macs", level=DEBUG)
70 macs = macs_66
71
72 return macs[weak_macs]
73
74 def get_kexs(self, allow_weak_kex):
75 if allow_weak_kex:
76 weak_kex = 'weak'
77 else:
78 weak_kex = 'default'
79
80 default = 'diffie-hellman-group-exchange-sha256'
81 weak = (default + ',diffie-hellman-group14-sha1,'
82 'diffie-hellman-group-exchange-sha1,'
83 'diffie-hellman-group1-sha1')
84 kex = {'default': default,
85 'weak': weak}
86
87 default = ('curve25519-sha256@libssh.org,'
88 'diffie-hellman-group-exchange-sha256')
89 weak = (default + ',diffie-hellman-group14-sha1,'
90 'diffie-hellman-group-exchange-sha1,'
91 'diffie-hellman-group1-sha1')
92 kex_66 = {'default': default,
93 'weak': weak}
94
95 # Use newer kex on Ubuntu Trusty and above
96 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
97 log('Detected Ubuntu 14.04 or newer, using new key exchange '
98 'algorithms', level=DEBUG)
99 kex = kex_66
100
101 return kex[weak_kex]
102
103 def get_ciphers(self, cbc_required):
104 if cbc_required:
105 weak_ciphers = 'weak'
106 else:
107 weak_ciphers = 'default'
108
109 default = 'aes256-ctr,aes192-ctr,aes128-ctr'
110 cipher = {'default': default,
111 'weak': default + 'aes256-cbc,aes192-cbc,aes128-cbc'}
112
113 default = ('chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,'
114 'aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr')
115 ciphers_66 = {'default': default,
116 'weak': default + ',aes256-cbc,aes192-cbc,aes128-cbc'}
117
118 # Use newer ciphers on ubuntu Trusty and above
119 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
120 log('Detected Ubuntu 14.04 or newer, using new ciphers',
121 level=DEBUG)
122 cipher = ciphers_66
123
124 return cipher[weak_ciphers]
125
126 def __call__(self):
127 settings = utils.get_settings('ssh')
128 if settings['common']['network_ipv6_enable']:
129 addr_family = 'any'
130 else:
131 addr_family = 'inet'
132
133 ctxt = {
134 'addr_family': addr_family,
135 'remote_hosts': settings['common']['remote_hosts'],
136 'password_auth_allowed':
137 settings['client']['password_authentication'],
138 'ports': settings['common']['ports'],
139 'ciphers': self.get_ciphers(settings['client']['cbc_required']),
140 'macs': self.get_macs(settings['client']['weak_hmac']),
141 'kexs': self.get_kexs(settings['client']['weak_kex']),
142 'roaming': settings['client']['roaming'],
143 }
144 return ctxt
145
146
147class SSHConfig(TemplatedFile):
148 def __init__(self):
149 path = '/etc/ssh/ssh_config'
150 super(SSHConfig, self).__init__(path=path,
151 template_dir=TEMPLATES_DIR,
152 context=SSHConfigContext(),
153 user='root',
154 group='root',
155 mode=0o0644)
156
157 def pre_write(self):
158 settings = utils.get_settings('ssh')
159 apt_update(fatal=True)
160 apt_install(settings['client']['package'])
161 if not os.path.exists('/etc/ssh'):
162 os.makedir('/etc/ssh')
163 # NOTE: don't recurse
164 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
165 maxdepth=0)
166
167 def post_write(self):
168 # NOTE: don't recurse
169 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
170 maxdepth=0)
171
172
173class SSHDConfigContext(SSHConfigContext):
174
175 type = 'server'
176
177 def __call__(self):
178 settings = utils.get_settings('ssh')
179 if settings['common']['network_ipv6_enable']:
180 addr_family = 'any'
181 else:
182 addr_family = 'inet'
183
184 ctxt = {
185 'ssh_ip': settings['server']['listen_to'],
186 'password_auth_allowed':
187 settings['server']['password_authentication'],
188 'ports': settings['common']['ports'],
189 'addr_family': addr_family,
190 'ciphers': self.get_ciphers(settings['server']['cbc_required']),
191 'macs': self.get_macs(settings['server']['weak_hmac']),
192 'kexs': self.get_kexs(settings['server']['weak_kex']),
193 'host_key_files': settings['server']['host_key_files'],
194 'allow_root_with_key': settings['server']['allow_root_with_key'],
195 'password_authentication':
196 settings['server']['password_authentication'],
197 'use_priv_sep': settings['server']['use_privilege_separation'],
198 'use_pam': settings['server']['use_pam'],
199 'allow_x11_forwarding': settings['server']['allow_x11_forwarding'],
200 'print_motd': settings['server']['print_motd'],
201 'print_last_log': settings['server']['print_last_log'],
202 'client_alive_interval':
203 settings['server']['alive_interval'],
204 'client_alive_count': settings['server']['alive_count'],
205 'allow_tcp_forwarding': settings['server']['allow_tcp_forwarding'],
206 'allow_agent_forwarding':
207 settings['server']['allow_agent_forwarding'],
208 'deny_users': settings['server']['deny_users'],
209 'allow_users': settings['server']['allow_users'],
210 'deny_groups': settings['server']['deny_groups'],
211 'allow_groups': settings['server']['allow_groups'],
212 'use_dns': settings['server']['use_dns'],
213 'sftp_enable': settings['server']['sftp_enable'],
214 'sftp_group': settings['server']['sftp_group'],
215 'sftp_chroot': settings['server']['sftp_chroot'],
216 'max_auth_tries': settings['server']['max_auth_tries'],
217 'max_sessions': settings['server']['max_sessions'],
218 }
219 return ctxt
220
221
222class SSHDConfig(TemplatedFile):
223 def __init__(self):
224 path = '/etc/ssh/sshd_config'
225 super(SSHDConfig, self).__init__(path=path,
226 template_dir=TEMPLATES_DIR,
227 context=SSHDConfigContext(),
228 user='root',
229 group='root',
230 mode=0o0600,
231 service_actions=[{'service': 'ssh',
232 'actions':
233 ['restart']}])
234
235 def pre_write(self):
236 settings = utils.get_settings('ssh')
237 apt_update(fatal=True)
238 apt_install(settings['server']['package'])
239 if not os.path.exists('/etc/ssh'):
240 os.makedir('/etc/ssh')
241 # NOTE: don't recurse
242 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
243 maxdepth=0)
244
245 def post_write(self):
246 # NOTE: don't recurse
247 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
248 maxdepth=0)
249
250
251class SSHConfigFileContentAudit(FileContentAudit):
252 def __init__(self):
253 self.path = '/etc/ssh/ssh_config'
254 super(SSHConfigFileContentAudit, self).__init__(self.path, {})
255
256 def is_compliant(self, *args, **kwargs):
257 self.pass_cases = []
258 self.fail_cases = []
259 settings = utils.get_settings('ssh')
260
261 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
262 if not settings['server']['weak_hmac']:
263 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
264 else:
265 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
266
267 if settings['server']['weak_kex']:
268 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
269 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
270 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
271 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
272 else:
273 self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
274 self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
275
276 if settings['server']['cbc_required']:
277 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
278 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
279 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
280 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
281 else:
282 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
283 self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
284 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
285 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
286 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
287 else:
288 if not settings['client']['weak_hmac']:
289 self.fail_cases.append(r'^MACs.+,hmac-sha1$')
290 else:
291 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
292
293 if settings['client']['weak_kex']:
294 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
295 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
296 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
297 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
298 else:
299 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
300 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
301 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
302 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
303
304 if settings['client']['cbc_required']:
305 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
306 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
307 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
308 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
309 else:
310 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
311 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
312 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
313 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
314
315 if settings['client']['roaming']:
316 self.pass_cases.append(r'^UseRoaming yes$')
317 else:
318 self.fail_cases.append(r'^UseRoaming yes$')
319
320 return super(SSHConfigFileContentAudit, self).is_compliant(*args,
321 **kwargs)
322
323
324class SSHDConfigFileContentAudit(FileContentAudit):
325 def __init__(self):
326 self.path = '/etc/ssh/sshd_config'
327 super(SSHDConfigFileContentAudit, self).__init__(self.path, {})
328
329 def is_compliant(self, *args, **kwargs):
330 self.pass_cases = []
331 self.fail_cases = []
332 settings = utils.get_settings('ssh')
333
334 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
335 if not settings['server']['weak_hmac']:
336 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
337 else:
338 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
339
340 if settings['server']['weak_kex']:
341 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
342 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
343 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
344 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
345 else:
346 self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
347 self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
348
349 if settings['server']['cbc_required']:
350 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
351 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
352 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
353 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
354 else:
355 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
356 self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
357 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
358 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
359 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
360 else:
361 if not settings['server']['weak_hmac']:
362 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
363 else:
364 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
365
366 if settings['server']['weak_kex']:
367 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
368 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
369 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
370 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
371 else:
372 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
373 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
374 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
375 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
376
377 if settings['server']['cbc_required']:
378 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
379 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
380 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
381 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
382 else:
383 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
384 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
385 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
386 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
387
388 if settings['server']['sftp_enable']:
389 self.pass_cases.append(r'^Subsystem\ssftp')
390 else:
391 self.fail_cases.append(r'^Subsystem\ssftp')
392
393 return super(SSHDConfigFileContentAudit, self).is_compliant(*args,
394 **kwargs)
diff --git a/hooks/charmhelpers/contrib/hardening/templating.py b/hooks/charmhelpers/contrib/hardening/templating.py
new file mode 100644
index 0000000..d2ab7dc
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/templating.py
@@ -0,0 +1,71 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18
19from charmhelpers.core.hookenv import (
20 log,
21 DEBUG,
22 WARNING,
23)
24
25try:
26 from jinja2 import FileSystemLoader, Environment
27except ImportError:
28 from charmhelpers.fetch import apt_install
29 from charmhelpers.fetch import apt_update
30 apt_update(fatal=True)
31 apt_install('python-jinja2', fatal=True)
32 from jinja2 import FileSystemLoader, Environment
33
34
35# NOTE: function separated from main rendering code to facilitate easier
36# mocking in unit tests.
37def write(path, data):
38 with open(path, 'wb') as out:
39 out.write(data)
40
41
42def get_template_path(template_dir, path):
43 """Returns the template file which would be used to render the path.
44
45 The path to the template file is returned.
46 :param template_dir: the directory the templates are located in
47 :param path: the file path to be written to.
48 :returns: path to the template file
49 """
50 return os.path.join(template_dir, os.path.basename(path))
51
52
53def render_and_write(template_dir, path, context):
54 """Renders the specified template into the file.
55
56 :param template_dir: the directory to load the template from
57 :param path: the path to write the templated contents to
58 :param context: the parameters to pass to the rendering engine
59 """
60 env = Environment(loader=FileSystemLoader(template_dir))
61 template_file = os.path.basename(path)
62 template = env.get_template(template_file)
63 log('Rendering from template: %s' % template.name, level=DEBUG)
64 rendered_content = template.render(context)
65 if not rendered_content:
66 log("Render returned None - skipping '%s'" % path,
67 level=WARNING)
68 return
69
70 write(path, rendered_content.encode('utf-8').strip())
71 log('Wrote template %s' % path, level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/utils.py b/hooks/charmhelpers/contrib/hardening/utils.py
new file mode 100644
index 0000000..a6743a4
--- /dev/null
+++ b/hooks/charmhelpers/contrib/hardening/utils.py
@@ -0,0 +1,157 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import glob
18import grp
19import os
20import pwd
21import six
22import yaml
23
24from charmhelpers.core.hookenv import (
25 log,
26 DEBUG,
27 INFO,
28 WARNING,
29 ERROR,
30)
31
32
33# Global settings cache. Since each hook fire entails a fresh module import it
34# is safe to hold this in memory and not risk missing config changes (since
35# they will result in a new hook fire and thus re-import).
36__SETTINGS__ = {}
37
38
39def _get_defaults(modules):
40 """Load the default config for the provided modules.
41
42 :param modules: stack modules config defaults to lookup.
43 :returns: modules default config dictionary.
44 """
45 default = os.path.join(os.path.dirname(__file__),
46 'defaults/%s.yaml' % (modules))
47 return yaml.safe_load(open(default))
48
49
50def _get_schema(modules):
51 """Load the config schema for the provided modules.
52
53 NOTE: this schema is intended to have 1-1 relationship with they keys in
54 the default config and is used a means to verify valid overrides provided
55 by the user.
56
57 :param modules: stack modules config schema to lookup.
58 :returns: modules default schema dictionary.
59 """
60 schema = os.path.join(os.path.dirname(__file__),
61 'defaults/%s.yaml.schema' % (modules))
62 return yaml.safe_load(open(schema))
63
64
65def _get_user_provided_overrides(modules):
66 """Load user-provided config overrides.
67
68 :param modules: stack modules to lookup in user overrides yaml file.
69 :returns: overrides dictionary.
70 """
71 overrides = os.path.join(os.environ['JUJU_CHARM_DIR'],
72 'hardening.yaml')
73 if os.path.exists(overrides):
74 log("Found user-provided config overrides file '%s'" %
75 (overrides), level=DEBUG)
76 settings = yaml.safe_load(open(overrides))
77 if settings and settings.get(modules):
78 log("Applying '%s' overrides" % (modules), level=DEBUG)
79 return settings.get(modules)
80
81 log("No overrides found for '%s'" % (modules), level=DEBUG)
82 else:
83 log("No hardening config overrides file '%s' found in charm "
84 "root dir" % (overrides), level=DEBUG)
85
86 return {}
87
88
89def _apply_overrides(settings, overrides, schema):
90 """Get overrides config overlayed onto modules defaults.
91
92 :param modules: require stack modules config.
93 :returns: dictionary of modules config with user overrides applied.
94 """
95 if overrides:
96 for k, v in six.iteritems(overrides):
97 if k in schema:
98 if schema[k] is None:
99 settings[k] = v
100 elif type(schema[k]) is dict:
101 settings[k] = _apply_overrides(settings[k], overrides[k],
102 schema[k])
103 else:
104 raise Exception("Unexpected type found in schema '%s'" %
105 type(schema[k]), level=ERROR)
106 else:
107 log("Unknown override key '%s' - ignoring" % (k), level=INFO)
108
109 return settings
110
111
112def get_settings(modules):
113 global __SETTINGS__
114 if modules in __SETTINGS__:
115 return __SETTINGS__[modules]
116
117 schema = _get_schema(modules)
118 settings = _get_defaults(modules)
119 overrides = _get_user_provided_overrides(modules)
120 __SETTINGS__[modules] = _apply_overrides(settings, overrides, schema)
121 return __SETTINGS__[modules]
122
123
124def ensure_permissions(path, user, group, permissions, maxdepth=-1):
125 """Ensure permissions for path.
126
127 If path is a file, apply to file and return. If path is a directory,
128 apply recursively (if required) to directory contents and return.
129
130 :param user: user name
131 :param group: group name
132 :param permissions: octal permissions
133 :param maxdepth: maximum recursion depth. A negative maxdepth allows
134 infinite recursion and maxdepth=0 means no recursion.
135 :returns: None
136 """
137 if not os.path.exists(path):
138 log("File '%s' does not exist - cannot set permissions" % (path),
139 level=WARNING)
140 return
141
142 _user = pwd.getpwnam(user)
143 os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid)
144 os.chmod(path, permissions)
145
146 if maxdepth == 0:
147 log("Max recursion depth reached - skipping further recursion",
148 level=DEBUG)
149 return
150 elif maxdepth > 0:
151 maxdepth -= 1
152
153 if os.path.isdir(path):
154 contents = glob.glob("%s/*" % (path))
155 for c in contents:
156 ensure_permissions(c, user=user, group=group,
157 permissions=permissions, maxdepth=maxdepth)
diff --git a/hooks/charmhelpers/contrib/network/ip.py b/hooks/charmhelpers/contrib/network/ip.py
index 998f00c..b9c7900 100644
--- a/hooks/charmhelpers/contrib/network/ip.py
+++ b/hooks/charmhelpers/contrib/network/ip.py
@@ -191,6 +191,15 @@ get_iface_for_address = partial(_get_for_address, key='iface')
191get_netmask_for_address = partial(_get_for_address, key='netmask') 191get_netmask_for_address = partial(_get_for_address, key='netmask')
192 192
193 193
194def resolve_network_cidr(ip_address):
195 '''
196 Resolves the full address cidr of an ip_address based on
197 configured network interfaces
198 '''
199 netmask = get_netmask_for_address(ip_address)
200 return str(netaddr.IPNetwork("%s/%s" % (ip_address, netmask)).cidr)
201
202
194def format_ipv6_addr(address): 203def format_ipv6_addr(address):
195 """If address is IPv6, wrap it in '[]' otherwise return None. 204 """If address is IPv6, wrap it in '[]' otherwise return None.
196 205
@@ -456,3 +465,18 @@ def get_hostname(address, fqdn=True):
456 return result 465 return result
457 else: 466 else:
458 return result.split('.')[0] 467 return result.split('.')[0]
468
469
470def port_has_listener(address, port):
471 """
472 Returns True if the address:port is open and being listened to,
473 else False.
474
475 @param address: an IP address or hostname
476 @param port: integer port
477
478 Note calls 'zc' via a subprocess shell
479 """
480 cmd = ['nc', '-z', address, str(port)]
481 result = subprocess.call(cmd)
482 return not(bool(result))
diff --git a/hooks/charmhelpers/contrib/network/ovs/__init__.py b/hooks/charmhelpers/contrib/network/ovs/__init__.py
index 77e2db7..83d1813 100644
--- a/hooks/charmhelpers/contrib/network/ovs/__init__.py
+++ b/hooks/charmhelpers/contrib/network/ovs/__init__.py
@@ -25,10 +25,14 @@ from charmhelpers.core.host import (
25) 25)
26 26
27 27
28def add_bridge(name): 28def add_bridge(name, datapath_type=None):
29 ''' Add the named bridge to openvswitch ''' 29 ''' Add the named bridge to openvswitch '''
30 log('Creating bridge {}'.format(name)) 30 log('Creating bridge {}'.format(name))
31 subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name]) 31 cmd = ["ovs-vsctl", "--", "--may-exist", "add-br", name]
32 if datapath_type is not None:
33 cmd += ['--', 'set', 'bridge', name,
34 'datapath_type={}'.format(datapath_type)]
35 subprocess.check_call(cmd)
32 36
33 37
34def del_bridge(name): 38def del_bridge(name):
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index d2ede32..d21c9c7 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -126,7 +126,9 @@ class OpenStackAmuletDeployment(AmuletDeployment):
126 # Charms which can not use openstack-origin, ie. many subordinates 126 # Charms which can not use openstack-origin, ie. many subordinates
127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe', 127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
128 'openvswitch-odl', 'neutron-api-odl', 'odl-controller', 128 'openvswitch-odl', 'neutron-api-odl', 'odl-controller',
129 'cinder-backup'] 129 'cinder-backup', 'nexentaedge-data',
130 'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw',
131 'cinder-nexentaedge', 'nexentaedge-mgmt']
130 132
131 if self.openstack: 133 if self.openstack:
132 for svc in services: 134 for svc in services:
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
index 388b60e..ef3bdcc 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
@@ -27,7 +27,11 @@ import cinderclient.v1.client as cinder_client
27import glanceclient.v1.client as glance_client 27import glanceclient.v1.client as glance_client
28import heatclient.v1.client as heat_client 28import heatclient.v1.client as heat_client
29import keystoneclient.v2_0 as keystone_client 29import keystoneclient.v2_0 as keystone_client
30import novaclient.v1_1.client as nova_client 30from keystoneclient.auth.identity import v3 as keystone_id_v3
31from keystoneclient import session as keystone_session
32from keystoneclient.v3 import client as keystone_client_v3
33
34import novaclient.client as nova_client
31import pika 35import pika
32import swiftclient 36import swiftclient
33 37
@@ -38,6 +42,8 @@ from charmhelpers.contrib.amulet.utils import (
38DEBUG = logging.DEBUG 42DEBUG = logging.DEBUG
39ERROR = logging.ERROR 43ERROR = logging.ERROR
40 44
45NOVA_CLIENT_VERSION = "2"
46
41 47
42class OpenStackAmuletUtils(AmuletUtils): 48class OpenStackAmuletUtils(AmuletUtils):
43 """OpenStack amulet utilities. 49 """OpenStack amulet utilities.
@@ -139,7 +145,7 @@ class OpenStackAmuletUtils(AmuletUtils):
139 return "role {} does not exist".format(e['name']) 145 return "role {} does not exist".format(e['name'])
140 return ret 146 return ret
141 147
142 def validate_user_data(self, expected, actual): 148 def validate_user_data(self, expected, actual, api_version=None):
143 """Validate user data. 149 """Validate user data.
144 150
145 Validate a list of actual user data vs a list of expected user 151 Validate a list of actual user data vs a list of expected user
@@ -150,10 +156,15 @@ class OpenStackAmuletUtils(AmuletUtils):
150 for e in expected: 156 for e in expected:
151 found = False 157 found = False
152 for act in actual: 158 for act in actual:
153 a = {'enabled': act.enabled, 'name': act.name, 159 if e['name'] == act.name:
154 'email': act.email, 'tenantId': act.tenantId, 160 a = {'enabled': act.enabled, 'name': act.name,
155 'id': act.id} 161 'email': act.email, 'id': act.id}
156 if e['name'] == a['name']: 162 if api_version == 3:
163 a['default_project_id'] = getattr(act,
164 'default_project_id',
165 'none')
166 else: