Browse Source

make sync

Junaid Ali 3 years ago
parent
commit
c0d2d9c303
43 changed files with 4127 additions and 156 deletions
  1. 253
    0
      bin/charm_helpers_sync.py
  2. 22
    11
      hooks/charmhelpers/contrib/amulet/utils.py
  3. 15
    0
      hooks/charmhelpers/contrib/hardening/__init__.py
  4. 19
    0
      hooks/charmhelpers/contrib/hardening/apache/__init__.py
  5. 31
    0
      hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
  6. 100
    0
      hooks/charmhelpers/contrib/hardening/apache/checks/config.py
  7. 63
    0
      hooks/charmhelpers/contrib/hardening/audits/__init__.py
  8. 100
    0
      hooks/charmhelpers/contrib/hardening/audits/apache.py
  9. 105
    0
      hooks/charmhelpers/contrib/hardening/audits/apt.py
  10. 552
    0
      hooks/charmhelpers/contrib/hardening/audits/file.py
  11. 84
    0
      hooks/charmhelpers/contrib/hardening/harden.py
  12. 19
    0
      hooks/charmhelpers/contrib/hardening/host/__init__.py
  13. 50
    0
      hooks/charmhelpers/contrib/hardening/host/checks/__init__.py
  14. 39
    0
      hooks/charmhelpers/contrib/hardening/host/checks/apt.py
  15. 55
    0
      hooks/charmhelpers/contrib/hardening/host/checks/limits.py
  16. 67
    0
      hooks/charmhelpers/contrib/hardening/host/checks/login.py
  17. 52
    0
      hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py
  18. 134
    0
      hooks/charmhelpers/contrib/hardening/host/checks/pam.py
  19. 45
    0
      hooks/charmhelpers/contrib/hardening/host/checks/profile.py
  20. 39
    0
      hooks/charmhelpers/contrib/hardening/host/checks/securetty.py
  21. 131
    0
      hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py
  22. 211
    0
      hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py
  23. 19
    0
      hooks/charmhelpers/contrib/hardening/mysql/__init__.py
  24. 31
    0
      hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py
  25. 89
    0
      hooks/charmhelpers/contrib/hardening/mysql/checks/config.py
  26. 19
    0
      hooks/charmhelpers/contrib/hardening/ssh/__init__.py
  27. 31
    0
      hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py
  28. 394
    0
      hooks/charmhelpers/contrib/hardening/ssh/checks/config.py
  29. 71
    0
      hooks/charmhelpers/contrib/hardening/templating.py
  30. 157
    0
      hooks/charmhelpers/contrib/hardening/utils.py
  31. 24
    0
      hooks/charmhelpers/contrib/network/ip.py
  32. 6
    2
      hooks/charmhelpers/contrib/network/ovs/__init__.py
  33. 3
    1
      hooks/charmhelpers/contrib/openstack/amulet/deployment.py
  34. 40
    13
      hooks/charmhelpers/contrib/openstack/amulet/utils.py
  35. 108
    2
      hooks/charmhelpers/contrib/openstack/context.py
  36. 35
    7
      hooks/charmhelpers/contrib/openstack/ip.py
  37. 10
    4
      hooks/charmhelpers/contrib/openstack/neutron.py
  38. 602
    71
      hooks/charmhelpers/contrib/openstack/utils.py
  39. 22
    7
      hooks/charmhelpers/contrib/python/packages.py
  40. 183
    16
      hooks/charmhelpers/contrib/storage/linux/ceph.py
  41. 5
    5
      hooks/charmhelpers/contrib/storage/linux/utils.py
  42. 31
    0
      hooks/charmhelpers/core/hookenv.py
  43. 61
    17
      hooks/charmhelpers/core/host.py

+ 253
- 0
bin/charm_helpers_sync.py View File

@@ -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
+
22
+import logging
23
+import optparse
24
+import os
25
+import subprocess
26
+import shutil
27
+import sys
28
+import tempfile
29
+import yaml
30
+from fnmatch import fnmatch
31
+
32
+import six
33
+
34
+CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
35
+
36
+
37
+def 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
+
44
+def 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
+
52
+def _module_path(module):
53
+    return os.path.join(*module.split('.'))
54
+
55
+
56
+def _src_path(src, module):
57
+    return os.path.join(src, 'charmhelpers', _module_path(module))
58
+
59
+
60
+def _dest_path(dest, module):
61
+    return os.path.join(dest, _module_path(module))
62
+
63
+
64
+def _is_pyfile(path):
65
+    return os.path.isfile(path + '.py')
66
+
67
+
68
+def 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
+
83
+def 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
+
96
+def 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
+
127
+def 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
+
137
+def 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
+
162
+def parse_sync_options(options):
163
+    if not options:
164
+        return []
165
+    return options.split(',')
166
+
167
+
168
+def 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
+
178
+def 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
+
196
+if __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)

+ 22
- 11
hooks/charmhelpers/contrib/amulet/utils.py View File

@@ -601,7 +601,7 @@ class AmuletUtils(object):
601 601
                 return ('Process name count mismatch.  expected, actual: {}, '
602 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 605
                     zip(e_proc_names.items(), a_proc_names.items()):
606 606
                 if e_proc_name != a_proc_name:
607 607
                     return ('Process name mismatch.  expected, actual: {}, '
@@ -610,25 +610,31 @@ class AmuletUtils(object):
610 610
                 a_pids_length = len(a_pids)
611 611
                 fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
612 612
                             '{}, {} ({})'.format(e_sentry_name, e_proc_name,
613
-                                                 e_pids_length, a_pids_length,
613
+                                                 e_pids, a_pids_length,
614 614
                                                  a_pids))
615 615
 
616
-                # If expected is not bool, ensure PID quantities match
617
-                if not isinstance(e_pids_length, bool) and \
618
-                        a_pids_length != e_pids_length:
616
+                # If expected is a list, ensure at least one PID quantity match
617
+                if isinstance(e_pids, list) and \
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 625
                     return fail_msg
620 626
                 # If expected is bool True, ensure 1 or more PIDs exist
621
-                elif isinstance(e_pids_length, bool) and \
622
-                        e_pids_length is True and a_pids_length < 1:
627
+                elif isinstance(e_pids, bool) and \
628
+                        e_pids is True and a_pids_length < 1:
623 629
                     return fail_msg
624 630
                 # If expected is bool False, ensure 0 PIDs exist
625
-                elif isinstance(e_pids_length, bool) and \
626
-                        e_pids_length is False and a_pids_length != 0:
631
+                elif isinstance(e_pids, bool) and \
632
+                        e_pids is False and a_pids_length != 0:
627 633
                     return fail_msg
628 634
                 else:
629 635
                     self.log.debug('PID check OK: {} {} {}: '
630 636
                                    '{}'.format(e_sentry_name, e_proc_name,
631
-                                               e_pids_length, a_pids))
637
+                                               e_pids, a_pids))
632 638
         return None
633 639
 
634 640
     def validate_list_of_identical_dicts(self, list_of_dicts):
@@ -782,15 +788,20 @@ class AmuletUtils(object):
782 788
 
783 789
 # amulet juju action helpers:
784 790
     def run_action(self, unit_sentry, action,
785
-                   _check_output=subprocess.check_output):
791
+                   _check_output=subprocess.check_output,
792
+                   params=None):
786 793
         """Run the named action on a given unit sentry.
787 794
 
795
+        params a dict of parameters to use
788 796
         _check_output parameter is used for dependency injection.
789 797
 
790 798
         @return action_id.
791 799
         """
792 800
         unit_id = unit_sentry.info["unit_name"]
793 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 805
         self.log.info("Running command: %s\n" % " ".join(command))
795 806
         output = _check_output(command, universal_newlines=True)
796 807
         data = json.loads(output)

+ 15
- 0
hooks/charmhelpers/contrib/hardening/__init__.py View File

@@ -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/>.

+ 19
- 0
hooks/charmhelpers/contrib/hardening/apache/__init__.py View File

@@ -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
+
17
+from os import path
18
+
19
+TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

+ 31
- 0
hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py View File

@@ -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
+
17
+from charmhelpers.core.hookenv import (
18
+    log,
19
+    DEBUG,
20
+)
21
+from charmhelpers.contrib.hardening.apache.checks import config
22
+
23
+
24
+def 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)

+ 100
- 0
hooks/charmhelpers/contrib/hardening/apache/checks/config.py View File

@@ -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
+
17
+import os
18
+import re
19
+import subprocess
20
+
21
+
22
+from charmhelpers.core.hookenv import (
23
+    log,
24
+    INFO,
25
+)
26
+from charmhelpers.contrib.hardening.audits.file import (
27
+    FilePermissionAudit,
28
+    DirectoryPermissionAudit,
29
+    NoReadWriteForOther,
30
+    TemplatedFile,
31
+)
32
+from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
33
+from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
34
+from charmhelpers.contrib.hardening import utils
35
+
36
+
37
+def 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
+
84
+class 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

+ 63
- 0
hooks/charmhelpers/contrib/hardening/audits/__init__.py View File

@@ -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
+
18
+class 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

+ 100
- 0
hooks/charmhelpers/contrib/hardening/audits/apache.py View File

@@ -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
+
17
+import re
18
+import subprocess
19
+
20
+from six import string_types
21
+
22
+from charmhelpers.core.hookenv import (
23
+    log,
24
+    INFO,
25
+    ERROR,
26
+)
27
+
28
+from charmhelpers.contrib.hardening.audits import BaseAudit
29
+
30
+
31
+class 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'])

+ 105
- 0
hooks/charmhelpers/contrib/hardening/audits/apt.py View File

@@ -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
+
17
+from __future__ import absolute_import  # required for external apt import
18
+from apt import apt_pkg
19
+from six import string_types
20
+
21
+from charmhelpers.fetch import (
22
+    apt_cache,
23
+    apt_purge
24
+)
25
+from charmhelpers.core.hookenv import (
26
+    log,
27
+    DEBUG,
28
+    WARNING,
29
+)
30
+from charmhelpers.contrib.hardening.audits import BaseAudit
31
+
32
+
33
+class 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
+
51
+class 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

+ 552
- 0
hooks/charmhelpers/contrib/hardening/audits/file.py View File

@@ -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
+
17
+import grp
18
+import os
19
+import pwd
20
+import re
21
+
22
+from subprocess import (
23
+    CalledProcessError,
24
+    check_output,
25
+    check_call,
26
+)
27
+from traceback import format_exc
28
+from six import string_types
29
+from stat import (
30
+    S_ISGID,
31
+    S_ISUID
32
+)
33
+
34
+from charmhelpers.core.hookenv import (
35
+    log,
36
+    DEBUG,
37
+    INFO,
38
+    WARNING,
39
+    ERROR,
40
+)
41
+from charmhelpers.core import unitdata
42
+from charmhelpers.core.host import file_hash
43
+from charmhelpers.contrib.hardening.audits import BaseAudit
44
+from charmhelpers.contrib.hardening.templating import (
45
+    get_template_path,
46
+    render_and_write,
47
+)
48
+from charmhelpers.contrib.hardening import utils
49
+
50
+
51
+class 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
+
117
+class 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
+
198
+class 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
+
240
+class 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
+
274
+class 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
+
308
+class 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
+
331
+class 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
+
489
+class 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
+
501
+class 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)

+ 84
- 0
hooks/charmhelpers/contrib/hardening/harden.py View File

@@ -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
+
17
+import six
18
+
19
+from collections import OrderedDict
20
+
21
+from charmhelpers.core.hookenv import (
22
+    config,
23
+    log,
24
+    DEBUG,
25
+    WARNING,
26
+)
27
+from charmhelpers.contrib.hardening.host.checks import run_os_checks
28
+from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks
29
+from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
30
+from charmhelpers.contrib.hardening.apache.checks import run_apache_checks
31
+
32
+
33
+def 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

+ 19
- 0
hooks/charmhelpers/contrib/hardening/host/__init__.py View File

@@ -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
+
17
+from os import path
18
+
19
+TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

+ 50
- 0
hooks/charmhelpers/contrib/hardening/host/checks/__init__.py View File

@@ -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
+
17
+from charmhelpers.core.hookenv import (
18
+    log,
19
+    DEBUG,
20
+)
21
+from 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
+
34
+def 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)

+ 39
- 0
hooks/charmhelpers/contrib/hardening/host/checks/apt.py View File

