DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at on

Hardening Craft CMS Permissions

Published: / Updated:

Hardening Craft CMS Permissions

An impor­tant part of hard­en­ing Craft CMS from a secu­ri­ty point of view is get­ting the file per­mis­sions right

Andrew Welch / nystudio107


Update: This arti­cle has been updat­ed to cov­er both Craft CMS 2.x and Craft CMS 3.x

Part of hard­en­ing Craft CMS is ensur­ing that the file per­mis­sions are as strict as pos­si­ble, while still allow­ing for the prop­er func­tion­ing of Craft CMS itself. File per­mis­sions are just one part of the larg­er dis­cus­sion of Secur­ing Craft.

We want the web­serv­er to be able to write to spe­cif­ic direc­to­ries so that things like asset upload­ing works, but we don’t want the web­serv­er to be able to mod­i­fy things that it should­n’t. If a secu­ri­ty exploit hap­pens, we want to mit­i­gate and con­tain the dam­age as much as pos­si­ble. Addi­tion­al­ly, prop­er per­mis­sions are need­ed for Craft CMS to even work.

Before we get into the nit­ty grit­ty, let’s review Unix file permissions.

Unix File Per­mis­sions Primer

Here is an info­graph­ic show­ing Unix file permissions:

Unix Permissions

With stan­dard Unix POSIX per­mis­sions, every file/​directory has dif­fer­ent per­mis­sions for the file owner, group, and all (every­one else). So for exam­ple, the owner of the file might be able to read & write it, users in the file’s group file might just be able to read it, and all oth­er users might not be able to access it at all.

You don’t need to know the gory details, but here’s how the per­mis­sions are expressed numerically:

Unix Permissions Numbers

For for exam­ple, this file:

-rw-r--r-- 1 admin nginx 9275 Nov 18 17:50 gulpfile.js

…is write­able & read­able by the owner admin, but can only be read by the user in the group nginx, and all oth­ers sim­i­lar­ly can only read it. No one can execute it (run it as a script or oth­er exe­cutable bina­ry). Expressed numer­i­cal­ly, the per­mis­sions would be 644.

Here’s a direc­to­ry with sim­i­lar permissions:

drwxr-xr-x 12 admin nginx 4096 Nov 18 18:21 public

You’ll notice that the execute per­mis­sion is set for the direc­to­ry owner, group, and all oth­ers. The x flag for direc­to­ries sim­ply means that those with per­mis­sion can list the files in that direc­to­ry. Expressed numer­i­cal­ly, the per­mis­sions would be 755.

A Per­mis­sions Strat­e­gy for Craft CMS

Still with me? Okay, great. Now let’s look at how we might apply this knowl­edge to Craft CMS per­mis­sions so that our Craft install is secure, but still func­tions properly.

The owner of our entire Craft CMS install should be a user oth­er than the web­serv­er user. It might be the admin account, it might be the user account you access the serv­er with, or it might be forge if you’re using serv­er pro­vi­sion­ing soft­ware like Lar­avel Forge.

The group of our entire Craft CMS install should be the web­serv­er group. We allow it to read any of the files in our Craft install so that it can serve up our web­site, but it can only write to a few spe­cif­ic directories.

Final­ly, all oth­er users can only read the files in our Craft install. If you’re real­ly para­noid, you could dis­al­low even read­ing, but it seems a bit overkill unless you’re using a shared host­ing envi­ron­ment (which you real­ly should­n’t be these days).

Noth­ing in our Craft CMS install (oth­er than direc­to­ries, and any shell scripts you might be using) needs to be executable. This is because .php files aren’t actu­al­ly exe­cut­ed, they are read in and parsed by either php or php-fpm.

Craft CMS 2.x and Craft CMS 3.x both have very sim­i­lar per­mis­sions require­ments, but the fold­er struc­ture is slight­ly dif­fer­ent. Check out the Set­ting up a New Craft CMS 3 Project arti­cle for details on the differences.

Craft CMS 2.x Permissions

The web­serv­er group needs to be able to write to:

  • craft/storage for Craft’s nor­mal operation
  • Any des­ig­nat­ed asset direc­to­ries, so that the client can upload images & oth­er assets

