From 2be52db8b8e167657b3ba0cda07053e993641f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20B=C3=A9rtoli?= Date: Tue, 10 May 2016 22:48:26 -0300 Subject: [PATCH] Add mod_security management Add gitignore, kitchen-ci files Add some tests and documentation --- .gitignore | 2 + .kitchen.yml | 54 ++++ README.rst | 7 + apache/files/Debian/modsecurity.conf.jinja | 241 ++++++++++++++++++ apache/files/RedHat/modsecurity.conf.jinja | 75 ++++++ apache/map.jinja | 9 + apache/mod_security.sls | 46 ++++ apache/modsecurity.yaml | 15 ++ pillar.example | 23 +- .../serverspec/mod_security_spec.rb | 29 +++ test/shared/spec_helper.rb | 9 + 11 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 .kitchen.yml create mode 100644 apache/files/Debian/modsecurity.conf.jinja create mode 100644 apache/files/RedHat/modsecurity.conf.jinja create mode 100644 apache/mod_security.sls create mode 100644 apache/modsecurity.yaml create mode 100644 test/integration/mod_security/serverspec/mod_security_spec.rb create mode 100644 test/shared/spec_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cc873f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +.kitchen diff --git a/.kitchen.yml b/.kitchen.yml new file mode 100644 index 0000000..114ed91 --- /dev/null +++ b/.kitchen.yml @@ -0,0 +1,54 @@ +--- +driver: + name: vagrant + +platforms: + - name: debian-jessie64 + driver_config: + box: ssplatt/salt-deb-8 + - name: centos-7 + driver_config: + box: rchrd/centos-7-x64-salt + - name: centos-6 + driver_config: + box: rchrd/centos-6-x64-salt + +provisioner: + name: salt_solo + salt_version: 2015.8.8 + data_path: test/shared + is_file_root: true + pillars: + top.sls: + base: + '*': + - apache + apache.sls: + apache: + mod_security: + crs_install: True + manage_config: True + sec_rule_engine: 'On' + sec_request_body_access: 'On' + sec_request_body_limit: '14000000' + sec_request_body_no_files_limit: '114002' + sec_request_body_in_memory_limit: '114002' + sec_request_body_limit_action: 'Reject' + sec_pcre_match_limit: '15000' + sec_pcre_match_limit_recursion: '15000' + sec_debug_log_level: '3' + +suites: + - name: apache + provisioner: + state_top: + base: + '*': + - apache + - name: mod_security + provisioner: + state_top: + base: + '*': + - apache + - apache.mod_security diff --git a/README.rst b/README.rst index 6c1be23..6044f3b 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,13 @@ Installs and enables the mod_fcgid module Installs and enables the mod_dav_svn module (Debian only) +``apache.mod_security`` +---------------------- + +Installs an enables the `Apache mod_security2 WAF``_ +using data from Pillar. (Debian and RedHat Only) + +Allows you to install the basic Core Rules (CRS) and some basic configuration for mod_security2 ``apache.mod_vhost_alias`` ---------------------- diff --git a/apache/files/Debian/modsecurity.conf.jinja b/apache/files/Debian/modsecurity.conf.jinja new file mode 100644 index 0000000..baee065 --- /dev/null +++ b/apache/files/Debian/modsecurity.conf.jinja @@ -0,0 +1,241 @@ +{%- set apache = pillar.get('apache', {}) %} +{%- set modsec = apache.get('mod_security', {}) %} +{%- set sec_rule_engine = modsec.get('sec_rule_engine', 'DetectionOnly' ) -%} +{%- set sec_request_body_access = modsec.get('sec_request_body_access', 'On' ) -%} +{%- set sec_request_body_limit = modsec.get('sec_request_body_limit', 13107200 ) -%} +{%- set sec_request_body_no_files_limit = modsec.get('sec_request_body_no_files_limit', 131072 ) -%} +{%- set sec_request_body_in_memory_limit = modsec.get('sec_request_body_in_memory_limit', 131072 ) -%} +{%- set sec_request_body_limit_action = modsec.get('sec_request_body_limit_action', 'Reject' ) -%} +{%- set sec_pcre_match_limit = modsec.get('sec_pcre_match_limit', 1000 ) -%} +{%- set sec_pcre_match_limit_recursion = modsec.get('sec_pcre_match_limit_recursion', 1000 ) -%} +{%- set sec_debug_log_level = modsec.get('sec_debug_log_level', 0 ) -%} +# +# This file is managed/autogenerated by salt. +# Modify the salt pillar that generates this file instead +# +# -- Rule engine initialization ---------------------------------------------- + +# Enable ModSecurity, attaching it to every transaction. Use detection +# only to start with, because that minimises the chances of post-installation +# disruption. +# +SecRuleEngine {{ sec_rule_engine }} + + +# -- Request body handling --------------------------------------------------- + +# Allow ModSecurity to access request bodies. If you don't, ModSecurity +# won't be able to see any POST parameters, which opens a large security +# hole for attackers to exploit. +# +SecRequestBodyAccess {{ sec_request_body_access }} + + +# Enable XML request body parser. +# Initiate XML Processor in case of xml content-type +# +SecRule REQUEST_HEADERS:Content-Type "text/xml" \ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + +# Enable JSON request body parser. +# Initiate JSON Processor in case of JSON content-type; change accordingly +# if your application does not use 'application/json' +# +SecRule REQUEST_HEADERS:Content-Type "application/json" \ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + +# Maximum request body size we will accept for buffering. If you support +# file uploads then the value given on the first line has to be as large +# as the largest file you are willing to accept. The second value refers +# to the size of data, with files excluded. You want to keep that value as +# low as practical. +# +SecRequestBodyLimit {{ sec_request_body_limit }} +SecRequestBodyNoFilesLimit {{ sec_request_body_no_files_limit }} + +# Store up to 128 KB of request body data in memory. When the multipart +# parser reachers this limit, it will start using your hard disk for +# storage. That is slow, but unavoidable. +# +SecRequestBodyInMemoryLimit {{ sec_request_body_in_memory_limit }} + +# What do do if the request body size is above our configured limit. +# Keep in mind that this setting will automatically be set to ProcessPartial +# when SecRuleEngine is set to DetectionOnly mode in order to minimize +# disruptions when initially deploying ModSecurity. +# +SecRequestBodyLimitAction {{ sec_request_body_limit_action }} + +# Verify that we've correctly processed the request body. +# As a rule of thumb, when failing to process a request body +# you should reject the request (when deployed in blocking mode) +# or log a high-severity alert (when deployed in detection-only mode). +# +SecRule REQBODY_ERROR "!@eq 0" \ +"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + +# By default be strict with what we accept in the multipart/form-data +# request body. If the rule below proves to be too strict for your +# environment consider changing it to detection-only. You are encouraged +# _not_ to remove it altogether. +# +SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ +"id:'200003',phase:2,t:none,log,deny,status:400, \ +msg:'Multipart request body failed strict validation: \ +PE %{REQBODY_PROCESSOR_ERROR}, \ +BQ %{MULTIPART_BOUNDARY_QUOTED}, \ +BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ +DB %{MULTIPART_DATA_BEFORE}, \ +DA %{MULTIPART_DATA_AFTER}, \ +HF %{MULTIPART_HEADER_FOLDING}, \ +LF %{MULTIPART_LF_LINE}, \ +SM %{MULTIPART_MISSING_SEMICOLON}, \ +IQ %{MULTIPART_INVALID_QUOTING}, \ +IP %{MULTIPART_INVALID_PART}, \ +IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ +FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + +# Did we see anything that might be a boundary? +# +SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ +"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" + +# PCRE Tuning +# We want to avoid a potential RegEx DoS condition +# +SecPcreMatchLimit {{ sec_pcre_match_limit }} +SecPcreMatchLimitRecursion {{ sec_pcre_match_limit_recursion }} + +# Some internal errors will set flags in TX and we will need to look for these. +# All of these are prefixed with "MSC_". The following flags currently exist: +# +# MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. +# +SecRule TX:/^MSC_/ "!@streq 0" \ + "id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" + + +# -- Response body handling -------------------------------------------------- + +# Allow ModSecurity to access response bodies. +# You should have this directive enabled in order to identify errors +# and data leakage issues. +# +# Do keep in mind that enabling this directive does increases both +# memory consumption and response latency. +# +SecResponseBodyAccess On + +# Which response MIME types do you want to inspect? You should adjust the +# configuration below to catch documents but avoid static files +# (e.g., images and archives). +# +SecResponseBodyMimeType text/plain text/html text/xml + +# Buffer response bodies of up to 512 KB in length. +SecResponseBodyLimit 524288 + +# What happens when we encounter a response body larger than the configured +# limit? By default, we process what we have and let the rest through. +# That's somewhat less secure, but does not break any legitimate pages. +# +SecResponseBodyLimitAction ProcessPartial + + +# -- Filesystem configuration ------------------------------------------------ + +# The location where ModSecurity stores temporary files (for example, when +# it needs to handle a file upload that is larger than the configured limit). +# +# This default setting is chosen due to all systems have /tmp available however, +# this is less than ideal. It is recommended that you specify a location that's private. +# +SecTmpDir /tmp/ + +# The location where ModSecurity will keep its persistent data. This default setting +# is chosen due to all systems have /tmp available however, it +# too should be updated to a place that other users can't access. +# +SecDataDir /tmp/ + + +# -- File uploads handling configuration ------------------------------------- + +# The location where ModSecurity stores intercepted uploaded files. This +# location must be private to ModSecurity. You don't want other users on +# the server to access the files, do you? +# +#SecUploadDir /opt/modsecurity/var/upload/ + +# By default, only keep the files that were determined to be unusual +# in some way (by an external inspection script). For this to work you +# will also need at least one file inspection rule. +# +#SecUploadKeepFiles RelevantOnly + +# Uploaded files are by default created with permissions that do not allow +# any other user to access them. You may need to relax that if you want to +# interface ModSecurity to an external program (e.g., an anti-virus). +# +#SecUploadFileMode 0600 + + +# -- Debug log configuration ------------------------------------------------- + +# The default debug log configuration is to duplicate the error, warning +# and notice messages from the error log. +# +#SecDebugLog /opt/modsecurity/var/log/debug.log +SecDebugLogLevel {{ sec_debug_log_level }} + + +# -- Audit log configuration ------------------------------------------------- + +# Log the transactions that are marked by a rule, as well as those that +# trigger a server error (determined by a 5xx or 4xx, excluding 404, +# level response status codes). +# +SecAuditEngine RelevantOnly +SecAuditLogRelevantStatus "^(?:5|4(?!04))" + +# Log everything we know about a transaction. +SecAuditLogParts ABIJDEFHZ + +# Use a single file for logging. This is much easier to look at, but +# assumes that you will use the audit log only ocassionally. +# +SecAuditLogType Serial +SecAuditLog /var/log/apache2/modsec_audit.log + +# Specify the path for concurrent audit logging. +#SecAuditLogStorageDir /opt/modsecurity/var/audit/ + + +# -- Miscellaneous ----------------------------------------------------------- + +# Use the most commonly used application/x-www-form-urlencoded parameter +# separator. There's probably only one application somewhere that uses +# something else so don't expect to change this value. +# +SecArgumentSeparator & + +# Settle on version 0 (zero) cookies, as that is what most applications +# use. Using an incorrect cookie version may open your installation to +# evasion attacks (against the rules that examine named cookies). +# +SecCookieFormat 0 + +# Specify your Unicode Code Point. +# This mapping is used by the t:urlDecodeUni transformation function +# to properly map encoded data to your language. Properly setting +# these directives helps to reduce false positives and negatives. +# +SecUnicodeMapFile unicode.mapping 20127 + +# Improve the quality of ModSecurity by sharing information about your +# current ModSecurity version and dependencies versions. +# The following information will be shared: ModSecurity version, +# Web Server version, APR version, PCRE version, Lua version, Libxml2 +# version, Anonymous unique id for host. +SecStatusEngine On + diff --git a/apache/files/RedHat/modsecurity.conf.jinja b/apache/files/RedHat/modsecurity.conf.jinja new file mode 100644 index 0000000..c41baf1 --- /dev/null +++ b/apache/files/RedHat/modsecurity.conf.jinja @@ -0,0 +1,75 @@ +{%- set apache = pillar.get('apache', {}) %} +{%- set modsec = apache.get('mod_security', {}) %} +{%- set sec_rule_engine = modsec.get('sec_rule_engine', 'DetectionOnly' ) -%} +{%- set sec_request_body_access = modsec.get('sec_request_body_access', 'On' ) -%} +{%- set sec_request_body_limit = modsec.get('sec_request_body_limit', 13107200 ) -%} +{%- set sec_request_body_no_files_limit = modsec.get('sec_request_body_no_files_limit', 131072 ) -%} +{%- set sec_request_body_in_memory_limit = modsec.get('sec_request_body_in_memory_limit', 131072 ) -%} +{%- set sec_request_body_limit_action = modsec.get('sec_request_body_limit_action', 'Reject' ) -%} +{%- set sec_pcre_match_limit = modsec.get('sec_pcre_match_limit', 1000 ) -%} +{%- set sec_pcre_match_limit_recursion = modsec.get('sec_pcre_match_limit_recursion', 1000 ) -%} +{%- set sec_debug_log_level = modsec.get('sec_debug_log_level', 0 ) -%} +# +# This file is managed/autogenerated by salt. +# Modify the salt pillar that generates this file instead +# + +LoadModule security2_module modules/mod_security2.so + + + LoadModule unique_id_module modules/mod_unique_id.so + + + # ModSecurity Core Rules Set configuration + Include modsecurity.d/*.conf + Include modsecurity.d/activated_rules/*.conf + + # Default recommended configuration + SecRuleEngine {{ sec_rule_engine }} + SecRequestBodyAccess {{ sec_request_body_access }} + SecRule REQUEST_HEADERS:Content-Type "text/xml" \ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRequestBodyLimit {{ sec_request_body_limit }} + SecRequestBodyNoFilesLimit {{ sec_request_body_no_files_limit }} + SecRequestBodyInMemoryLimit {{ sec_request_body_in_memory_limit }} + SecRequestBodyLimitAction {{ sec_request_body_limit_action }} + SecRule REQBODY_ERROR "!@eq 0" \ + "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ + "id:'200002',phase:2,t:none,log,deny,status:44,msg:'Multipart request body \ + failed strict validation: \ + PE %{REQBODY_PROCESSOR_ERROR}, \ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ + DB %{MULTIPART_DATA_BEFORE}, \ + DA %{MULTIPART_DATA_AFTER}, \ + HF %{MULTIPART_HEADER_FOLDING}, \ + LF %{MULTIPART_LF_LINE}, \ + SM %{MULTIPART_MISSING_SEMICOLON}, \ + IQ %{MULTIPART_INVALID_QUOTING}, \ + IP %{MULTIPART_INVALID_PART}, \ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + + SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ + "id:'200003',phase:2,t:none,log,deny,status:44,msg:'Multipart parser detected a possible unmatched boundary.'" + + SecPcreMatchLimit {{ sec_pcre_match_limit }} + SecPcreMatchLimitRecursion {{ sec_pcre_match_limit_recursion }} + + SecRule TX:/^MSC_/ "!@streq 0" \ + "id:'200004',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" + + SecResponseBodyAccess Off + SecDebugLog /var/log/httpd/modsec_debug.log + SecDebugLogLevel {{ sec_debug_log_level }} + SecAuditEngine RelevantOnly + SecAuditLogRelevantStatus "^(?:5|4(?!04))" + SecAuditLogParts ABIJDEFHZ + SecAuditLogType Serial + SecAuditLog /var/log/httpd/modsec_audit.log + SecArgumentSeparator & + SecCookieFormat 0 + SecTmpDir /var/lib/mod_security + SecDataDir /var/lib/mod_security + diff --git a/apache/map.jinja b/apache/map.jinja index bdbcbd9..a06bb79 100644 --- a/apache/map.jinja +++ b/apache/map.jinja @@ -1,4 +1,5 @@ {% import_yaml "apache/osfingermap.yaml" as osfingermap %} +{% import_yaml "apache/modsecurity.yaml" as modsec %} {% set apache = salt['grains.filter_by']({ 'Debian': { @@ -116,3 +117,11 @@ }, grain='oscodename', merge=salt['grains.filter_by']( osfingermap , grain='osfinger', merge=salt['pillar.get']('apache:lookup')))) %} + +{% set modsecurity = salt['grains.filter_by']( + modsec +, grain='os_family', merge=salt['pillar.get']('apache:mod_security')) or {} %} + +{# merge the os family/codename mod_sec's specific data over the defaults #} +{% do apache.update({ 'mod_security': modsecurity }) %} + diff --git a/apache/mod_security.sls b/apache/mod_security.sls new file mode 100644 index 0000000..9c5f353 --- /dev/null +++ b/apache/mod_security.sls @@ -0,0 +1,46 @@ +{% from "apache/map.jinja" import apache with context %} + +include: + - apache + +mod-security: + pkg.installed: + - name: {{ apache.mod_security.package }} + - order: 180 + - require: + - pkg: apache + +{% if apache.mod_security.crs_install %} +mod-security-crs: + pkg.installed: + - name: {{ apache.mod_security.crs_package }} + - order: 180 + - require: + - pkg: mod-security +{% endif %} + +{% if apache.mod_security.manage_config %} +mod-security-main-config: + file.managed: + - name: {{ apache.mod_security.config_file }} + - order: 220 + - template: jinja + - source: + - {{ 'salt://apache/files/' ~ salt['grains.get']('os_family') ~ '/modsecurity.conf.jinja' }} + - context: {{ apache.mod_security }} + - require: + - pkg: mod-security + - watch_in: + - module: apache-reload +{% endif %} + +{% if grains['os_family']=="Debian" %} +a2enmod security2: + cmd.run: + - unless: ls /etc/apache2/mods-enabled/security2.load && ls /etc/apache2/mods-enabled/security2.conf + - order: 225 + - require: + - pkg: mod-security + - watch_in: + - module: apache-restart +{% endif %} diff --git a/apache/modsecurity.yaml b/apache/modsecurity.yaml new file mode 100644 index 0000000..af4b9f0 --- /dev/null +++ b/apache/modsecurity.yaml @@ -0,0 +1,15 @@ +default: + crs_install: False + manage_config: False +Debian: + crs_install: False + manage_config: False + package: libapache2-mod-security2 + crs_package: modsecurity-crs + config_file: /etc/modsecurity/modsecurity.conf-recommended +RedHat: + crs_install: False + manage_config: False + package: mod_security + crs_package: mod_security_crs + config_file: /etc/httpd/conf.d/mod_security.conf diff --git a/pillar.example b/pillar.example index 2e76be3..5b8cad7 100644 --- a/pillar.example +++ b/pillar.example @@ -18,15 +18,16 @@ apache: # ``apache.mod_wsgi`` formula additional configuration: mod_wsgi: mod_wsgi + global: # global apache directives - AllowEncodedSlashes: "On" + AllowEncodedSlashes: 'On' name_virtual_hosts: - - interface: * + - interface: '*' port: 80 - - interface: * + - interface: '*' port: 443 # ``apache.vhosts`` formula additional configuration: @@ -142,3 +143,19 @@ apache: RemoteIPTrustedProxy: - 10.0.8.0/24 - 127.0.0.1 + + # ``apache.mod_security`` formula additional configuration: + mod_security: + crs_install: True + # If not set, default distro's configuration is installed as is + manage_config: True + sec_rule_engine: 'On' + sec_request_body_access: 'On' + sec_request_body_limit: '14000000' + sec_request_body_no_files_limit: '114002' + sec_request_body_in_memory_limit: '114002' + sec_request_body_limit_action: 'Reject' + sec_pcre_match_limit: '15000' + sec_pcre_match_limit_recursion: '15000' + sec_debug_log_level: '3' + diff --git a/test/integration/mod_security/serverspec/mod_security_spec.rb b/test/integration/mod_security/serverspec/mod_security_spec.rb new file mode 100644 index 0000000..ae5c0c0 --- /dev/null +++ b/test/integration/mod_security/serverspec/mod_security_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../kitchen/data/spec_helper' + +describe 'apache.mod_security' do + + case os[:family] + when 'redhat' + modspec_file = '/etc/httpd/conf.d/mod_security.conf' + when 'debian', 'ubuntu' + modspec_file = '/etc/modsecurity/modsecurity.conf-recommended' + else + # No other supported ATM + end + + describe file(modspec_file) do + it { should exist } + it { should be_mode 644 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + its(:content) { should match /SecRuleEngine On/ } + its(:content) { should match /SecRequestBodyAccess On/ } + its(:content) { should match /SecRequestBodyLimit 14000000/ } + its(:content) { should match /SecRequestBodyNoFilesLimit 114002/ } + its(:content) { should match /SecRequestBodyInMemoryLimit 114002/ } + its(:content) { should match /SecRequestBodyLimitAction Reject/ } + its(:content) { should match /SecPcreMatchLimit 15000/ } + its(:content) { should match /SecPcreMatchLimitRecursion 15000/ } + its(:content) { should match /SecDebugLogLevel 3/ } + end +end diff --git a/test/shared/spec_helper.rb b/test/shared/spec_helper.rb new file mode 100644 index 0000000..d62fc6d --- /dev/null +++ b/test/shared/spec_helper.rb @@ -0,0 +1,9 @@ +require "serverspec" +require "pathname" + +# Set backend type +set :backend, :exec + +RSpec.configure do |c| + c.path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +end