/ 4 min read

ISPConfig 3.3.3 Blocks the Nginx include Directive - Here's How to Allow It

I updated an ISPConfig server to 3.3.3 this weekend, then went to add a new site and found that the nginx Directives box on the Options tab would no longer accept an include line. The same directives had been fine on every other site on the box. Saving the site just gave this:

Nginx directive blocked by security settings:
include

3.3.3 is a security release, and this turns out to be deliberate. The release notes say the directive check “refuses more directives” without listing them, so this post is mostly me writing down what I found so I don’t have to work it out again on the next server.

What changed in 3.3.3

Two things matter here:

  1. The validation on the Apache and nginx Directives fields got stricter. Existing websites keep whatever directives they already have, but they are checked again the next time the site is saved, so this will bite you on old sites too, just later.
  2. The nginx check is now driven by a blacklist file, and a custom copy of that file replaces the shipped one.

The release notes only mention the second point in passing, in the context of the temp-path directives (client_body_temp_path and friends) that are now refused. They don’t mention include at all.

The shipped blacklist

The list lives at /usr/local/ispconfig/security/nginx_directives.blacklist. It is a set of PCRE patterns, one per line, and on 3.3.3 it contains exactly two:

/(^|[;{}])\s*["']?(load_module|error_log|include|client_body_temp_path|proxy_temp_path|fastcgi_temp_path|uwsgi_temp_path|scgi_temp_path)["']?(\s|;|$)/mi
/(^|[;{}])\s*["']?access_log["']?(\s|;|$)(?!\s*["']?off["']?\s*;)/mi

There’s include, sitting between error_log and the temp paths.

A few notes on what the patterns actually do:

  • (^|[;{}])\s* anchors the match to the start of a line or to just after a ;, { or }. That means include is caught whether it is on its own line, tucked after another directive, or inside a location {} block. The check doesn’t parse context, it just scans for the word in a directive position.
  • error_log and the temp-path directives are blocked for the same reason as include: they all take an arbitrary filesystem path. If a reseller has been given the Options tab, any directive that reads or writes a path of their choosing is a way out of their own sites.
  • access_log is allowed, but only as access_log off;. The negative lookahead rejects anything else.
  • load_module isn’t even valid inside a server {} block, so that one is belt and braces.

The override

Copy the file, drop include| from the first pattern, and save it with a .custom suffix:

cd /usr/local/ispconfig/security
cp nginx_directives.blacklist nginx_directives.blacklist.custom
sed -i 's/error_log|include|/error_log|/' nginx_directives.blacklist.custom
chown root:ispconfig nginx_directives.blacklist.custom
chmod 750 nginx_directives.blacklist.custom

The result should be:

/(^|[;{}])\s*["']?(load_module|error_log|client_body_temp_path|proxy_temp_path|fastcgi_temp_path|uwsgi_temp_path|scgi_temp_path)["']?(\s|;|$)/mi
/(^|[;{}])\s*["']?access_log["']?(\s|;|$)(?!\s*["']?off["']?\s*;)/mi

Then re-save the website in the panel. The check runs in the interface on save, so there is nothing to resync for this specifically.

The ownership and permissions matter. The shipped file is -rwxr-x--- root:ispconfig. The panel’s PHP runs as the ispconfig user, so if the custom file is root:root 644 or similar the interface may not be able to read it and will silently fall back to the shipped list. Matching the shipped file exactly avoids any guesswork.

Two gotchas:

  • The custom file replaces the shipped one, it doesn’t extend it. If you create a .custom with only a single line in it, you’ve just removed every other check as well. Copy the whole file and edit the one bit you need.
  • Future updates change the shipped file but not yours. Diff the two after each ISPConfig update and pull across anything new.

Should you do this?

The threat model behind the block is a reseller with access to the Options tab, or a compromised admin session. On a server where you are the only admin and nobody else can see that tab, re-allowing include gives up very little.

If you want a middle ground, narrow the pattern instead of removing it. A negative lookahead lets include through only for a directory you control:

/(^|[;{}])\s*["']?(load_module|error_log|include(?!\s+\/etc\/nginx\/snippets\/)|client_body_temp_path|proxy_temp_path|fastcgi_temp_path|uwsgi_temp_path|scgi_temp_path)["']?(\s|;|$)/mi

That blocks include /var/www/clients/...; and include /etc/passwd; but allows include /etc/nginx/snippets/wordpress.conf;. Most of the protection stays and your own snippets keep working.

Alternatives that don’t touch the blacklist

  • Paste the snippet contents directly into the nginx Directives field. Ugly, and you lose the single-file-many-sites benefit, but it needs no server changes.
  • Edit the vhost template instead. Copy /usr/local/ispconfig/server/conf/nginx_vhost.conf.master to /usr/local/ispconfig/server/conf-custom/ and put the include there. This applies to every site on the server, so it suits shared security headers or bot blocking but not per-site includes. Run Tools > Resync > Websites afterwards.
  • Directive snippets (System > Directive Snippets) are not a workaround. 3.3.3 also gated them behind admin_allow_server_config and, as far as I can tell, runs them through the same validation.

Other 3.3.3 things worth knowing

While I was in there:

  • The nginx vhost template changed in 3.3.3, so run Tools > Resync > Websites once after the update regardless.
  • Directives and snippets are now written into the vhost as plain text. Any <tmpl_if> or <tmpl_var> tags in them are no longer processed and will make nginx refuse the vhost. The supported placeholders are {DOCROOT}, {DOCROOT_CLIENT}, {DOMAIN}, {FASTCGIPASS}, ##merge## and ##delete##.
  • There’s a new Security Check page for admins. Expect it to flag a customised blacklist. That’s fine on a single-admin box.

Sources: ISPConfig 3.3.3 release notes.

Robert Went

Freelance PHP developer with 19+ years experience, specialising in technical SEO, WordPress, and Laravel.

More about Rob