File permissions Wordpress - Write Only

Hello, I am trying to harden the wordpress security and I am going to restrict permissions.

  • directories: 555 since with 550 fails to load css files
  • php files: 400 - so far wordpress seems to work

I know that no one will be able to update themes / plugins / wp core and I am also aware that users won’t be able to upload files to the media folder
cache plugins may fail too if they cannot create new folders.

Apart from that, is there anything else I shold take into account?

The idea is to create some scripts:

  • to put everything in read-only mode
  • to be able to update php files
  • to be able to upload files and post content

First of all what are the “most” easiest way to hack an website

  • Execute php files in /wp-content/uploads/

Nginx does by default:

Something should also be done for Apache2 imo

  • User adding “unknown” / new plugins
    If you don’t want your users to update / install new plugins / themes don’t provide them with access to allow them / Don’t give them administrator rights

  • Outdated plugins and core with bugs
    Update regularly

Thank you @eris for your reply. Just applied it to my testing.tpl and everything works fine so far.

Should we update default.tpl for the apache + nginx option to include that policy?

location ~* /(?:uploads|files)/.*.php$ {
deny all;
return 404;

It should not harm other CMSs and it increases the security.

As for the question since I am willing to update the permissions. Can you help me with that?

Easiest method to secure it to change the php user to www-data

Excuse my ignorance. Should I then change those lines for this?

user = www-data
group = %user%

Then I will put it in a readonly.tpl in the fpm templates folder and whenever I want to set a WP to readonly I only have to switch the template.

If that’s the case, do you want me to submit a PR with this?

The only thing it needs to be complete is to have a toggle button in a wp plugin so it activates this for an hour or so and then switches back to readonly.tpl


Ideally we should make it so it copies with v-add-web-php x.x

And make it suitable for that version like the multiphp.tpl template

And replace user = %user% with www-data

Thank you @eris. One more thing. In this case, if fpm is run as www-data, and cannot write, the cache plugins will fail. Is that correct? or am I missing something?

Will the nginx cache template work in this case?

Have a look a this plugin: It can really help to block access to plugins area, zero-day exploit prevention, login attempts etc.

Also, make sure your Security Headers are set correctly. If you need a template for that, let me know.

Thank you @chris. Two things:

I like the plugin and it was in the to do things so I will definitely give a try. To block by country… maybe it would be faster to do it at a server level with ipsets? Or it is okay to use both tools. Maybe this one to spot offender countries and then manually ban countries with ipsets.

I have no idea what a security header is :s so I suppose I need templates for that.

Do give it a try and see all the settings for yourself. I think you can you both options at the same time. I use this plugin to block all access to backend, redirecting every IP outside my preset countries to the frontend. (For the Geo location API settings I use both the IP2Location and the GeoLite2 api. For the last one you can sign up for free at Maxmind

For the security headers, here you can read about them:

Security headers are directives used by web applications to configure security defenses in web browsers. Based on these directives, browsers can make it harder to exploit client-side vulnerabilities such as Cross-Site Scripting or Clickjacking. Headers can also be used to configure the browser to only allow valid TLS communication and enforce valid certificates, or even enforce using a specific server certificate.

With this tool you can scan your website’s to see their score:

Edit the's and put this -at the bottom- of your .htaccess, and scan the website again for an A+ result.

# START Security Headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "no-referrer-when-downgrade"
Header always set Expect-CT "max-age=7776000, enforce"
Header set Access-Control-Allow-Origin "null"
Header set Access-Control-Allow-Methods "GET,PUT,POST,DELETE"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
Header set X-Content-Security-Policy "img-src *; media-src * data:;"
Header always set Content-Security-Policy "report-uri"
Header set Cross-Origin-Embedder-Policy-Report-Only 'unsafe-none; report-to="default"'
Header set Cross-Origin-Embedder-Policy 'unsafe-none; report-to="default"'
Header set Cross-Origin-Opener-Policy-Report-Only 'same-origin; report-to="default"'
Header set Cross-Origin-Opener-Policy 'same-origin; report-to="default"'
Header set Cross-Origin-Resource-Policy 'cross-origin'
Header set strict-dynamic "https: 'self'; default-src 'self'"
Header always set X-Frame-Options "ALLOWALL"
Header set X-Powered-By ""
Header always set Permissions-Policy "geolocation=(self), microphone=(), accelerometer=(), gyroscope=(), fullscreen=(), magnetometer=()"
Header set X-Permitted-Cross-Domain-Policies "none"
Header set Feature-Policy "camera 'none'; fullscreen 'self'; geolocation *; microphone 'self'*"
# END Security Headers

nginx cache uses by default www-data as user as that is the user nginx is running under…

Will work fine with the new api system…

1 Like

I could append this lines to the .htaccess just on domain creation and create a “secure-all” script with:

cat .htaccess | grep ‘START Security Headers’ && echo “Ok” || cat security_headers.txt >> .htaccess

The only thing that I need to have a perfect cache solution is the ability to define different expire times policies depending on the URL so that I can have the pages that are updated frequently updated soon but those that receive no visits, have a cached version just in case Google pays a visit.

The thing is that if I remember correctly the method used to purge the cache is equivalent to rm -Rf * so… that can’t be easily done.

If the plugin could pass a list of urls to be purged to the API and then only the elements of the list would be purged that would be awesome.

For now it is the best solution so far so… we’ll give it a try at the office.