diff --git a/.github/workflows/kitchen.vagrant.yml b/.github/workflows/kitchen.vagrant.yml new file mode 100644 index 0000000..92e007b --- /dev/null +++ b/.github/workflows/kitchen.vagrant.yml @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# vim: ft=yaml +--- +name: 'Kitchen Vagrant (FreeBSD)' +'on': ['push', 'pull_request'] + +env: + KITCHEN_LOCAL_YAML: 'kitchen.vagrant.yml' + +jobs: + test: + runs-on: 'macos-10.15' + strategy: + fail-fast: false + matrix: + instance: + # - default-freebsd-122-latest-py3 + - freebsd-122-latest-py3 + # - default-freebsd-114-latest-py3 + - freebsd-114-latest-py3 + steps: + - name: 'Check out code' + uses: 'actions/checkout@v2' + - name: 'Set up Bundler cache' + uses: 'actions/cache@v1' + with: + path: 'vendor/bundle' + key: "${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}" + restore-keys: "${{ runner.os }}-gems-" + - name: 'Run Bundler' + run: | + ruby --version + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: 'Run Test Kitchen' + run: 'bundle exec kitchen verify ${{ matrix.instance }}' diff --git a/Gemfile b/Gemfile index c08ad6c..8be107e 100644 --- a/Gemfile +++ b/Gemfile @@ -12,3 +12,7 @@ gem 'kitchen-docker', git: 'https://gitlab.com/saltstack-formulas/infrastructure # rubocop:enable Layout/LineLength gem 'kitchen-inspec', '>= 2.2.1' gem 'kitchen-salt', '>= 0.6.3' + +group :vagrant do + gem 'kitchen-vagrant' +end diff --git a/Gemfile.lock b/Gemfile.lock index d7cbc3b..ef26d96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -292,6 +292,8 @@ GEM kitchen-salt (0.6.3) hashie (>= 3.5) test-kitchen (>= 1.4) + kitchen-vagrant (1.7.0) + test-kitchen (>= 1.4, < 3) libyajl2 (1.2.0) license-acceptance (1.0.19) pastel (~> 0.7) @@ -525,6 +527,7 @@ DEPENDENCIES kitchen-docker! kitchen-inspec (>= 2.2.1) kitchen-salt (>= 0.6.3) + kitchen-vagrant BUNDLED WITH 2.1.2 diff --git a/apache/files/FreeBSD/apache-2.4.config.jinja b/apache/files/FreeBSD/apache-2.4.config.jinja index 5c0100c..024945c 100644 --- a/apache/files/FreeBSD/apache-2.4.config.jinja +++ b/apache/files/FreeBSD/apache-2.4.config.jinja @@ -1,3 +1,7 @@ +# +# This file is managed by Salt! Do not edit by hand! +# + # # This is the main Apache HTTP server configuration file. It contains the # configuration directives that give the server its instructions. diff --git a/apache/files/FreeBSD/modsecurity.conf.jinja b/apache/files/FreeBSD/modsecurity.conf.jinja new file mode 100644 index 0000000..02cdead --- /dev/null +++ b/apache/files/FreeBSD/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 by Salt! Do not edit by hand! +# 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 "(?:application(?:/soap\+|/)|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 reaches 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/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/modsecurity.yaml b/apache/modsecurity.yaml index 858d6ff..a3a9627 100644 --- a/apache/modsecurity.yaml +++ b/apache/modsecurity.yaml @@ -23,3 +23,10 @@ Suse: manage_config: false package: apache2-mod_security2 config_file: /etc/apache2/conf.d/mod_security2.conf + +FreeBSD: + mod_security: + crs_install: false + manage_config: false + package: ap24-mod_security + config_file: /usr/local/etc/modsecurity/modsecurity.conf diff --git a/apache/osfamilymap.yaml b/apache/osfamilymap.yaml index 42497d6..e8d0d35 100644 --- a/apache/osfamilymap.yaml +++ b/apache/osfamilymap.yaml @@ -202,7 +202,9 @@ FreeBSD: confext: '' default_site: default default_site_ssl: default-ssl + moddir: /usr/local/etc/apache24/modules.d logdir: /var/log/ + logrotatedir: /usr/local/etc/logrotate.d/apache2 wwwdir: /usr/local/www/apache24/ OpenBSD: diff --git a/docs/README.rst b/docs/README.rst index 1e29b6f..49117f0 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -325,7 +325,7 @@ e.g. ``debian-9-2019-2-py3``. ``bin/kitchen converge`` ^^^^^^^^^^^^^^^^^^^^^^^^ -Creates the docker instance and runs the ``template`` main state, ready for testing. +Creates the docker instance and runs the ``apache`` main states, ready for testing. ``bin/kitchen verify`` ^^^^^^^^^^^^^^^^^^^^^^ @@ -346,3 +346,65 @@ Runs all of the stages above in one go: i.e. ``destroy`` + ``converge`` + ``veri ^^^^^^^^^^^^^^^^^^^^^ Gives you SSH access to the instance for manual testing. + +Testing with Vagrant +-------------------- + +Windows/FreeBSD/OpenBSD testing is done with ``kitchen-salt``. + +Requirements +^^^^^^^^^^^^ + +* Ruby +* Virtualbox +* Vagrant + +Setup +^^^^^ + +.. code-block:: bash + + $ gem install bundler + $ bundle install --with=vagrant + $ bin/kitchen test [platform] + +Where ``[platform]`` is the platform name defined in ``kitchen.vagrant.yml``, +e.g. ``windows-81-latest-py3``. + +Note +^^^^ + +When testing using Vagrant you must set the environment variable ``KITCHEN_LOCAL_YAML`` to ``kitchen.vagrant.yml``. For example: + +.. code-block:: bash + + $ KITCHEN_LOCAL_YAML=kitchen.vagrant.yml bin/kitchen test # Alternatively, + $ export KITCHEN_LOCAL_YAML=kitchen.vagrant.yml + $ bin/kitchen test + +Then run the following commands as needed. + +``bin/kitchen converge`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +Creates the Vagrant instance and runs the ``apache`` main states, ready for testing. + +``bin/kitchen verify`` +^^^^^^^^^^^^^^^^^^^^^^ + +Runs the ``inspec`` tests on the actual instance. + +``bin/kitchen destroy`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Removes the Vagrant instance. + +``bin/kitchen test`` +^^^^^^^^^^^^^^^^^^^^ + +Runs all of the stages above in one go: i.e. ``destroy`` + ``converge`` + ``verify`` + ``destroy``. + +``bin/kitchen login`` +^^^^^^^^^^^^^^^^^^^^^ + +Gives you RDP/SSH access to the instance for manual testing. diff --git a/kitchen.vagrant.yml b/kitchen.vagrant.yml new file mode 100644 index 0000000..cb9c6d9 --- /dev/null +++ b/kitchen.vagrant.yml @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# vim: ft=yaml +--- +driver: + name: vagrant + cache_directory: false + customize: + usbxhci: 'off' + gui: false + linked_clone: true + ssh: + shell: /bin/sh + +platforms: + - name: freebsd-122-latest-py3 + driver: + box: bento/freebsd-12.2 + - name: freebsd-114-latest-py3 + driver: + box: bento/freebsd-11.4 + +provisioner: + salt_install: bootstrap diff --git a/test/integration/default/controls/config_spec.rb b/test/integration/default/controls/config_spec.rb index 5847900..e9f0f18 100644 --- a/test/integration/default/controls/config_spec.rb +++ b/test/integration/default/controls/config_spec.rb @@ -28,9 +28,11 @@ control 'apache configuration' do sitesdir = '/etc/httpd/sites-enabled' when 'bsd' vhostdir = '/usr/local/etc/apache24/Includes' - # logrotatedir = ? - # moddir = '?' - # sitesdir = '?' + logrotatedir = '/usr/local/etc/logrotate.d/apache2' + moddir = '/usr/local/etc/apache24/modules.d' + # https://docs.freebsd.org/en/books/handbook/network-servers/#_virtual_hosting + # All done under `/usr/local/etc/apache24/httpd.conf` + sitesdir = '/usr/local/etc/apache24' end describe file(vhostdir) do it { should exist } @@ -56,6 +58,7 @@ end control 'apache configuration (unique)' do title 'should be valid' + config_file_group = 'root' case platform[:family] when 'debian' config_file = '/etc/apache2/apache2.conf' @@ -74,11 +77,12 @@ control 'apache configuration (unique)' do wwwdir = '/srv/http' when 'bsd' config_file = '/usr/local/etc/apache24/httpd.conf' + config_file_group = 'wheel' wwwdir = '/usr/local/www/apache24/' end describe file(config_file) do it { should be_file } - it { should be_grouped_into 'root' } + it { should be_grouped_into config_file_group } its('mode') { should cmp '0644' } its('content') do should include( diff --git a/test/integration/modules/controls/config_spec.rb b/test/integration/modules/controls/config_spec.rb index eb8443e..43d7eb0 100644 --- a/test/integration/modules/controls/config_spec.rb +++ b/test/integration/modules/controls/config_spec.rb @@ -34,9 +34,11 @@ control 'apache configuration' do when 'bsd' vhostdir = '/usr/local/etc/apache24/Includes' logdir = '/var/log' - # logrotatedir = ? - # moddir = '?' - # sitesdir = '?' + logrotatedir = '/usr/local/etc/logrotate.d/apache2' + moddir = '/usr/local/etc/apache24/modules.d' + # https://docs.freebsd.org/en/books/handbook/network-servers/#_virtual_hosting + # All done under `/usr/local/etc/apache24/httpd.conf` + sitesdir = '/usr/local/etc/apache24' end describe command(apachectl) do its('stdout') { should eq '' } @@ -72,6 +74,7 @@ end control 'apache configuration (unique)' do title 'should match desired lines' + config_file_group = 'root' case platform[:family] when 'debian' config_file = '/etc/apache2/apache2.conf' @@ -90,11 +93,12 @@ control 'apache configuration (unique)' do wwwdir = '/srv/http' when 'bsd' config_file = '/usr/local/etc/apache24/httpd.conf' + config_file_group = 'wheel' wwwdir = '/usr/local/www/apache24/' end describe file(config_file) do it { should be_file } - it { should be_grouped_into 'root' } + it { should be_grouped_into config_file_group } its('mode') { should cmp '0644' } its('content') do should include( diff --git a/test/integration/modules/controls/mod_security_spec.rb b/test/integration/modules/controls/mod_security_spec.rb index 825eb7d..115c00c 100644 --- a/test/integration/modules/controls/mod_security_spec.rb +++ b/test/integration/modules/controls/mod_security_spec.rb @@ -8,20 +8,30 @@ control 'apache mod_security configuration' do end modspec_file = - case platform[:family] + case system.platform[:family] when 'redhat', 'fedora' '/etc/httpd/conf.d/mod_security.conf' when 'debian' '/etc/modsecurity/modsecurity.conf-recommended' when 'suse' '/etc/apache2/conf.d/mod_security2.conf' + when 'bsd' + '/usr/local/etc/modsecurity/modsecurity.conf' + end + + modspec_file_group = + case system.platform[:family] + when 'bsd' + 'wheel' + else + 'root' end describe file(modspec_file) do it { should be_file } its('mode') { should cmp '0644' } its('owner') { should eq 'root' } - its('group') { should eq 'root' } + its('group') { should eq modspec_file_group } its('content') { should match(/SecRuleEngine On/) } its('content') { should match(/SecRequestBodyAccess On/) } its('content') { should match(/SecRequestBodyLimit 14000000/) } diff --git a/test/integration/modules/controls/server_status_spec.rb b/test/integration/modules/controls/server_status_spec.rb index 51f6802..de86ed0 100644 --- a/test/integration/modules/controls/server_status_spec.rb +++ b/test/integration/modules/controls/server_status_spec.rb @@ -13,22 +13,31 @@ control 'apache server_status configuration' do SS_STANZA confdir = - case platform[:family] + case system.platform[:family] when 'debian' '/etc/apache2/conf-available' when 'redhat', 'fedora' '/etc/httpd/conf.d' when 'suse' '/etc/apache2/conf.d' - # `linux` here is sufficient for `arch` - when 'linux' + when 'arch' '/etc/httpd/conf/extra' + when 'bsd' + '/usr/local/etc/apache24/extra' end - describe file("#{confdir}/server-status.conf") do + conffile, conffile_group = + case system.platform[:family] + when 'bsd' + %W[#{confdir}/server-status wheel] + else + %W[#{confdir}/server-status.conf root] + end + + describe file(conffile) do it { should be_file } it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + it { should be_grouped_into conffile_group } its('mode') { should cmp '0644' } its('content') { should include '# File managed by Salt' } its('content') { should include server_status_stanza }