Commit aa8903c5 authored by Paul Cammish's avatar Paul Cammish
Browse files

Significantly improved PHP security

parent 835cb69f
CHANGELOG
---------
* 2019-06-16 - Significantly improved default security for PHP
- PHP is now restricted to public/, and has domain-specific tmp and
sessions directories which are automatically created.
- PHP is now disabled in a path that matches 'wp-content/uploads'
significantly securing all WordPress sites.
- Enables OSCP stapling by default. Disables HSTS by default.
- zz-mass-hosting now configures all sites, not just SSL sites.
- sympl-web-logger now only used for the zz-mass-hosting fallbacks.
- PHP defaults to blocking dangerous functions such as eval() and
exec() which should not be needed typically. This can be re-enabled
but effects all sites on the server.
- new config files: config/disable-php-security and config/hsts.
* 2019-06-13 - Improved SQL backup script
- New script with configurbility.
- Run sympl-sqldump --help for info.
......
......@@ -34,7 +34,7 @@
Alias /__sympl/ "/usr/share/sympl/static/"
<Directory "/usr/share/sympl/static/">
DirectoryIndex index.html
AllowOverride All
AllowOverride none
Require all granted
</Directory>
......@@ -46,14 +46,46 @@
ErrorDocument 403 /__sympl/index.html
</LocationMatch>
#
#
# Allow users to override settings via .htaccess
# and enables PHP in htdocs/.
#
<Directory <%=domain_directory%> >
<Directory <%= htdocs_directory %>/ >
AllowOverride all
Require all granted
php_flag engine on
</Directory>
% if php_security_disabled?
#
# Set unique tmp/ and sessions/ directories
#
php_admin_value upload_tmp_dir <%=domain_directory%>/tmp/
php_admin_value session.safe_path <%=domain_directory%>/sessions/
% else
#
# Restrict PHP directories
# Restricts PHP from leaving the public directory.
# Also sets a unique tmp directory and sessions directory.
#
php_admin_value open_basedir <%=domain_directory%>/public/
php_admin_value upload_tmp_dir <%=domain_directory%>/tmp/
php_admin_value session.safe_path <%=domain_directory%>/sessions/
#
# Prevent executing anything from a WordPress uploads directory,
# And block access to any PHP files in that directory.
#
<LocationMatch "wp-content/uploads/">
php_admin_flag engine off
</LocationMatch>
<LocationMatch "wp-content/uploads/.*\.php">
deny from all
</LocationMatch>
% end
#
# The document root
#
......@@ -89,11 +121,9 @@
</Directory>
#
# We need to log the virtual hostname the incoming request was
# made against, so that the cron-job in /etc/cron.daily may generate
# statistics for each domain.
# Write logs
#
ErrorLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/error.log"
CustomLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/access.log" combined
ErrorLog "<%= domain.log_dir %>/error.log"
CustomLog "<%= domain.log_dir %>/access.log" combined
</VirtualHost>
......@@ -28,13 +28,13 @@
<%= server_aliases %>
#
# This is the directory people are redirected to if their site is
# empty.
# This is the directory of the error page people are
# redirected to if their site is empty.
#
Alias /__sympl/ "/usr/share/sympl/static/"
<Directory "/usr/share/sympl/static/">
DirectoryIndex index.html
AllowOverride All
AllowOverride none
Require all granted
</Directory>
......@@ -43,7 +43,7 @@
#
<LocationMatch "^/+$">
Options -Indexes
ErrorDocument 403 /__sympl/
ErrorDocument 403 /__sympl/index.html
</LocationMatch>
<IfModule ssl_module>
......@@ -66,14 +66,13 @@
SSLCompression off
#
# OCSP Stapling -- make sure you remove the reject-www-data
# rule from the outgoing firewall if you use this.
# OCSP Stapling
#
SSLUseStapling off
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
% if mandatory_ssl?
% if hsts_enabled?
<IfModule headers_module>
# HSTS (mod_headers is required) (15768000 seconds = 6 months)
Header always set Strict-Transport-Security "max-age=15768000"
......@@ -83,12 +82,44 @@
#
# Allow users to override settings via .htaccess
# and eanble PHP in
#
<Directory <%=domain_directory%> >
<Directory <%=domain_directory%>/public/ >
AllowOverride all
Require all granted
php_flag engine on
</Directory>
% if php_security_disabled?
#
# Sets a unique tmp/ and sessions/ directory for the site.
#
php_admin_value upload_tmp_dir <%=domain_directory%>/php_tmp/
php_admin_value session.safe_path <%=domain_directory%>/php_sessions/
% else
#
# Restrict PHP directories
# Restricts PHP from leaving the public directory.
# Also sets a unique tmp directory and sessions directory.
#
php_admin_value open_basedir <%=domain_directory%>/public/
php_admin_value upload_tmp_dir <%=domain_directory%>/php_tmp/
php_admin_value session.safe_path <%=domain_directory%>/php_sessions/
#
# Prevent executing anything from a WordPress uploads directory,
# And block access to any PHP files in that directory.
#
<LocationMatch "wp-content/uploads/">
php_admin_flag engine off
</LocationMatch>
<LocationMatch "wp-content/uploads/.*\.php">
deny from all
</LocationMatch>
% end
#
# The document root
#
......@@ -124,12 +155,10 @@
</Directory>
#
# We need to log the virtual hostname the incoming request was
# made against, so that the cron-job in /etc/cron.daily may generate
# statistics for each domain.
# Write logs
#
ErrorLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/ssl_error.log"
CustomLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/ssl_access.log" combined
ErrorLog "<%= domain.log_dir %>/ssl_error.log"
CustomLog "<%= domain.log_dir %>/ssl_access.log" combined
</VirtualHost>
<VirtualHost <%= ips.collect{|ip| ip+":80"}.join(" ") %>>
......@@ -150,13 +179,13 @@
<%= server_aliases %>
#
# This is the directory people are redirected to if their site is
# empty.
# This is the directory of the error page people are
# redirected to if their site is empty.
#
Alias /__sympl/ "/usr/share/sympl/static/"
<Directory "/usr/share/sympl/static/">
DirectoryIndex index.html
AllowOverride All
AllowOverride none
Require all granted
</Directory>
......@@ -165,9 +194,10 @@
#
<LocationMatch "^/+$">
Options -Indexes
ErrorDocument 403 /__sympl/
ErrorDocument 403 /__sympl/index.html
</LocationMatch>
% if mandatory_ssl?
<IfModule rewrite_module>
#
......@@ -176,10 +206,11 @@
RewriteEngine On
#
# Use our server nane if HTTP_HOST is empty.
# Use our server name if HTTP_HOST is empty.
#
RewriteCond "%{HTTP_HOST}" =""
RewriteCond "%{HTTP_HOST}" = ""
RewriteRule ^/?(.*) https://<%= domain %>/$1 [R=301,L]
RewriteRule ^/?(.*) https://%{HTTP_HOST}/$1 [R=301,L]
</IfModule>
% else
......@@ -196,12 +227,44 @@
</IfModule>
#
# Allow users to override settings via .htaccess
# and enables PHP in htdocs/.
#
<Directory <%=domain_directory%> >
<Directory <%= htdocs_directory %>/ >
AllowOverride all
Require all granted
php_flag engine on
</Directory>
% if php_security_disabled?
#
# Set unique tmp/ and sessions/ directories
#
php_admin_value upload_tmp_dir <%=domain_directory%>/tmp/
php_admin_value session.safe_path <%=domain_directory%>/sessions/
% else
#
# Restrict PHP directories
# Restricts PHP from leaving the public directory.
# Also sets a unique tmp directory and sessions directory.
#
php_admin_value open_basedir <%=domain_directory%>/public/
php_admin_value upload_tmp_dir <%=domain_directory%>/tmp/
php_admin_value session.safe_path <%=domain_directory%>/sessions/
#
# Prevent executing anything from a WordPress uploads directory,
# And block access to any PHP files in that directory.
#
<LocationMatch "wp-content/uploads/">
php_admin_flag engine off
</LocationMatch>
<LocationMatch "wp-content/uploads/.*\.php">
deny from all
</LocationMatch>
% end
#
# The document root
#
......@@ -237,12 +300,10 @@
</Directory>
#
# We need to log the virtual hostname the incoming request was
# made against, so that the cron-job in /etc/cron.daily may generate
# statistics for each domain.
# Write logs
#
ErrorLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/error.log"
CustomLog "|| /usr/sbin/sympl-web-logger <%= domain.log_dir %>/access.log" combined
ErrorLog "<%= domain.log_dir %>/error.log"
CustomLog "<%= domain.log_dir %>/access.log" combined
% end
</VirtualHost>
......
......@@ -38,10 +38,9 @@
SSLCompression off
#
# OCSP Stapling -- make sure you remove the reject-www-data
# rule from the outgoing firewall if you use this.
# OCSP Stapling
#
SSLUseStapling off
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
......@@ -58,7 +57,7 @@
Alias /__sympl/ "/usr/share/sympl/static/"
<Directory "/usr/share/sympl/static/">
DirectoryIndex index.html
AllowOverride All
AllowOverride none
Require all granted
</Directory>
......@@ -67,7 +66,7 @@
#
<LocationMatch "^/+$">
Options -Indexes
ErrorDocument 403 /__sympl/
ErrorDocument 403 /__sympl/index.html
</LocationMatch>
#
......@@ -76,8 +75,13 @@
<Directory "/srv">
AllowOverride all
Require all granted
php_flag engine on
</Directory>
# There is no PHP security ( preventing access outside public/ ) for
# sites not individually configured, as the their directory is floating.
# However, any site created in /srv will have it's own config generated.
<IfModule cgi_module>
#
# We will allow global CGIs without any effort though.
......@@ -111,11 +115,25 @@
#
VirtualDocumentRoot /srv/%0/public/htdocs/
php_admin_value open_basedir /srv/%0/public/
<IfModule cgi_module>
VirtualScriptAlias /srv/%0/public/cgi-bin/
</IfModule>
</IfModule>
#
# Prevent executing anything from a WordPress uploads directory,
# And block access to any PHP files in that directory.
#
<LocationMatch "wp-content/uploads/">
php_admin_flag engine off
</LocationMatch>
<LocationMatch "wp-content/uploads/.*\.php">
deny from all
</LocationMatch>
#
# Disable any restrictions or rewrites to /.well-known/acme-challenge
# This ensures Let's Encrypt can validate domain ownership.
......
......@@ -26,7 +26,7 @@
Alias /__sympl/ "/usr/share/sympl/static/"
<Directory "/usr/share/sympl/static/">
DirectoryIndex index.html index.php
AllowOverride All
AllowOverride none
Require all granted
</Directory>
......@@ -35,17 +35,23 @@
#
<LocationMatch "^/+$">
Options -Indexes
ErrorDocument 403 /__sympl/
ErrorDocument 403 /__sympl/index.html
</LocationMatch>
#
# Allow users to override settings via .htaccess
# and enables PHP in htdocs/.
#
<Directory "/srv">
<Directory <%= htdocs_directory %>/ >
AllowOverride all
Require all granted
php_flag engine on
</Directory>
# There is no PHP security ( preventing access outside public/ ) for
# sites not individually configured, as the their directory is floating.
# However, any site created in /srv will have it's own config generated.
<IfModule cgi_module>
#
# We will allow global CGIs without any effort though.
......@@ -85,7 +91,11 @@
#
# The document root + CGI-directories.
#
VirtualDocumentRoot /srv/%0/public/htdocs/
php_admin_value open_basedir /srv/%0/public/
<IfModule cgi_module>
VirtualScriptAlias /srv/%0/public/cgi-bin/
</IfModule>
......
sympl-web (9.0.190616.0) stable; urgency=medium
* Massively improved security for PHP
* PHP is now restricted to public/, and has domain-specific tmp and
sessions directories which are automatically created.
* PHP is now disabled in a path that matches 'wp-content/uploads'
significantly securing all WordPress sites.
* Enables OSCP stapling by default. Disables HSTS by default.
* zz-mass-hosting now configures all sites, not just SSL sites.
* sympl-web-logger now only used for the zz-mass-hosting fallbacks.
* PHP defaults to blocking dangerous functions such as eval() and
exec() which should not be needed typically. This can be re-enabled
but effects all sites on the server.
* new config files: config/disable-php-security and config/hsts.
-- Paul Cammish <sympl@kelduum.net> Sun, 16 Jun 2019 22:25:00 +0100
sympl-web (9.0.190612.0) stable; urgency=medium
* Massively improved security for web stats.
......
usr/sbin/sympl-web-configure etc/cron.hourly/sympl-web-configure
usr/sbin/sympl-web-rotate-logs etc/cron.daily/sympl-web-rotate-logs
usr/share/sympl/ssl-hooks.d/sympl-web etc/sympl/ssl-hooks.d/sympl-web
etc/php/7.0/conf.d/sympl-web.ini etc/php/7.0/apache2/conf.d/00-sympl-web.ini
usr/sbin/sympl-web-configure usr/sbin/symbiosis-httpd-configure
usr/sbin/sympl-web-rotate-logs usr/sbin/symbiosis-httpd-rotate-logs
usr/sbin/sympl-web-generate-stats usr/sbin/symbiosis-httpd-generate-stats
usr/sbin/sympl-web-configure etc/cron.hourly/sympl-web-configure
usr/sbin/sympl-web-rotate-logs etc/cron.daily/sympl-web-rotate-logs
usr/share/sympl/ssl-hooks.d/sympl-web etc/sympl/ssl-hooks.d/sympl-web
etc/php/7.0/mods-available/sympl-web.ini etc/php/7.0/apache2/conf.d/00-sympl-web.ini
etc/php/7.0/mods-available/sympl-web-security.ini etc/php/7.0/apache2/conf.d/01-sympl-web-security.ini
usr/sbin/sympl-web-configure usr/sbin/symbiosis-httpd-configure
usr/sbin/sympl-web-rotate-logs usr/sbin/symbiosis-httpd-rotate-logs
usr/sbin/sympl-web-generate-stats usr/sbin/symbiosis-httpd-generate-stats
......@@ -3,6 +3,21 @@ require 'symbiosis/domain'
module Symbiosis
class Domain
#
# Checks if the PHP security should be disabled for the specific site.
#
def php_security_disabled?
get_param("disable-php-security", self.config_dir) == true
end
#
# Disabled HSTS initially, but allows it to be enabled at a later date.
#
def hsts_enabled?
get_param("hsts", self.config_dir) == true
end
#
# Checks to see if a domain should have statistics generated for it.
# Returns true if statistics should be generated, false if not.
......
[PHP]
;
; Don't add PHP to the service signature
;
expose_php = 0
;
; upload_max_filesize and post_max_size default to 2M and 8M
;
upload_max_filesize = 64M
post_max_size = 64M
[PHP]
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
[PHP]
;
; Don't add PHP to the service signature
;
expose_php = 0
;
; upload_max_filesize and post_max_size default to 2M and 8M
;
upload_max_filesize = 128M
post_max_size = 128M
;
; Disable dangerous functions by default. To override this and enable them
; you must add a seperate php configuration file.
;
; These can only be set on a server-wide basis.
;
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
......@@ -344,16 +344,37 @@ domains.each do |domain|
FileUtils.chown_R 'sympl', nil, "#{domain.directory}/public/logs", :verbose => false
end
end
# If public/ exists and php_tmp/ doesnt, then create it
if File.directory?("#{domain.directory}/php_tmp/")
unless File.directory?("#{domain.directory}/php_tmp")
verbose "\tCreating PHP tmp directory #{domain.directory}/php_tmp"
FileUtils.mkdir_p("#{domain.directory}/php_tmp")
FileUtils.chown_R 'www-data', nil, "#{domain.directory}/php_tmp", :verbose => false
end
end
if apache_mass_hosting_enabled and domain.ips.any?{|ip| primary_ips.include?(ip)}
if domain.ssl_enabled?
verbose "\tThis site has SSL enabled, and is using the host's primary IPs -- continuing with SNI."
else
verbose "\tThis site is using the host's primary IPs -- it is covered by the mass-hosting config. Skipping."
next
# If public/ exists and php_sessions/ doesnt, then create it
if File.directory?("#{domain.directory}/php_sessions/")
unless File.directory?("#{domain.directory}/php_sessions/")
verbose "\tCreating PHP sessions directory #{domain.directory}/php_sessions"
FileUtils.mkdir_p("#{domain.directory}/php_sessions")
FileUtils.chown_R 'www-data', nil, "#{domain.directory}/php_sessions", :verbose => false
end
end
#
# We no longer ant this, as all sites should have their own config created automatically.
#
# if apache_mass_hosting_enabled and domain.ips.any?{|ip| primary_ips.include?(ip)}
# if domain.ssl_enabled?
# verbose "\tThis site has SSL enabled, and is using the host's primary IPs -- continuing with SNI."
# else
# verbose "\tThis site is using the host's primary IPs -- it is covered by the mass-hosting config. Skipping."
#dd next
# end
# end
this_config = domain.apache_configuration(ssl_template, non_ssl_template, apache2_dir)
unless this_config.is_a?(Symbiosis::ConfigFiles::Apache)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment