From 9662e8b4ab737045290a5d9f284be3b9b595e24e Mon Sep 17 00:00:00 2001 From: karim Hamza Date: Fri, 22 Feb 2019 10:10:30 +0100 Subject: [PATCH] Feature (rhel7/httpd 2.4) : hardening apache and code refactoring (#251) * Feature (rhel7/httpd 2.4) : hardening apache and code refactoring * remove hard returns * Add default Listen 80 in httpd.conf In case there no vhosts defined in pillar httpd will listen on port 80. Without this default it will not start * empty file autoindex.conf instead of deleting it * explicit hardening items and references from CIS * add #3.5 hardening rule * explain CIS recommendations categories * add dependencies before start service * add recommendation #7.1 Install mod_ssl * link in readme to hardening doc --- Hardening.md | 105 +++++ README-ng.rst | 149 ++++++ README.rst | 7 +- _modules/apache_directives.py | 430 ++++++++++++++++++ apache/config-ng.sls | 118 +++++ .../defaults/RedHat/defaults-apache-2.4.yaml | 77 ++++ .../files/RedHat/apache-2.4-ng.config.jinja | 40 ++ apache/files/RedHat/welcome.conf | 15 + apache/hardening-values.yaml | 151 ++++++ apache/hardening.sls | 67 +++ apache/lib.sls | 26 ++ apache/modules-ng.sls | 86 ++++ apache/osfamilymap.yaml | 3 +- apache/vhosts/vhost-ng.conf.jinja | 6 + apache/vhosts/vhost-ng.sls | 110 +++++ pillar-ng.example.yaml | 122 +++++ pillar.example | 5 +- 17 files changed, 1512 insertions(+), 5 deletions(-) create mode 100644 Hardening.md create mode 100644 README-ng.rst create mode 100644 _modules/apache_directives.py create mode 100644 apache/config-ng.sls create mode 100644 apache/defaults/RedHat/defaults-apache-2.4.yaml create mode 100644 apache/files/RedHat/apache-2.4-ng.config.jinja create mode 100644 apache/files/RedHat/welcome.conf create mode 100644 apache/hardening-values.yaml create mode 100644 apache/hardening.sls create mode 100644 apache/lib.sls create mode 100644 apache/modules-ng.sls create mode 100644 apache/vhosts/vhost-ng.conf.jinja create mode 100644 apache/vhosts/vhost-ng.sls create mode 100644 pillar-ng.example.yaml diff --git a/Hardening.md b/Hardening.md new file mode 100644 index 0000000..1ccfc93 --- /dev/null +++ b/Hardening.md @@ -0,0 +1,105 @@ +# Hardening list + +This formula enforce security recommandations from [CIS Benchmarks](https://www.cisecurity.org/cis-benchmarks/) website + +From ***CIS_Apache_HTTP_Server_2.4_Benchmark_v1.4.pdf*** document + +> A scoring status indicates whether compliance with the given recommendation impacts the assessed target's benchmark score. + +> Items in [*level 2*] profile exhibit one or more of the following characteristics: +> - are intended for environments or use cases where security is paramount +> - acts as defense in depth measure +> - may negatively inhibit the utility or performance of the technology + +In this formula we focus on (**Scored**) [*level* ***1***] items + +## List of all items with their CIS references + +## 2. Minimize Apache Modules +- [ ] 2.1 Enable Only Necessary Authentication and Authorization Modules (Not Scored) +- [X] 2.2 Enable the Log Config Module (**Scored**) +- [X] 2.3 Disable WebDAV Modules (**Scored**) +- [X] 2.4 Disable Status Module (**Scored**) +- [X] 2.5 Disable Autoindex Module (**Scored**) +- [ ] 2.6 Disable Proxy Modules (**Scored**) +- [X] 2.7 Disable User Directories Modules (**Scored**) +- [X] 2.8 Disable Info Module (**Scored**) +## 3. Principles, Permissions, and Ownership +- [X] 3.1 Run the Apache Web Server as a non-root user (**Scored**) +- [X] 3.2 Give the Apache User Account an Invalid Shell (**Scored**) +- [ ] 3.3 Lock the Apache User Account (**Scored**) +- [X] 3.4 Set Ownership on Apache Directories and Files (**Scored**) +- [X] 3.5 Set Group Id on Apache Directories and Files (**Scored**) +- [ ] 3.6 Restrict Other Write Access on Apache Directories and Files (**Scored**) +- [X] 3.7 Secure Core Dump Directory (**Scored**) +- [ ] 3.8 Secure the Lock File (**Scored**) +- [X] 3.9 Secure the Pid File (**Scored**) +- [X] 3.10 Secure the ScoreBoard File (**Scored**) +- [X] 3.11 Restrict Group Write Access for the Apache Directories and Files (**Scored**) +- [X] 3.12 Restrict Group Write Access for the Document Root Directories and Files (**Scored**) +## 4. Apache Access Control +- [X] 4.1 Deny Access to OS Root Directory (**Scored**) +- [ ] 4.2 Allow Appropriate Access to Web Content (Not Scored) +- [X] 4.3 Restrict Override for the OS Root Directory (**Scored**) +- [X] 4.4 Restrict Override for All Directories (**Scored**) +## 5. Minimize Features, Content and Options +- [X] 5.1 Restrict Options for the OS Root Directory (**Scored**) +- [X] 5.2 Restrict Options for the Web Root Directory (**Scored**) +- [X] 5.3 Minimize Options for Other Directories (**Scored**) +- [X] 5.4 Remove Default HTML Content (**Scored**) +- [X] 5.5 Remove Default CGI Content printenv (**Scored**) +- [X] 5.6 Remove Default CGI Content test-cgi (**Scored**) +- [X] 5.7 Limit HTTP Request Methods (**Scored**) +- [X] 5.8 Disable HTTP TRACE Method (**Scored**) +- [X] 5.9 Restrict HTTP Protocol Versions (**Scored**) +- [X] 5.10 Restrict Access to .ht* files (**Scored**) +- [ ] 5.11 Restrict File Extensions [*level 2*] (**Scored**) +- [ ] 5.12 Deny IP Address Based Requests [*level 2*] (**Scored**) +- [ ] 5.13 Restrict Listen Directive [*level 2*] (**Scored**) +- [ ] 5.14 Restrict Browser Frame Options [*level 2*] (**Scored**) +## 6. Operations - Logging, Monitoring and Maintenance +- [X] 6.1 Configure the Error Log (**Scored**) +- [ ] 6.2 Configure a Syslog Facility for Error Logging [*level 2*] (**Scored**) +- [X] 6.3 Configure the Access Log (**Scored**) +- [X] 6.4 Log Storage and Rotation (**Scored**) +- [ ] 6.5 Apply Applicable Patches (**Scored**) +- [ ] 6.6 Install and Enable ModSecurity [*level 2*] (**Scored**) +- [ ] 6.7 Install and Enable OWASP ModSecurity Core Rule Set [*level 2*] (**Scored**) +## 7. SSL/TLS Configuration +- [X] 7.1 Install mod_ssl and/or mod_nss (**Scored**) +- [ ] 7.2 Install a Valid Trusted Certificate (**Scored**) +- [ ] 7.3 Protect the Server's Private Key (**Scored**) +- [X] 7.4 Disable the SSL v3.0 Protocol (**Scored**) +- [ ] 7.5 Restrict Weak SSL/TLS Ciphers (**Scored**) +- [X] 7.6 Disable SSL Insecure Renegotiation (**Scored**) +- [X] 7.7 Ensure SSL Compression is not Enabled (**Scored**) +- [ ] 7.8 Restrict Medium Strength SSL/TLS Ciphers (**Scored**) +- [ ] 7.9 Disable the TLS v1.0 Protocol [*level 2*] (**Scored**) +- [ ] 7.10 Enable OCSP Stapling [*level 2*] (**Scored**) +- [ ] 7.11 Enable HTTP Strict Transport Security [*level 2*] (**Scored**) +## 8. Information Leakage +- [X] 8.1 Set ServerToken to 'Prod' (**Scored**) +- [X] 8.2 Set ServerSignature to 'Off' (**Scored**) +- [ ] 8.3 Information Leakage via Default Apache Content [*level 2*] (**Scored**) +- [ ] 8.4 Information Leakage via ETag [*level 2*] (**Scored**) +## 9. Denial of Service Mitigations +- [X] 9.1 Set TimeOut to 10 or less (**Scored**) +- [X] 9.2 Set the KeepAlive directive to On (**Scored**) +- [X] 9.3 Set MaxKeepAliveRequests to 100 or greater (**Scored**) +- [X] 9.4 Set KeepAliveTimeout Low to Mitigate Denial of Service (**Scored**) +- [X] 9.5 Set Timeout Limits for Request Headers (**Scored**) +- [X] 9.6 Set Timeout Limits for the Request Body (**Scored**) +## 10. Request Limits +- [ ] 10.1 Set the LimitRequestLine directive to 512 or less [*level 2*] (**Scored**) +- [ ] 10.2 Set the LimitRequestFields directive to 100 or less [*level 2*] (**Scored**) +- [ ] 10.3 Set the LimitRequestFieldsize directive to 1024 or less [*level 2*] (**Scored**) +- [ ] 10.4 Set the LimitRequestBody directive to 102400 or less [*level 2*] (**Scored**) +## 11. Enable SELinux to Restrict Apache Processes +- [ ] 11.1 Enable SELinux in Enforcing Mode [*level 2*] (**Scored**) +- [ ] 11.2 Run Apache Processes in the httpd_t Confined Context [*level 2*] (**Scored**) +- [ ] 11.3 Ensure the httpd_t Type is Not in Permissive Mode [*level 2*] (**Scored**) +- [ ] 11.4 Ensure Only the Necessary SELinux Booleans are Enabled [*level 2*] (Not Scored) +## 12. Enable AppArmor to Restrict Apache Processes +- [ ] 12.1 Enable the AppArmor Framework [*level 2*] (**Scored**) +- [ ] 12.2 Customize the Apache AppArmor Profile [*level 2*] (Not Scored) +- [ ] 12.3 Ensure Apache AppArmor Profile is in Enforce Mode [*level 2*] (**Scored**) diff --git a/README-ng.rst b/README-ng.rst new file mode 100644 index 0000000..eeee153 --- /dev/null +++ b/README-ng.rst @@ -0,0 +1,149 @@ +====== +apache +====== + +Formulas to set up and configure the Apache HTTP server. + +This Formula uses the concepts of ``directive`` and ``container`` in pillars + +* ``directive`` is an httpd directive https://httpd.apache.org/docs/2.4/en/mod/directives.html +* ``container`` is what described the `configuration sections` https://httpd.apache.org/docs/2.4/en/sections.html + +see examples below for more explanation + +Also it includes and enforce some hardening rules to prevent security issues + +See ``_ and ``_. + +.. note:: + + See the full `Salt Formulas installation and usage instructions + `_. + +Available states +================ + +.. contents:: + :local: + +``apache`` +---------- + +Installs the Apache package and starts the service. + +``apache.config-ng`` +----------------- + +Configures apache server. + +The configuration is done by merging the pillar content with defaults +present in the state ``_ + +.. code:: yaml + + apache: + server_apache_config: + directives: + - Timeout: 5 + containers: + IfModule: + - + item: 'mime_module' + directives: + - AddType: 'application/x-font-ttf ttc ttf' + - AddType: 'application/x-font-opentype otf' + - AddType: 'application/x-font-woff woff2' + + +``apache.modules-ng`` +------------------ + +Enables and disables Apache modules. + +``apache.vhosts.vhost-ng`` +-------------------------- + +Configures Apache name-based virtual hosts and creates virtual host directories using data from Pillar. + +All necessary data must be provided in the pillar + +Exceptions are : + +* ``CustomLog`` default is ``/path/apache/log/ServerName-access.log combined`` + +* if ``Logformat`` is defined in pillar, ``CustomLog`` is enforced to ``/path/apache/log/ServerName-access.log Logformat`` + +* ``ErrorLog`` is enforced to ``/path/apache/log/ServerName-error.log`` + +Example Pillar: + +Create two vhosts ``example.com.conf`` and ``test.example.com.conf`` + +.. code:: yaml + + apache: + VirtualHost: + example.com: # <-- this is an id decalaration used in salt and default ServerName + item: '*:80' + directives: + - RewriteEngine: 'on' + - Header: 'set Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS' + containers: + Location: + item: '/test.html' + directives: + - Require: 'all granted' + site_id_declaration: + item: '10.10.1.1:8080' + directives: + - ServerName: 'test.example.com' + - LogFormat: '"%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %{ms}T"' + +Files produced by these pillars : + +``example.com.conf`` + +.. code:: bash + + + ServerName example.com + CustomLog /var/log/httpd/example.com-access.log combined + ErrorLog /var/log/httpd/example.com-error.log + RewriteEngine on + Header set Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS + + Require all granted + + + + +``test.example.com.conf`` + +.. code:: bash + + + ServerName test.example.com + CustomLog /var/log/httpd/test.example.com-access.log "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %{ms}T" + ErrorLog /var/log/httpd/test.example.com-error.log + + + + +this will delete ``test.example.com.conf`` + +.. code:: yaml + + apache: + VirtualHost: + test.example.com: + item: '10.10.1.1:8080' + absent: True # <-- delete test.example.com.conf + directives: + - ServerName: 'test.example.com' + + + +``apache.uninstall`` +---------- + +Stops the Apache service and uninstalls the package. diff --git a/README.rst b/README.rst index 0e8b4b5..fb860fd 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ apache ====== + +.. note:: See ``_ for new gen of the state. + Formulas to set up and configure the Apache HTTP server. .. note:: @@ -185,14 +188,14 @@ Example Pillar: When using the provided templates, one can use a space separated list of interfaces to bind to. For example, to bind both IPv4 and IPv6: - + .. code:: yaml apache: sites: example.com: interface: '1.2.3.4 [2001:abc:def:100::3]' - + ``apache.manage_security`` -------------------------- diff --git a/_modules/apache_directives.py b/_modules/apache_directives.py new file mode 100644 index 0000000..50b9f69 --- /dev/null +++ b/_modules/apache_directives.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +''' +apache directives +:maintainer: "karim Hamza" + + +''' + +from __future__ import absolute_import, unicode_literals + +# Import python libs +import re +from copy import deepcopy + + +from salt.exceptions import CommandExecutionError +from salt.ext.six.moves import range + + +def _get_directive_values(directive, d_list): + ''' + Returns list all values of directive + ''' + values = [item.get(directive) for item in d_list if directive in item] + + return values + + +def get_directive_single_value(directive, d_list, default=None): + ''' + Returns single value of directive + + default is returned if directive is absent from list + ''' + values = _get_directive_values(directive, d_list) + try: + return values[0] + except IndexError: + if default is not None: + return default + + error_msg = "invalid Pillar content - " \ + + directive + " - is not defined" + raise CommandExecutionError(error_msg) + + +def append_to_container_directives(directive, value, container): + ''' + Append directive to directives list + ''' + try: + container['directives'].append({directive: value}) + except KeyError: + container['directives'] = [] + container['directives'].append({directive: value}) + + return container + + +def _manage_directive_into_containers(directive, + value, + container, + container_name_target, + item, + enforce_value=False, + add_directive=True): + ''' + Enforce value for directive into specific container + + directive + directive label (name) + + value + value to enforce + + container + container to parse + + container_name_target + container name target into directive/value have to be enforced + + item + name of the item target + + enforce_value : default=False + True: enforce value if directive exists, otherwise add it if add_directive=True + + add_directive : default=True + Only if enforce_value=False add directive if it is not present + ''' + for n_container, l_containers in container.get('containers', {}).items(): + for idx, nested_container in enumerate(l_containers): + if (n_container == container_name_target + and nested_container['item'] == item): + if enforce_value: + container['containers'][n_container][idx] = \ + enforce_directive_value(directive, + {'value': value, 'add_if_absent': add_directive}, + n_container, + nested_container) + else: + container['containers'][n_container][idx] = \ + append_to_container_directives(directive, + value, + nested_container) + + container['containers'][n_container][idx] = \ + _manage_directive_into_containers(directive, + value, + nested_container, + container_name_target, + item, + enforce_value, + add_directive) + + return container + + +def set_vhost_logging_directives(container, servername, logdir): + ''' + set value of CustomLog and LogFormat directives in vhost + ''' + logformat = get_directive_single_value('LogFormat', + container.get('directives', []), + default='combined') + + enforce_directive_value( + directive='CustomLog', + enforced_directive_data= + {'value': logdir + '/' + servername +'-access.log ' + logformat, + 'add_if_absent': True}, + container_name='VirtualHost', + container_data=container) + enforce_directive_value( + directive='ErrorLog', + enforced_directive_data= + {'value': logdir + '/' + servername +'-error.log ', + 'add_if_absent': True}, + container_name='VirtualHost', + container_data=container) + + return container + + +def _container_merge_multiple_directives(container): + ''' + append directives_multiple list into directives + ''' + try: + container['directives'].extend(container.get('directives_multiple', [])) + except KeyError: + container['directives'] = [] + container['directives'] = container.get('directives_multiple', []) + + container.pop('directives_multiple', None) + + for sub_container_name, sub_containers_list in container.get('containers', {}).items(): + for sub_idx, sub_container in enumerate(sub_containers_list): + container['containers'][sub_container_name][sub_idx] = \ + _container_merge_multiple_directives(sub_container) + + return container + + +def merge_container_with_additional_data(container_to_update, + container_to_import, + add_directive=True, + add_container=True): + ''' + Merge containers usually to merge default values with pillar content + + container_to_update + the default container into which put or modify values with pillar content + + container_to_import + usually pillar content + + add_directive : default=True + add directive if it is not present + + add_container : default=True + add sub_container if it is absent in container_to_update + ''' + merged_container = deepcopy(container_to_update) + multiple_directives_to_append = [] + for mult_directive_item in container_to_update.get('directives_multiple', []): + for mult_directive, imp_value in mult_directive_item.items(): + append_to_container_directives(mult_directive, + imp_value, + merged_container) + if mult_directive not in multiple_directives_to_append: + multiple_directives_to_append.append(mult_directive) + merged_container.pop('directives_multiple', None) + + for p_directive_item in container_to_import.get('directives', []): + for p_directive, p_value in p_directive_item.items(): + if p_directive in multiple_directives_to_append: + append_to_container_directives(p_directive, + p_value, + merged_container) + else: + merged_container = enforce_directive_value( + p_directive, + {'value': p_value, 'add_if_absent': add_directive}, + 'virtual_name_container', + merged_container) + # containers: + sub_containers_to_update = merged_container.get('containers', {}) + sub_containers_to_import = container_to_import.get('containers', {}) + if sub_containers_to_update and sub_containers_to_import: + # merge directives of sub containers + for container_name, u_container_list in sub_containers_to_update.items(): + to_imp_containers = sub_containers_to_import.get(container_name, []) + for container_idx, to_upd_container_data in enumerate(u_container_list): + imp_items_containers = [container for container in to_imp_containers + if container['item'] == to_upd_container_data['item']] + for i_item_container in imp_items_containers: + merged_container['containers'][container_name][container_idx] = \ + merge_container_with_additional_data( + merged_container['containers'][container_name][container_idx], + i_item_container, + add_directive) + if add_container: + # merge containers not present in default 'container_name' list + d_container_items = set([container.get('item') for container + in u_container_list]) + p_container_items = set([container.get('item') for container + in to_imp_containers]) + items_diff = (p_container_items - d_container_items) + for item in items_diff: + merged_container['containers'][container_name].extend( + [container for container in to_imp_containers if + container.get('item') == item]) + + if add_container: + # merge global containers not present in default + k_containers_diff = (set(sub_containers_to_import.keys()) + - set(sub_containers_to_update.keys())) + for k_container in k_containers_diff: + merged_container['containers'][k_container] = {} + merged_container['containers'][k_container] = sub_containers_to_import[k_container] + + elif not sub_containers_to_update \ + and sub_containers_to_import \ + and add_container: + merged_container['containers'] = {} + merged_container['containers'] = sub_containers_to_import + elif not sub_containers_to_import: + pass + + # move directives_multiple into directives and delete directives_multiple + for container_name, containers_list in merged_container.get('containers', {}).items(): + for container_idx, container_data in enumerate(containers_list): + merged_container['containers'][container_name][container_idx] = \ + _container_merge_multiple_directives(container_data) + + return merged_container + + +def enforce_security_directives_into_containers(container_to_secure, + secured_containers, + add_directive=True, + add_container=True): + ''' + Merge secured containers into pillar content + + container_to_secure + usually pillar content + + secured_containers + content of hadened values + + add_directive : default=True + add directive if it is not present + + add_container : default=True + add sub_container if it is absent in container_to_secure + ''' + i_secured_containers = {} + i_secured_containers['containers'] = secured_containers + container_to_secure = merge_container_with_additional_data( + container_to_secure, + i_secured_containers, + add_directive=add_directive, + add_container=add_container) + # search in (sub) nested containers and secure them + for secure_container_name, l_s_containers in secured_containers.items(): + for s_container in l_s_containers: + # search into container_to_secure + secured_item = s_container.get('item') + for s_directive in s_container.get('directives', []): + for s_d_label, s_d_value in s_directive.items(): + container_to_secure = _manage_directive_into_containers( + s_d_label, + s_d_value, + container_to_secure, + container_name_target=secure_container_name, + item=secured_item, + enforce_value=True, + add_directive=add_directive) + + return container_to_secure + + +def _substitute_value(text, enforced_value): + ''' + conditional replace in 'text' with regex and condition + + text + string to process + + enforced_value + dict : + match: regex to match + value: value to enforce + onlyif_pillar_is: condition on pillar content + regex_group_position: number of group to replace in regex + ''' + def my_match_function(m_object): + + return_value = ''.join([m_object.group(idx) for idx in range(1, position) + if m_object.group(idx) is not None]) + if condition == 'greater': + return_value = return_value \ + + str(min(int(m_object.group(position)), int(enforced_value['value']))) \ + + ''.join([m_object.group(idx) for idx in range(position+1, m_object.lastindex+1) if m_object.group(idx) is not None]) + elif condition == 'lower': + return_value = return_value \ + + str(max(int(m_object.group(position)), int(enforced_value['value']))) \ + + ''.join([m_object.group(idx) for idx in range(position+1, m_object.lastindex+1) if m_object.group(idx) is not None]) + + elif condition == 'different' and m_object.group(position) != str(enforced_value['value']): + return_value = return_value \ + + enforced_value['value'] \ + + ''.join([m_object.group(idx) for idx in range(position+1, m_object.lastindex+1) if m_object.group(idx) is not None]) + else: + return_value = m_object.group(0) + + return return_value + + _pattern = re.compile(enforced_value.get('match', r'(\S+(\s+\S+)*)'), re.IGNORECASE) + condition = enforced_value.get('onlyif_pillar_is', 'different') + position = enforced_value.get('regex_group_position', 1) + value = _pattern.sub(my_match_function, str(text)) + + return value + + +def enforce_directive_value(directive, + enforced_directive_data, + container_name, + container_data): + ''' + Enforce value of directive under conditions + + directive + directive label (name) + + enforced_directive_data + dict containning + value to put + condition (greater|lower|different) + regex match : default= r'(\\w+(\\s+\\w+)*)' + regex group position : default=1 + container : enforce value only on the specified container + + container_name + the name of httpd container + + container_data + container to parse + ''' + d_is_present = False + add_directive = enforced_directive_data.get('add_if_absent', False) + enforced_data_values = enforced_directive_data.get('values', [enforced_directive_data]) + for idx_d, d_item in enumerate(container_data.get('directives', [])): + if directive in d_item: + d_is_present = True + for enforced_data_value in enforced_data_values: + if (not enforced_data_value.get('container', '')) \ + or (enforced_data_value.get('container') == container_name): + container_data['directives'][idx_d][directive] = \ + _substitute_value(container_data['directives'][idx_d][directive], + enforced_data_value) + if re.match(r'(\s*)?$', container_data['directives'][idx_d][directive]) is not None: + # delete directive from list in case of + # the value is empty after replacement + del container_data['directives'][idx_d] + break + if add_directive and not d_is_present \ + and not enforced_directive_data.get('match', '') \ + and not enforced_directive_data.get('values', ''): + append_to_container_directives(directive, + enforced_directive_data.get('value'), + container_data) + + # directive is not added in subcontainers + enforced_directive_data['add_if_absent'] = False + + for sub_container_name, sub_containers in \ + container_data.get('containers', {}).items(): + container_to_match = enforced_directive_data.get('container', sub_container_name) + if container_to_match == sub_container_name: + for idx, nested_container in enumerate(sub_containers): + container_data['containers'][sub_container_name][idx] = \ + enforce_directive_value(directive, + enforced_directive_data, + sub_container_name, + nested_container) + + return container_data + + +def remove_container(container_data, + container_name_to_remove, + item_name_to_remove): + ''' + remove container_name/item from container_data + ''' + for idx, container in enumerate(container_data.get('containers', {}).get(container_name_to_remove, [])): + if container.get('item') == item_name_to_remove: + del container_data['containers'][container_name_to_remove][idx] + + for sub_container_name, sub_containers in \ + container_data.get('containers', {}).items(): + for sub_idx, sub_container in enumerate(sub_containers): + container_data['containers'][sub_container_name][sub_idx] = \ + remove_container(sub_container, container_name_to_remove, item_name_to_remove) + + return container_data diff --git a/apache/config-ng.sls b/apache/config-ng.sls new file mode 100644 index 0000000..d23bb13 --- /dev/null +++ b/apache/config-ng.sls @@ -0,0 +1,118 @@ +{% from "apache/map.jinja" import apache with context %} +{% import_yaml "apache/hardening-values.yaml" as hardening_values %} +{% import_yaml "apache/defaults/" ~ salt['grains.get']('os_family') ~ "/defaults-apache-" ~ apache.version ~ ".yaml" as global_defaults %} + +include: + - apache + - apache.mod_ssl + - apache.hardening + +{# merge defaults with pillar content #} +{% set pillar_server_config = salt['pillar.get']('apache:server_apache_config', {}) %} +{% set server_config = salt['apache_directives.merge_container_with_additional_data']( + global_defaults.server_apache_config, + pillar_server_config) %} + +{# enforce directives values #} +{% for directive, directive_data in hardening_values.enforced_directives.items() %} +{% set server_config = salt['apache_directives.enforce_directive_value'](directive, + directive_data, + container_name='server', + container_data=server_config) %} +{% endfor %} + +{# merge server config with hardened sections #} +{% set server_config = salt['apache_directives.enforce_security_directives_into_containers']( + server_config, + hardening_values.enforced_containers ) %} + +{# remove containers #} +{% for container_name_to_remove, items_names in hardening_values.containers_to_remove.items() %} +{% for item_name in items_names %} +{% set server_config = salt['apache_directives.remove_container']( + server_config, + container_name_to_remove, + item_name) %} +{% endfor %} +{% endfor %} + +{# add supplemental security directives in server configuration #} +{% for d_directive in hardening_values.server_supplemental_directives %} +{% for directive, value in d_directive.items() %} +{% set server_config = salt['apache_directives.append_to_container_directives']( + directive, + value, + server_config) %} +{% endfor %} +{% endfor %} + +{% if grains['os_family']=="RedHat" %} + +{{ apache.logdir }}: + file.directory: + - makedirs: True + - require: + - pkg: apache + - user: root + - group: {{ apache.group }} + - dir_mode: 750 + - watch_in: + - module: apache-restart + - require_in: + - module: apache-restart + - module: apache-reload + - service: apache + +{{ apache.configfile }}: + file.managed: + - template: jinja + - source: + - salt://apache/files/{{ salt['grains.get']('os_family') }}/apache-{{ apache.version }}-ng.config.jinja + - user: root + - group: root + - mode: 644 + - require: + - pkg: apache + - watch_in: + - module: apache-restart + - require_in: + - module: apache-restart + - module: apache-reload + - service: apache + - context: + apache: {{ apache }} + server_config: {{ server_config | json }} + +{{ apache.vhostdir_ng }}: + file.directory: + - makedirs: True + - require: + - pkg: apache + - user: root + - group: root + - dir_mode: 755 + - file_mode: 644 + - recurse: + - user + - group + - mode + - watch_in: + - module: apache-restart + - require_in: + - module: apache-restart + - module: apache-reload + - service: apache + + +/etc/httpd/conf.d/welcome.conf: + file.managed: + - source: + - salt://apache/files/{{ salt['grains.get']('os_family') }}/welcome.conf + - user: root + - group: root + - mode: 644 + - require: + - pkg: apache + - watch_in: + - service: apache +{% endif %} diff --git a/apache/defaults/RedHat/defaults-apache-2.4.yaml b/apache/defaults/RedHat/defaults-apache-2.4.yaml new file mode 100644 index 0000000..90ac791 --- /dev/null +++ b/apache/defaults/RedHat/defaults-apache-2.4.yaml @@ -0,0 +1,77 @@ +# defaults for httpd.conf + +# The data structure is a little bit different with pillar structure +# ``directives_multiple`` list are directives that can be present multiple time in conf file +# if the same directive is present in pillar, it will be appended to the defaults ones +# there will be no replacement of values +server_apache_config: + directives: + - ServerRoot: '"/etc/httpd"' + - AllowEncodedSlashes: 'On' + - DocumentRoot: '"/var/www"' + - ServerAdmin: 'root@localhost' + - EnableSendfile: 'on' + - ErrorLog: '"/var/log/httpd/error.log"' + - LogLevel: 'warn core:info' + - AddDefaultCharset: 'UTF-8' + - ServerTokens: 'Prod' + + containers: + Directory: + - + item: '/' + directives: + - AllowOverride: 'None' + - Require: 'all denied' + - + item: '/var/www' + directives: + - AllowOverride: 'None' + - Require: 'all granted' + - Options: 'Indexes FollowSymLinks' + - + item: '/var/www/cgi-bin' + directives: + - AllowOverride: 'None' + - Options: 'None' + - Require: 'all granted' + IfModule: + - + item: 'dir_module' + directives: + - DirectoryIndex: index.html + - + item: 'log_config_module' + directives: + - CustomLog: '"/var/log/httpd/access.log" combined' + directives_multiple: # <-- Theses directives are appended as it to pillar content + - LogFormat: '"%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined' + - LogFormat: '"%h %l %u %t \"%r\" %>s %b" common' + containers: + IfModule: + - + item: 'logio_module' + directives: + - LogFormat: '"%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio' + - + item: 'alias_module' + directives: + - ScriptAlias: '/cgi-bin/ "/var/www/cgi-bin/"' + - + item: 'mime_module' + directives: + - TypesConfig: '/etc/mime.types' + - AddOutputFilter: 'INCLUDES .shtml' + directives_multiple: + - AddType: 'application/x-compress .Z' + - AddType: 'application/x-gzip .gz .tgz' + - AddType: 'text/html .shtml' + - + item: 'mime_magic_module' + directives: + - MIMEMagicFile: 'conf/magic' + FilesMatch: + - + item: '"^\.ht"' + directives: + - Require: 'all denied' diff --git a/apache/files/RedHat/apache-2.4-ng.config.jinja b/apache/files/RedHat/apache-2.4-ng.config.jinja new file mode 100644 index 0000000..97ed636 --- /dev/null +++ b/apache/files/RedHat/apache-2.4-ng.config.jinja @@ -0,0 +1,40 @@ +# +# This file is managed by Salt! Do not edit by hand! +# +{%- from "apache/map.jinja" import apache with context %} +{%- import_yaml "apache/hardening-values.yaml" as hardening_values %} +{%- from "apache/lib.sls" import directives_output, container_output with context %} + +{%- set list_interfaces_ports = [] %} +{%- for name, vhost in salt['pillar.get']('apache:VirtualHost', {}).items() %} +{%- set items = vhost.item.split() %} +{%- for item in items if item not in list_interfaces_ports %} +{%- do list_interfaces_ports.append(item) %} +{%- endfor %} +{%- endfor %} + +{%- for item in list_interfaces_ports %} +Listen {{ item }} +{% else %} +Listen *:80 +{%- endfor %} + +{{ directives_output(server_config, 0) }} + +Include conf.modules.d/*.conf + +User {{ apache.user }} +Group {{ apache.group }} + + +{%- for container_name, container_data_list in server_config.get('containers', {}).items() %} +{%- for container_data in container_data_list %} +{{ container_output(container_name, container_data) }} +{%- endfor %} +{%- endfor %} + + +IncludeOptional {{ apache.confdir }}/*.conf +{%- if apache.vhostdir_ng != apache.confdir %} +IncludeOptional {{ apache.vhostdir_ng }}/*.conf +{%- endif %} diff --git a/apache/files/RedHat/welcome.conf b/apache/files/RedHat/welcome.conf new file mode 100644 index 0000000..9961af1 --- /dev/null +++ b/apache/files/RedHat/welcome.conf @@ -0,0 +1,15 @@ +# +# This file is managed by Salt! Do not edit by hand! +# + +# httpd welcome file commented for security reasons + +# This configuration file enables the default ""Welcome"" +# page if there is no default index page present for +# the root URL. To disable the Welcome page, comment +# out all the lines below. +# +## +## Options -Indexes +## ErrorDocument 403 /error/noindex.html +## diff --git a/apache/hardening-values.yaml b/apache/hardening-values.yaml new file mode 100644 index 0000000..dfd2e88 --- /dev/null +++ b/apache/hardening-values.yaml @@ -0,0 +1,151 @@ +enforced_directives: + # httpd directives enforced in all configuration files and sections + # data structure : + # directive: + # value: numeric or string - value to enforce + # add_if_absent: False (default) - True -> add it to server configuration if it is absent from pillar + # onlyif_pillar_is: different (default) |greater|lower -> compare numeric values + # - greater : enforce value if the pillar content is > value + # - lower : enforce value if the pillar content is < value + # match : regex + # container : enforce only on the specified container + # regex_group_position : the position of the group to substitute in regex + # values : list of dict - for multiple replacements in the same directive + + # Set TimeOut to 10 or less + Timeout: + value: 10 + onlyif_pillar_is: 'greater' + add_if_absent: True + # Set Timeout Limits for Request Headers + RequestReadTimeout: + values: + - + match: '(?<=header=)(\d+-)?(\d+)' + value: 40 + onlyif_pillar_is: 'greater' + regex_group_position: 2 + - + match: '(?<=body=)(\d+-)?(\d+)' + value: 20 + onlyif_pillar_is: 'greater' + regex_group_position: 2 + # Disable the SSL v3.0 Protocol + SSLProtocol: + value: '' + match: '(?' -%} +{{ output_indented(header_text, col) }} +{{ directives_output(container_data, col+4, default_directives ) }} +{%- for nested_container_name, nested_containers in container_data.get('containers', {}).items() %} +{%- for nested_container in nested_containers %} +{{ container_output(nested_container_name, nested_container, col+4) }} +{%- endfor %} +{%- endfor %} +{%- set footer_text = '' -%} +{{ output_indented(footer_text, col) }} +{%- endmacro %} diff --git a/apache/modules-ng.sls b/apache/modules-ng.sls new file mode 100644 index 0000000..ad6f0aa --- /dev/null +++ b/apache/modules-ng.sls @@ -0,0 +1,86 @@ +{%- import_yaml "apache/hardening-values.yaml" as hardening_values %} + +{% if grains['os_family']=="Debian" %} + +include: + - apache + +{% for module in salt['pillar.get']('apache:modules:enabled', []) %} +a2enmod {{ module }}: + cmd.run: + - unless: ls /etc/apache2/mods-enabled/{{ module }}.load + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + +{% for module in salt['pillar.get']('apache:modules:disabled', []) %} +a2dismod -f {{ module }}: + cmd.run: + - onlyif: ls /etc/apache2/mods-enabled/{{ module }}.load + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + +{% elif grains['os_family']=="RedHat" %} + +include: + - apache + +{% for module in salt['pillar.get']('apache:modules:enabled', default=hardening_values.modules.enforce_enabled, merge=True) if module not in hardening_values.modules.enforce_disabled %} +find /etc/httpd/ -name '*.conf' -type f -exec sed -i -e 's/\(^#\)\(\s*LoadModule.{{ module }}_module\)/\2/g' {} \;: + cmd.run: + - unless: httpd -M 2> /dev/null | grep "[[:space:]]{{ module }}_module" + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + +{% for module in salt['pillar.get']('apache:modules:disabled', default=hardening_values.modules.enforce_disabled, merge=True) if module not in hardening_values.modules.enforce_enabled %} +find /etc/httpd/ -name '*.conf' -type f -exec sed -i -e 's/\(^\s*LoadModule.{{ module }}_module\)/#\1/g' {} \;: + cmd.run: + - onlyif: httpd -M 2> /dev/null | grep "[[:space:]]{{ module }}_module" + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + + + +{% elif salt['grains.get']('os_family') == 'Suse' or salt['grains.get']('os') == 'SUSE' %} + +include: + - apache + +{% for module in salt['pillar.get']('apache:modules:enabled', []) %} +a2enmod {{ module }}: + cmd.run: + - unless: egrep "^APACHE_MODULES=" /etc/sysconfig/apache2 | grep {{ module }} + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + +{% for module in salt['pillar.get']('apache:modules:disabled', []) %} +a2dismod -f {{ module }}: + cmd.run: + - onlyif: egrep "^APACHE_MODULES=" /etc/sysconfig/apache2 | grep {{ module }} + - order: 225 + - require: + - pkg: apache + - watch_in: + - module: apache-restart +{% endfor %} + +{% endif %} diff --git a/apache/osfamilymap.yaml b/apache/osfamilymap.yaml index 1a30848..0efbe03 100644 --- a/apache/osfamilymap.yaml +++ b/apache/osfamilymap.yaml @@ -45,6 +45,7 @@ RedHat: mod_geoip_database: GeoIP vhostdir: /etc/httpd/vhosts.d + vhostdir_ng: /etc/httpd/conf.d confdir: /etc/httpd/conf.d confext: .conf default_site: default @@ -111,7 +112,7 @@ FreeBSD: modulesdir: /usr/local/etc/apache24/modules.d global_document_root: /usr/local/www/apache24/data - confext: + confext: default_site: default default_site_ssl: default-ssl logdir: /var/log/ diff --git a/apache/vhosts/vhost-ng.conf.jinja b/apache/vhosts/vhost-ng.conf.jinja new file mode 100644 index 0000000..267ad71 --- /dev/null +++ b/apache/vhosts/vhost-ng.conf.jinja @@ -0,0 +1,6 @@ +{% from "apache/lib.sls" import container_output with context %} +# +# This file is managed by Salt! Do not edit by hand! +# + +{{ container_output('VirtualHost', vhost_data, col=0, default_directives = []) }} diff --git a/apache/vhosts/vhost-ng.sls b/apache/vhosts/vhost-ng.sls new file mode 100644 index 0000000..2218f0e --- /dev/null +++ b/apache/vhosts/vhost-ng.sls @@ -0,0 +1,110 @@ +{% from "apache/map.jinja" import apache with context %} +{% import_yaml "apache/hardening-values.yaml" as hardening_values %} + +include: + - apache + +{% set vhosts = salt['pillar.get']('apache:VirtualHost', {}) %} + +{% for virtual_name, vhost in vhosts.items() %} + +{% set vhost_server_name = salt['apache_directives.get_directive_single_value']( + 'ServerName', + vhost.get('directives'), + default=virtual_name) %} +{% set vhost = salt['apache_directives.enforce_directive_value'](directive='ServerName', + enforced_directive_data={'value': vhost_server_name, + 'add_if_absent': True}, + container_name='VirtualHost', + container_data=vhost) %} +{% set default_documentroot = '{0}/{1}'.format(apache.wwwdir, vhost_server_name) %} +{% set documentroot = salt['apache_directives.get_directive_single_value']( + 'DocumentRoot', + vhost.get('directives'), + default=default_documentroot) %} +{% set vhost = salt['apache_directives.set_vhost_logging_directives'](vhost, + vhost_server_name, + apache.logdir) %} + +# enforce directives values # + +{% for directive, directive_data in hardening_values.enforced_directives.items() %} +{% if 'add_if_absent' in directive_data %} +{% do directive_data.update({'add_if_absent': False}) %} +{% endif %} +{% set vhost = salt['apache_directives.enforce_directive_value'](directive, + directive_data, + container_name='VirtualHost', + container_data=vhost) %} +{% endfor %} + +# merge vhost config with hardened sections # +{% set vhost = salt['apache_directives.enforce_security_directives_into_containers']( + vhost, + hardening_values.enforced_containers, + add_container=False ) %} + +# remove containers # +{% for container_name_to_remove, items_names in hardening_values.containers_to_remove.items() %} +{% for item_name in items_names %} +{% set vhost = salt['apache_directives.remove_container']( + vhost, + container_name_to_remove, + item_name) %} +{% endfor %} +{% endfor %} + +# add supplemental security directives in vhost configuration # +{% for d_directive in hardening_values.vhost_supplemental_directives %} +{% for directive, value in d_directive.items() %} +{% set vhost = salt['apache_directives.append_to_container_directives']( + directive, + value, + vhost) %} +{% endfor %} +{% endfor %} + +{% if vhost.get('absent', False) %} +{{ vhost_server_name }}: + file.absent: + - name: {{ apache.vhostdir_ng }}/{{ vhost_server_name }}{{ apache.confext }} + - require: + - pkg: apache + - watch_in: + - module: apache-reload + - require_in: + - module: apache-restart + - module: apache-reload + - service: apache + +{% else %} + + +{{ vhost_server_name }}: + file.managed: + - name: {{ apache.vhostdir_ng }}/{{ vhost_server_name }}{{ apache.confext }} + - source: 'salt://apache/vhosts/vhost-ng.conf.jinja' + - template: 'jinja' + - user: root + - group: root + - mode: 644 + - context: + vhost_data: {{ vhost|json }} + - require: + - pkg: apache + - watch_in: + - module: apache-reload + - require_in: + - module: apache-restart + - module: apache-reload + - service: apache + + +{{ documentroot }}-documentroot: + file.directory: + - name: {{ documentroot }} + - makedirs: True + - allow_symlink: True + +{% endif %} +{% endfor %} diff --git a/pillar-ng.example.yaml b/pillar-ng.example.yaml new file mode 100644 index 0000000..8be4d4c --- /dev/null +++ b/pillar-ng.example.yaml @@ -0,0 +1,122 @@ +# server configuration and any vhost configuration have the same data structure +# This data structure is similar to below : +# +# directives: # list of top level directives/values +# - directive_1: value_1 +# - directive_2: value_2 +# - directive_3: value_3 +# containers: # any type of httpd container +# container_name_1: # Files|Directory|DirectoryMatch|Proxy|location|locationMatch ... +# - +# item: 'path/to/1' # label, path or whatever that container applies to +# directives: # list of directives into this container +# - directive_1: value_1 +# ... +# - +# item: '/path/to/2' +# direcives: +# - ... +# containers: # nested containers in /path/to/2 +# nested_c_1: +# - item: '...' +# directives: +# - ... +# container_name_2: +# - +# item: '...' +# ... + +# ``apache`` formula configuration: +apache: + + # By default apache restart/reload states run (false skips) + manage_service_states: True + + # lookup section overrides ``map.jinja`` values + lookup: + server: apache2 + service: apache2 + user: some_system_user + group: some_system_group + + vhostdir: /etc/apache2/sites-available + confdir: /etc/apache2/conf.d + confext: .conf + logdir: /var/log/apache2 + wwwdir: /srv/apache2 + + # apache version (generally '2.2' or '2.4') + version: '2.2' + + # ``apache.mod_wsgi`` formula additional configuration: + mod_wsgi: mod_wsgi + + # global (server) apache directives + server_apache_config: # this content will populate httpd.conf + directives: + - AllowEncodedSlashes: 'On' + - Timeout: 5 + containers: + IfModule: + - + item: 'mime_module' + directives: + - AddType: 'application/x-font-ttf ttc ttf' + - AddType: 'application/x-font-opentype otf' + - AddType: 'application/x-font-woff woff2' + + + # ``apache.vhosts.vhost-ng`` formula additional configuration: + VirtualHost: + example.com: # <-- site_name : can be the real ServerName or a virtual name + item: '*:8080' # simple example + directives: + - ServerName: 'example.com' # if not defined default is site_name + - ServerAdmin: 'webmaster@example.com' + - DocumentRoot: '/path/to/www/dir/example.com' + - LogLevel: 'warn' + containers: + Location: + - + item: '/test.html' + directives: + - Require: 'all granted' + my_reverse_proxy: # example with a virtual site_name + item: '*:80' # vhost with proxypass + directives: + - ServerName: 'rp-example.com' + - ServerAdmin: 'webmaster@example.com' + - DocumentRoot: '/path/to/www/dir/rp-example.com' + - LogLevel: 'warn' + - ProxyPass: '/ balancer://cluster_1' + - ProxyPassReverse: '/ balancer://cluster_1' + - ProxyPreserveHost: 'On' + containers: + Proxy: + - + item: 'balancer://cluster_1' + directives: + - BalancerMember: 'http://my_backend_1:8081 route=backend-1-8081 timeout=240 retry=120' + - BalancerMember: 'http://my_backend_2:8081 route=backend-2-8081 timeout=240 retry=120' + - ProxySet: 'stickysession=JSESSIONID|jsessionid nofailover=off maxattempts=1' + unused_vhost: + item: '*:80' + absent: True # Delete this vhost + directives: + - ServerName: 'to-delete-example.com' + - ServerAdmin: 'webmaster@example.com' + - DocumentRoot: '/path/to/www/dir/to-delete-example.com' + - LogLevel: 'warn' + containers: + Location: + - + item: '/test.html' + directives: + - Require: 'all granted' + + modules: + enabled: # List modules to enable + - ldap + - ssl + disabled: # List modules to disable + - rewrite diff --git a/pillar.example b/pillar.example index f3039fd..dffd2e9 100644 --- a/pillar.example +++ b/pillar.example @@ -1,3 +1,5 @@ +# see ``pillar-ng.example.yaml`` for new gen pillar + # ``apache`` formula configuration: apache: @@ -98,7 +100,7 @@ apache: redirectmatch.com: # Use RedirectMatch Directive https://httpd.apache.org/docs/2.4/fr/mod/mod_alias.html#redirectmatch - # Require module mod_alias + # Require module mod_alias enabled: True template_file: salt://apache/vhosts/redirect.tmpl ServerName: www.redirectmatch.com @@ -368,4 +370,3 @@ apache: SSLProtocol: all -SSLv2 -SSLv3 -TLSv1 SSLHonorCipherOrder: On SSLOptions: "+StrictRequire" -