Home Anatomy of a hardened Apache2 configuration
Post
Me, my desk, and I.
Paid Plan

Anatomy of a hardened Apache2 configuration

Apache

Some Suggestions

This guide applies to Apache 2.4+.

I cover most things here, but anything else is easily found at the Apache2 documentation site.

So first I want to suggest that you break up your configuration files, and then include each site (and other major config types) into the main server config. Your configuration file with server wide settings will be called apache2.conf, or sometimes httpd.conf and is usually going to be located in the
/etc/apache2/ directory. Under this main apache2 configuration directory, you will probably also see some other directories, and these are going to be included from your primary configuration file later. The three we will be using today will be mods-available / mods-enabled, sites-available / sites-enabled, and conf-available / conf-enabled. Now you can really organize all this however you want, so long as you include everything correctly. But I suggest using the default scheme, as most people, as well as documentation, follows this setup.

The Primary Config File

Now that that is out of the way, we’re going to take a look at the current config, if you have just installed apache2 from your package manager, it is going to look very generic, like this (except with a lot of documentation commented out, I will be omitting):

Hint: You can document your own configuration as you like by adding a hash “#” character in front of what you want to remind yourself or someone else!

ServerRoot "/etc/apache2"
Mutex file:${APACHE_LOCK_DIR} default
PidFile ${APACHE_PID_FILE}
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
HostnameLookups Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
Include ports.conf
<Directory />
  Options FollowSymLinks
  AllowOverride None
  Require all denied
</Directory>
<Directory /usr/share>
  AllowOverride None
  Require all denied
</Directory>
<Directory /var/www/>
  Options Indexes FollowSymLinks
  AllowOverride None
  Require all granted
</Directory>
AccessFileName .htaccess
<FilesMatch "^\.ht">
  Require all denied
</FilesMatch>
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf

Most of these things should be left in, and can be unchanged.

So the first line ServerRoot "/etc/apache2" just tells us what directory all our configuration things will be housed in. It should never have a trailing slash.

Our first security type feature is the User and Group declerations, which tell the apache2 binary what priveleges to run under. This is important because if the server does happen to get pwnt, then the attacker does not get higher privs than what are listed here.

Warning: NEVER set either of these to root or your own personal account user.

Your next two important security type directives are ErrorLog, and LogLevel. I would recommend leaving these the same, except if you are having issues you are welcome to change the log level to anything from trace1 to trace8, as well as debug, info, notice, warn, error, crit, alert, emerg. This will give you some debugging output. Be careful not to leave the debugging output on in production, this will probably cause performance issues under load.

Your next three important things are your single Include files and your IncludeOptional directories, each with files to include in their first level. These will be important later on.

Now the only Directory directive that I leave in my prod servers is:

<Directory />
  Options FollowSymLinks
  AllowOverride None
  Require all denied
</Directory>

So that in no way can the root directory of our server be accessed. We first tell it to follow every simlink under, and then explicityly disallow reading of this directory by anything client side, and allows no possiblity of .htaccess override.

We also then completely disallow client side loading of any file beginning with .ht. This is a reference to .htaccess and .htpasswd, in case those are anywhere that are currently being served, and in the case of .htaccess, it will be.

You’ll also have your LogFormat directives that tell the server how to generate your error.log and access.log. Unless otherwise specified, these will appear in /var/log/apache2/.

Now Some Security Focused Additions

I’ll share a somewhat redacted version of one of my own apache2.conf files. Some these things may need tweaking for your intended use case, please don’t just copy/paste it.

DefaultRuntimeDir ${APACHE_RUN_DIR}
PidFile ${APACHE_PID_FILE}
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
HostnameLookups Off
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf
Include ports.conf
SetEnv SERVER_ADMIN youremail@yoursite.com
SetEnv TZ America/New_York
DefaultLanguage en-US
LoadModule headers_module modules/mod_headers.so
Header always set X-XSS-Protection "1; mode=block"
Header always append X-Frame-Options deny
Header always set X-Content-Type-Options "nosniff"
Header always set Content-Security-Policy "default-src 'self' data: blob: cdn.jsdelivr.net;"
Header always set Content-Security-Policy "style-src 'self' *.google.com googleapis.com;"
Header always set Content-Security-Policy "script-src 'self' nonce-76dbe9426bd20a5ffbe536edc407958201af3a03 *.google.com;"
Header always set Content-Security-Policy "frame-src: 'self' feed.mikle.com;"
Header always set Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"
Header always set Referrer-Policy "no-referrer-when-downgrade"
Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure;SameSite=None
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
ServerSignature Off
ServerTokens Prod
AccessFileName .htaccess
TraceEnable Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" detailed
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
<Directory />
  Options +FollowSymLinks
  AllowOverride None
  Require all denied
</Directory>

<FilesMatch "^\.ht">
  Require all denied
</FilesMatch>

Note: You can add the next few things I talk about to /etc/apache2/conf-enabled/security.conf, or keep them all in one place if you would like!

So lots of this is the same with some minor modifications, but I’ll go over things you should consider adding.

First, in case of unexpected errors, you should include your email in the server config so a user can contact you in case of a service outage. We do this with the SetEnv SERVER_ADMIN server environment variable.

We shoudl also turn on inline header addition and modification for the next couple lines, we’ll do this with LoadModule headers_module modules/mod_headers.so. As a result now we can set things like server-side XSS protection with: Header always set X-XSS-Protection "1; mode=block" and it’s companion options: Header always append X-Frame-Options deny and Header always set X-Content-Type-Options "nosniff". We do this via headers because some of this needs to direct our clients, the web browsers, how to interact wtih some of the data we share with them.

Another thing we can accomplish with header addition is using our CSP, or Content Security Policy, basically directing via the server what the client browser can pull and use on the page from an external source, and where specifically they can pull it from. We do this with Header always set Content-Security-Policy. If I say wanted to be able to run externally hosted javascript from js.google.com or script.google.com, we could set it as such: Header always set Content-Security-Policy "script-src 'self' *.google.com;". As you can see, there are different types, for javascript, iframes, css, etc.

We can also set our Referrer-Policy header, which is important in the case of other insecure websites picking up which page our clients were on last. For my uses, I have set mine to no-referrer-when-downgrade, but you can also set to a number of others including: “no-referrer”, “no-referrer-when-downgrade”, “same-origin”, “origin”, “strict-origin”, “origin-when-cross-origin”, “strict-origin-when-cross-origin”, and “unsafe-url”. Each has it’s own pace, and you can read more about each policy specifically here.

Our Permissions-Policy header also should be set on most modern webservers. This dictates what rights our server has to access on the client browser’s computer, like a camera, or their fine location. Your site and what you use the browser for is going to tell you what permissions you need, so here I will deferr to a nice Permissions Policy Generator I’ve found useful that can automagically generate this string for you without errors. I suggest only asking for what you really need, as asking for too many “unused” permissions is a great way to turn a user away.

One cool thing we can do with the Header edit directive is do things like edit a cookie inline before sending it to the user, so say, we want to bump all cookies to Httponly;Secure; we would use the regular expression: Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure; as a catchall.

I normally also remove access to insecure SSL protocols when connecting to my server. There is nothing worse than thinking your connection is secure, when it is not. You can do this by using the SSLProtocol directive and setting it as you wish. I set mine: SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1 for good measure.

If you would like to restrict access to a specific directory to only the user “marshall”, you can do so by creating a password file with htpasswd, ex: htpasswd -c /etc/apache2/htpasswd.sec marshall, and then adding a section to your config file that looks something like this:

<Location "/secret">
  Header set Cache-Control "no-cache"
  AllowOverride All
  <RequireAll>
    Require user marshall
    AuthType Basic
    AuthName "Secret Content"
    AuthUserFile /etc/apache2/htpasswd.sec
  </RequireAll>
</Location>

If you would like to add another user the ability to access that directory, you could create their access token with htpasswd, and then simply add their username to the line Requre user marshall [here]. If instead you would like to restrict access to an IP address, you could use the line Require ip 123.123.123.123, or specificy their hostname with Require host oxagast.org. To negate the access, add the keyword not between Require and ip/host/user.

It is important to make sure our now secured server does not leak internal server information when an attacker is poking at it, generating error codes. So now that our server is tightened up, we should set TraceEnable Off, ServerSignature Off and finally ServerTokens Prod. Now our errors should be able to be read, and seen as an error by an end user, but won’t leak important serverside information like IP addresses and server versions. For future reference, trace should always be disabled on an internet facing production server, as it is an unsafe HTTP method.

The Site

Now we can head over to /etc/apache2/sites-available and create your site. I’ll assume you already have SSL/HTTPS setup and working for your site, where it be with LetsEncrypt, or something else. I’ll also assume you have a working site with at least a main page and a subdirectory with something in it.

I’ll show you a simple config of one of my sites with some more sensitive things removed.

So in /etc/apache2/sites-available I have the following file called site.com-ssl.conf. There should also a symbolic link that links this file to /etc/apache2/sites-enabled so that the data is parsed by then served by apache2.

<IfModule mod_ssl.c>
<VirtualHost *:443>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
  ServerAdmin marshall@site.com
  DocumentRoot /var/www/site.com
  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
  ServerName site.com
  ServerAlias site.com
  Include /etc/letsencrypt/options-ssl-apache.conf
  SSLCertificateFile /etc/letsencrypt/live/site.com/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/site.com/privkey.pem
</VirtualHost>
</IfModule>
<Directory "/var/www/site.com/">
  Options -Indexes +FollowSymLinks -ExecCGI
  AllowOverride all
  Require all granted
  Header always set Access-Control-Allow-Origin "*"
  RewriteEngine on
  ErrorDocument 403 https://site.com/403.html
  ErrorDocument 404 https://site.com/404.html
  RewriteCond %{REQUEST_METHOD} !^(GET|HEAD|POST|OPTIONS)$ [NC]
  RewriteRule .? - [F,NS,L]
  RewriteCond %{THE_REQUEST} !^[A-Z]{3,9}\ .+\ HTTP/(0\.9|1\.0|1\.1) [NC]
  RewriteRule .? - [F,NS,L]
</Directory>
<Location "/code/">
  AddType text/plain .txt .cpp .c .pl .sh .py
  Options +Indexes +FollowSymLinks -ExecCGI
  require all granted
  AllowOverride None
</Location>

So as you can see, a rather generic apache2 setup with a couple additions.

You will need a DocumentRoot at minimum pointing to the lowest directory you would like to serve for this site.

Our Directory tags point to a place on the filesystem that we would like to add the enclosed directives to. The Location tags enclose a location that is served by the webserver that we would like to add the directives to. This is an important distinction.

If we are not using CGI (like Perl or PHP) in this directory, make sure to add -ExecCGI to the Options directive. You may also add or omit Indexes, with either a plus or minus, to tell it weather we want a directory listing in child directories or not. I leave this off here.

Assuming we have our header modifications on, we can have Header always set Access-Control-Allow-Origin "*" for access control, if you are not pulling from anything else, you can set this to your site only to tighten it up a little bit.

We want our url rewrite engine on so that we can tell it ONLY to alllow safe HTTP methods with RewriteCond %{REQUEST_METHOD} !^(GET|HEAD|POST|OPTIONS)$ [NC] and then RewriteRule .? - [F,NS,L], and we would like to only have valid HTTP requests come through, hence the next directive RewriteCond %{THE_REQUEST} !^[A-Z]{3,9}\ .+\ HTTP/(0\.9|1\.0|1\.1) [NC] and RewriteRule .? - [F,NS,L].

To wrap it up, we can optionally run a HTTP server as well as our HTTPS server, and just use the HTTP to bump over to HTTPS with a rewrite, so if you would like to do this, create a file called site.com.conf that looks something like this:

<VirtualHost *:80>
  ServerAdmin marshall@site.org
  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
  RewriteEngine on
  RewriteCond %{SERVER_NAME} =site.com
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Conclusion

Now you’ll just need to run systemctl restart apache2 to restart the server with your new settings. Now that we have it tightened up, you may have a little more peace of mind against hackers. However, always make sure to check your logs for suspicious code.

Feel free to email me with comments and suggestions!

Happy serving!


If you enjoy my work, sponsor or hire me! I work hard keeping oxasploits running!
Bitcoin Address:
bc1qclqhff9dlvmmuqgu4907gh6gxy8wy8yqk596yp

Thank you so much and happy hacking!
This post is licensed under CC BY 4.0 by the author.