Development: Using Caddy to harden WordPress

1. Caddy version (caddy version):

n/a

2. How I run Caddy:

a. System environment:

n/a

b. Command:

n/a

c. Service/unit/compose file:

n/a

d. My complete Caddyfile or JSON config:

n/a

3. The problem I’m having:

I’m looking to provide some Caddy examples for the WordPress support article Hardening WordPress. that presently only has a couple of Apache examples. More details in the next post.

4. Error messages and/or full log output:

n/a

5. What I already tried:

n/a

6. Links to relevant resources:

  1. Hardening WordPress

These are the two sections of the WP article of interest:

Securing wp-includes #

A second layer of protection can be added where scripts are generally not intended to be accessed by any user. One way to do that is to block those scripts using mod_rewrite in the .htaccess file. Note: to ensure the code below is not overwritten by WordPress, place it outside the # BEGIN WordPress and # END WordPress tags in the .htaccess file. WordPress can overwrite anything between these tags.

# Block the include-only files. 
<IfModule mod_rewrite.c> 
RewriteEngine On 
RewriteBase / 
RewriteRule ^wp-admin/includes/ - [F,L] 
RewriteRule !^wp-includes/ - [S=3] 
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L] 
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L] 
RewriteRule ^wp-includes/theme-compat/ - [F,L] 
</IfModule> 

# BEGIN WordPress

Note that this won’t work well on Multisite, as RewriteRule ^wp-includes/[^/]+\.php$ - [F,L] would prevent the ms-files.php file from generating images. Omitting that line will allow the code to work, but offers less security.

Securing wp-config.php #

You can move the wp-config.php file to the directory above your WordPress install. This means for a site installed in the root of your webspace, you can store wp-config.php outside the web-root folder.

Note: Some people assert that moving wp-config.php has minimal security benefits and, if not done carefully, may actually introduce serious vulnerabilities. Others disagree.

Note that wp-config.php can be stored ONE directory level above the WordPress (where wp-includes resides) installation. Also, make sure that only you (and the web server) can read this file (it generally means a 400 or 440 permission).

If you use a server with .htaccess, you can put this in that file (at the very top) to deny access to anyone surfing for it:

<files wp-config.php> 
order allow,deny 
deny from all 
</files>

I’ll see if I can come up with some Caddy equivalent code in the next post.

Initial observations on the Apache code for part 1 Securing wp-includes:

  1. In a standard WP install, paths wp-admin/includes/ and wp-includes/ exist, but paths wp-includes/js/tinymce/langs/ and wp-includes/theme-compat/ do not.
  2. When I look up RewriteRule and RewriteFlags, I think these lines…
RewriteRule !^wp-includes/ - [S=3] 
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L] 
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L] 
RewriteRule ^wp-includes/theme-compat/ - [F,L] 

… are saying ‘If wp-includes doesn’t exist then skip the next three lines’.

So, given these observations, I think this is the Caddy code replacement for a default WP install:

    @forbidden {
        path /wp-admin/includes/*
        path /wp-includes/*
    }
    respond @forbidden 403

Or would I use error amd handle_error instead of respond?

As for part II, Securing wp-config.php:
If wp-config.php isn’t moved outside the webroot, then add this to the matcher above:

        path /wp-config.php

However, if wp-config.php is moved above the webroot, I’m not so sure?

Assuming, I’m on the right track with the suggested Caddy code, would this matcher addition address the multisite concern raised here?

    @forbidden {
        not path /wp-includes/ms-files.php
        path /wp-admin/includes/*
        path /wp-includes/*
    }
    respond @forbidden "Access denied" 403

I can ignore this if it is felt that this is not a best practice approach.

Bump posts #3 and #4. I’d like to get this in the WP doc issue tracker. If the opportunity arises, I’ll raise it in tonight’s doc weekly meeting.

Looks okay to me, but @Whitestrake can probably fill in some gaps.

1 Like

This is what I’m proposing gets added to the end of each section in post #2:

Securing wp-includes #


For a Caddy web server, use a named matcher set to secure the include paths while still allowing access to ms-files.php for multisite.

    @forbidden {
        not path /wp-includes/ms-files.php
        path /wp-admin/includes/*
        path /wp-includes/*
    }
    respond @forbidden "Access denied" 403

Securing wp-config.php #


For a Caddy web server, add the wp-config.php path to the named matcher set described under Securing wp-includes. This will prevent access to wp-config.php in the webroot,

        path /wp-config.php

@francislavoie okay to proceed to a WP doc update request? @Whitestrake please let me know if I’ve missed something in the translation of the Apache code in post #2, and I’ll submit a change request in the WP doc issues tracker.

It looks like they’re very specific about targeting those three things in ./wp-includes/. I wonder if that is because there are other files in that directory that are meant to be accessible.

On its face I can’t think of any and assume that denying the entirety of those directories would be A-OK; but if that gets knocked back for some good reason, you might want to be ready to refactor to deny those paths specifically instead.

2 Likes

Thanks for your reply.

I get the feeling that the Apache code extract is:

  1. not generic; and
  2. hasn’t been updated in a long time.

What makes me believe this is that the reference to tinymce is a reference to an optional plugin Advanced Editor Tools (previously TinyMCE Advanced). Secondly, the reference to theme-compat is a reference to something to do with a very old version of WP. See What is theme-compat?.

I’m confident that the proposed Caddy code will work as I’ve been running with a not dissimilar named matcher set on my own and several customer WP sites for the last couple of years without incident.

1 Like

Assuming the proposed text/code is good to go, a WordPress doc update request has been submitted here Using Caddy to harden WordPress · Issue #22 · WordPress/Documentation-Issue-Tracker · GitHub

1 Like

A well reasoned answer. Sounds good to me!

Hmm…I’m thinking the named matcher should be inclusive of the sub-paths as well i.e.

    @forbidden {
        not path /wp-includes/ms-files.php
        path /wp-admin/includes*
        path /wp-includes*
    }
    respond @forbidden "Access denied" 403

I’ve updated the WP doc update request to reflect this adjustment.

It appears I cast the net too wide on the matcher. Some WordPress themes were impacted on. This version solves the issue:

    @forbidden {
        not path /wp-includes/ms-files.php
        path /wp-admin/includes/*.php
        path /wp-includes/*.php
    }
    respond @forbidden "Access denied" 403
1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.