That’s it! The Installing Craft CMS 2.x Instruc­tions state that the web­serv­er also needs to be able to write to craft/config and craft/app, how­ev­er write access to craft/config is only need­ed to install the license.key file, and write access to craft/app is only need­ed to allow for one-click updates.

Instead, I rec­om­mend that you install the license.key file in local dev, and use what­ev­er deploy­ment tool you use to push it to your staging and live pro­duc­tion servers. Sim­i­lar­ly, I rec­om­mend that you update & test any Craft CMS updates in local dev, and then push them to staging and live pro­duc­tion. Then dis­able one-click updates on staging and live pro­duc­tion by adding this to your craft/config/general.php file:

'allowAutoUpdates' => false,

Yes, auto-updates are con­ve­nient; and you can still do them in local dev. But we real­ly want a way to test updates before deploy­ing them to live pro­duc­tion. And giv­ing the web­serv­er write access to the craft/app and craft/config direc­to­ries poten­tial­ly allows some as-yet-undis­cov­ered exploit to do bad things to our website.

If you pre­fer or require that craft/app and craft/config are write­able, that’s fine. Just go into it with eyes wide open.

Craft CMS 3.x Permissions

The web­serv­er group needs to be able to write to the fol­low­ing directories:

  • storage/ — for Craft’s nor­mal operation
  • vendor/ — this is where Com­pos­er puts its PHP pack­ages for your project
  • web/cpresources/ — this is a cache direc­to­ry for AdminCP resources
  • Any des­ig­nat­ed asset direc­to­ries, so that the client can upload images & oth­er assets

Then due to Craft CMS 3 using Com­pos­er, it also needs to be able to write to a few spe­cif­ic files as well:

  • .env — for your envi­ron­ment-spe­cif­ic vari­ables like pass­words, etc.
  • composer.json — a list of Com­pos­er pack­ages that your project requires
  • composer.lock — a list of Com­pos­er pack­ages that are installed
  • config/license.key — your Craft CMS 3 license file

That’s it! You can check out the Craft CMS 3 Instal­la­tion Instruc­tions in more depth if you like. I con­tin­ue to rec­om­mend that you don’t allow updates to be done on live pro­duc­tion or stag­ing servers, via the fol­low­ing in your config/general.php file:

'allowUpdates' => false,

This is cov­ered in-depth in the Set­ting up a New Craft CMS 3 Project arti­cle, but the basic premise is that we update and test in local devel­op­ment, and once we know every­thing works, we deploy the updates to live pro­duc­tion and/​or staging.

Shell Scripts to Make it Simple!

Don’t wor­ry, you’re not going to have to do all of this by hand. I’ve cre­at­ed some handy craft-scripts shell scripts to make set­ting Craft CMS install per­mis­sions easy. To use them, you’ll need to do the following:

  1. Down­load or clone the craft-scripts git repo
  2. Copy the scripts fold­er into the root direc­to­ry of your Craft CMS project
  3. Dupli­cate the file, and rename it to
  4. Add to your .gitignore file
  5. Then open up the file into your favorite edi­tor, and replace REPLACE_ME with the appro­pri­ate settings.

There are a num­ber of set­tings in this file, but we only need to con­cern our­selves with the fol­low­ing for set­ting file permissions:

# Local path constants; paths should always have a trailing /

# Local user & group that should own the Craft CMS install

# Local directories that should be writeable by the $CHOWN_GROUP

LOCAL_ROOT_PATH is the absolute path to the root of your local Craft install, with a trail­ing / after it.

LOCAL_ASSETS_PATH is the path to your assets direc­to­ries rel­a­tive to LOCAL_ROOT_PATH, with a trail­ing / after it.

LOCAL_CHOWN_USER is the local user that is the owner of your entire Craft install, as dis­cussed previously.

LOCAL_CHOWN_GROUP is the local web­serv­er group, usu­al­ly either nginx or apache.

LOCAL_WRITEABLE_DIRS is a quot­ed list of direc­to­ries rel­a­tive to LOCAL_ROOT_PATH that should be write­able by your webserver.

So for exam­ple, here’s what part of my looks like for this webserver:

# The path of the `craft` folder, relative to the root path; paths should always have a trailing /

# Local path constants; paths should always have a trailing /