@@ -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
+
17
+from charmhelpers.contrib.hardening.utils import get_settings
18
+from charmhelpers.contrib.hardening.audits.apt import (
19
+    AptConfig,
20
+    RestrictedPackages,
21
+)
22
+
23
+
24
+def 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

+ 55
- 0
hooks/charmhelpers/contrib/hardening/host/checks/limits.py View File

@@ -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
+
17
+from charmhelpers.contrib.hardening.audits.file import (
18
+    DirectoryPermissionAudit,
19
+    TemplatedFile,
20
+)
21
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
22
+from charmhelpers.contrib.hardening import utils
23
+
24
+
25
+def 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
+
49
+class 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

+ 67
- 0
hooks/charmhelpers/contrib/hardening/host/checks/login.py View File

@@ -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
+
17
+from six import string_types
18
+
19
+from charmhelpers.contrib.hardening.audits.file import TemplatedFile
20
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
21
+from charmhelpers.contrib.hardening import utils
22
+
23
+
24
+def 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
+
35
+class 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

+ 52
- 0
hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py View File

@@ -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
+
17
+from charmhelpers.contrib.hardening.audits.file import (
18
+    FilePermissionAudit,
19
+    ReadOnly,
20
+)
21
+from charmhelpers.contrib.hardening import utils
22
+
23
+
24
+def 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

+ 134
- 0
hooks/charmhelpers/contrib/hardening/host/checks/pam.py View File

@@ -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
+
17
+from subprocess import (
18
+    check_output,
19
+    CalledProcessError,
20
+)
21
+
22
+from charmhelpers.core.hookenv import (
23
+    log,
24
+    DEBUG,
25
+    ERROR,
26
+)
27
+from charmhelpers.fetch import (
28
+    apt_install,
29
+    apt_purge,
30
+    apt_update,
31
+)
32
+from charmhelpers.contrib.hardening.audits.file import (
33
+    TemplatedFile,
34
+    DeletedFile,
35
+)
36
+from charmhelpers.contrib.hardening import utils
37
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
38
+
39
+
40
+def 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
+
60
+class 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
+
72
+class 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
+
101
+class 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
+
113
+class 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)

+ 45
- 0
hooks/charmhelpers/contrib/hardening/host/checks/profile.py View File

@@ -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
+
17
+from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19
+from charmhelpers.contrib.hardening import utils
20
+
21
+
22
+def 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
+
41
+class ProfileContext(object):
42
+
43
+    def __call__(self):
44
+        ctxt = {}
45
+        return ctxt

+ 39
- 0
hooks/charmhelpers/contrib/hardening/host/checks/securetty.py View File

@@ -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
+
17
+from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19
+from charmhelpers.contrib.hardening import utils
20
+
21
+
22
+def 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
+
34
+class SecureTTYContext(object):
35
+
36
+    def __call__(self):
37
+        settings = utils.get_settings('os')
38
+        ctxt = {'ttys': settings['auth']['root_ttys']}
39
+        return ctxt

+ 131
- 0
hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py View File

@@ -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
+
17
+import subprocess
18
+
19
+from charmhelpers.core.hookenv import (
20
+    log,
21
+    INFO,
22
+)
23
+from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit
24
+from charmhelpers.contrib.hardening import utils
25
+
26
+
27
+BLACKLIST = ['/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
+
47
+WHITELIST = ['/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
+
77
+def 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
+
120
+def 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'))

+ 211
- 0
hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py View File

@@ -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
+
17
+import os
18
+import platform
19
+import re
20
+import six
21
+import subprocess
22
+
23
+from charmhelpers.core.hookenv import (
24
+    log,
25
+    INFO,
26
+    WARNING,
27
+)
28
+from charmhelpers.contrib.hardening import utils
29
+from charmhelpers.contrib.hardening.audits.file import (
30
+    FilePermissionAudit,
31
+    TemplatedFile,
32
+)
33
+from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
34
+
35
+
36
+SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s
37
+net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s
38
+net.ipv4.conf.all.rp_filter=1
39
+net.ipv4.conf.default.rp_filter=1
40
+net.ipv4.icmp_echo_ignore_broadcasts=1
41
+net.ipv4.icmp_ignore_bogus_error_responses=1
42
+net.ipv4.icmp_ratelimit=100
43
+net.ipv4.icmp_ratemask=88089
44
+net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s
45
+net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s
46
+net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s
47
+net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s
48
+net.ipv4.tcp_rfc1337=1
49
+net.ipv4.tcp_syncookies=1
50
+net.ipv4.conf.all.shared_media=1
51
+net.ipv4.conf.default.shared_media=1
52
+net.ipv4.conf.all.accept_source_route=0
53
+net.ipv4.conf.default.accept_source_route=0
54
+net.ipv4.conf.all.accept_redirects=0
55
+net.ipv4.conf.default.accept_redirects=0
56
+net.ipv6.conf.all.accept_redirects=0
57
+net.ipv6.conf.default.accept_redirects=0
58
+net.ipv4.conf.all.secure_redirects=0
59
+net.ipv4.conf.default.secure_redirects=0
60
+net.ipv4.conf.all.send_redirects=0
61
+net.ipv4.conf.default.send_redirects=0
62
+net.ipv4.conf.all.log_martians=0
63
+net.ipv6.conf.default.router_solicitations=0
64
+net.ipv6.conf.default.accept_ra_rtr_pref=0
65
+net.ipv6.conf.default.accept_ra_pinfo=0
66
+net.ipv6.conf.default.accept_ra_defrtr=0
67
+net.ipv6.conf.default.autoconf=0
68
+net.ipv6.conf.default.dad_transmits=0
69
+net.ipv6.conf.default.max_addresses=1
70
+net.ipv6.conf.all.accept_ra=0
71
+net.ipv6.conf.default.accept_ra=0
72
+kernel.modules_disabled=%(kernel_modules_disabled)s
73
+kernel.sysrq=%(kernel_sysrq)s
74
+fs.suid_dumpable=%(fs_suid_dumpable)s
75
+kernel.randomize_va_space=2
76
+"""
77
+
78
+
79
+def 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
+
102
+class 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
+
126
+class 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
+
139
+class 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
+
193
+class 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)

+ 19
- 0
hooks/charmhelpers/contrib/hardening/mysql/__init__.py View File

@@ -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
+
17
+from os import path
18
+
19
+TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

+ 31
- 0
hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py View File

@@ -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
+
17
+from charmhelpers.core.hookenv import (
18
+    log,
19
+    DEBUG,
20
+)
21
+from charmhelpers.contrib.hardening.mysql.checks import config
22
+
23
+
24
+def 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)

+ 89
- 0
hooks/charmhelpers/contrib/hardening/mysql/checks/config.py View File

@@ -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
+
17
+import six
18
+import subprocess
19
+
20
+from charmhelpers.core.hookenv import (
21
+    log,
22
+    WARNING,
23
+)
24
+from charmhelpers.contrib.hardening.audits.file import (
25
+    FilePermissionAudit,
26
+    DirectoryPermissionAudit,
27
+    TemplatedFile,
28
+)
29
+from charmhelpers.contrib.hardening.mysql import TEMPLATES_DIR
30
+from charmhelpers.contrib.hardening import utils
31
+
32
+
33
+def 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
+
78
+class 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'])]}

+ 19
- 0
hooks/charmhelpers/contrib/hardening/ssh/__init__.py View File

@@ -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
+
17
+from os import path
18
+
19
+TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

+ 31
- 0
hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py View File

@@ -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
+
17
+from charmhelpers.core.hookenv import (
18
+    log,
19
+    DEBUG,
20
+)
21
+from charmhelpers.contrib.hardening.ssh.checks import config
22
+
23
+
24
+def 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)

+ 394
- 0
hooks/charmhelpers/contrib/hardening/ssh/checks/config.py View File

@@ -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
+
17
+import os
18
+
19
+from charmhelpers.core.hookenv import (
20
+    log,
21
+    DEBUG,
22
+)
23
+from charmhelpers.fetch import (
24
+    apt_install,
25
+    apt_update,
26
+)
27
+from charmhelpers.core.host import lsb_release
28
+from charmhelpers.contrib.hardening.audits.file import (
29
+    TemplatedFile,
30
+    FileContentAudit,
31
+)
32
+from charmhelpers.contrib.hardening.ssh import TEMPLATES_DIR
33
+from charmhelpers.contrib.hardening import utils
34
+
35
+
36
+def 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
+
46
+class 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,'