# Local user & group that should own the Craft CMS install

# Local directories relative to LOCAL_ROOT_PATH that should be writeable by the $CHOWN_GROUP

The rea­son that both the owner and the group are both forge is because there is both a forge user, and a forge group when using Lar­avel Forge.

You might won­der why all of this is in a file, rather than in the script itself. The rea­son is so that the same scripts can be used in mul­ti­ple envi­ron­ments such as local dev, staging, and live pro­duc­tion with­out mod­i­fi­ca­tion. We just cre­ate a file in each envi­ron­ment, and keep it out of our git repo via .gitignore.

Tan­gent: For a more in-depth dis­cus­sion of mul­ti­ple envi­ron­ments, check out the Mul­ti-Envi­ron­ment Con­fig for Craft CMS article.

Alright, now that we have our all filled out, to set our file per­mis­sions we just ssh into our serv­er, cd to the scripts direc­to­ry, and type:


That’s it! If it com­plains about per­mis­sion errors, you might need to type sudo ./ instead (and you will need to type your sudo pass­word to authenticate).

For the curi­ous, here’s what the script looks like:


# Set Permissions
# Set the proper, hardened permissions for an install
# @author nystudio107
# @copyright Copyright (c) 2017 nystudio107
# @link
# @package craft-scripts
# @since 1.1.0
# @license MIT

# Get the directory of the currently executing script
DIR="$(dirname "${BASH_SOURCE[0]}")"

# Include files
    if [-f "${DIR}/${INCLUDE_FILE}"]
        source "${DIR}/${INCLUDE_FILE}"
        echo 'File "${DIR}/${INCLUDE_FILE}" is missing, aborting.'
        exit 1

# The permissions for all files & directories in the Craft CMS install
GLOBAL_DIR_PERMS=755 # `-rwxr-xr-x`
GLOBAL_FILE_PERMS=644 # `-rw-r--r--`

# The permissions for files & directories that need to be writeable
WRITEABLE_DIR_PERMS=775 # `-rwxrwxr-x`
WRITEABLE_FILE_PERMS=664 # `-rw-rw-r--`

# Set project permissions
echo "Setting base permissions for the project ${LOCAL_ROOT_PATH}"
find "${LOCAL_ROOT_PATH}" -type f ! -name "*.sh" -exec chmod $GLOBAL_FILE_PERMS {} \;

        if [-d "${FULLPATH}"]
            echo "Fixing permissions for ${FULLPATH}"
            chmod -R $WRITEABLE_DIR_PERMS "${FULLPATH}"
            find "${FULLPATH}" -type f ! -name "*.sh" -exec chmod $WRITEABLE_FILE_PERMS {} \;
            echo "Creating directory ${FULLPATH}"
            mkdir "${FULLPATH}"
            chmod -R $WRITEABLE_DIR_PERMS "${FULLPATH}"

# Normal exit
exit 0

Note that it will cre­ate any direc­to­ries you spec­i­fied in LOCAL_WRITEABLE_DIRS if they don’t exist, which is handy because craft/storage, for instance, should always be exclud­ed from your git repo via .gitignore, but Craft won’t func­tion unless it exists (and is writeable).

Once you have a set up for each envi­ron­ment, you can set the per­mis­sions in each the exact same way.

So grab craft-scripts and give ​’em a whirl. Now relax, and enjoy.

Per­mis­sions and Git

If you use git, and change file per­mis­sions on your remote serv­er, you may encounter git com­plain­ing about overwriting existing local changes when you try to deploy. This is because git con­sid­ers chang­ing the exe­cutable flag to be a change in the file, so it thinks you changed the files on your serv­er (and the changes are not checked into your git repo).

To fix this, we just need to tell git to ignore per­mis­sion changes on the serv­er. You can change the fileMode set­ting for git on your serv­er, telling it to ignore per­mis­sion changes of the files on the server:

git config --global core.fileMode false

See the git-con­fig man page for details.

The oth­er way to fix this is to set the per­mis­sion using in local dev, and then check the files into your git repo. This will cause them to be saved with the cor­rect per­mis­sions in your git repo to begin with.

The down­side to the lat­ter approach is that you must have match­ing user/​groups in both local dev and on live production.

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Top comments (0)