Hardening Craft CMS Permissions
Tags: craftcms, devops, security
Published: 2020-02-14 22:57:17

---
title: Hardening Craft CMS Permissions
published: true
date: 2016-12-05 05:42:00 UTC
tags: craftcms,devops,security
canonical_url: https://nystudio107.com/blog/hardening-craft-cms-permissions
---

Published: <time datetime="Mon, 05 Dec 2016 00:42:00 -0500">2016.12.05</time> / Updated: <time datetime="Mon, 03 Jun 2019 11:29:00 -0400">2019.06.03</time>

# 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](https://nystudio107.com)

![Security](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/security.jpg)

**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](https://craftcms.com/support/securing-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](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_576x191_crop_center-center_100_line/unix_permissions.png)

With stan­dard Unix POSIX per­mis­sions, every file/​directory has dif­fer­ent per­mis­sions for the file <kbd>o</kbd>wner, <kbd>g</kbd>roup, and <kbd>a</kbd>ll (every­one else). So for exam­ple, the <kbd>o</kbd>wner of the file might be able to <kbd>r</kbd>ead & <kbd>w</kbd>rite it, users in the file’s <kbd>g</kbd>roup file might just be able to <kbd>r</kbd>ead it, and <kbd>a</kbd>ll 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](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_576x174_crop_center-center_100_line/unix_permissions_numbers.png)

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 <kbd>o</kbd>wner <kbd>admin</kbd>, but can only be <kbd>r</kbd>ead by the user in the group <kbd>nginx</kbd>, and <kbd>a</kbd>ll oth­ers sim­i­lar­ly can only <kbd>r</kbd>ead it. No one can e<kbd>x</kbd>ecute it (run it as a script or oth­er exe­cutable bina­ry). Expressed numer­i­cal­ly, the per­mis­sions would be <kbd>644</kbd>.

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 e<kbd>x</kbd>ecute per­mis­sion is set for the direc­to­ry <kbd>o</kbd>wner, <kbd>g</kbd>roup, and <kbd>a</kbd>ll oth­ers. The <kbd>x</kbd> 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 <kbd>755</kbd>.

## 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 <kbd>o</kbd>wner of our entire Craft CMS install should be a user _oth­er than_ the web­serv­er user. It might be the <kbd>admin</kbd> account, it might be the user account you access the serv­er with, or it might be <kbd>forge</kbd> if you’re using serv­er pro­vi­sion­ing soft­ware like [Lar­avel Forge](https://forge.laravel.com/).

<aside>
                                The own­er should be the only user that is able to write to every file in your Craft <span>CMS</span> install.
                            </aside>

The <kbd>g</kbd>roup of our entire Craft CMS install should be the web­serv­er group. We allow it to <kbd>r</kbd>ead any of the files in our Craft install so that it can serve up our web­site, but it can only <kbd>w</kbd>rite to a few spe­cif­ic directories.

Final­ly, <kbd>a</kbd>ll oth­er users can only <kbd>r</kbd>ead 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 e<kbd>x</kbd>ecutable. This is because <kbd>.php</kbd> files aren’t actu­al­ly exe­cut­ed, they are read in and parsed by either <kbd>php</kbd> or <kbd>php-fpm</kbd>.

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](https://nystudio107.com/blog/setting-up-a-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:

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

That’s it! The [Installing Craft CMS 2.x Instruc­tions](http://docs.craftcms.com/v2/installing.html#pre-flight-check) state that the web­serv­er also needs to be able to write to <kbd>craft/config</kbd> and <kbd>craft/app</kbd>, how­ev­er write access to <kbd>craft/config</kbd> is only need­ed to install the <kbd>license.key</kbd> file, and write access to <kbd>craft/app</kbd> is only need­ed to allow for one-click updates.

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

```

'allowAutoUpdates' => false,

```

Yes, auto-updates are con­ve­nient; and you can still do them in <kbd>local</kbd> dev. But we real­ly want a way to test updates before deploy­ing them to <kbd>live</kbd> pro­duc­tion. And giv­ing the web­serv­er write access to the <kbd>craft/app</kbd> and <kbd>craft/config</kbd> 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 <kbd>craft/app</kbd> and <kbd>craft/config</kbd> 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:

- <kbd>storage/</kbd> — for Craft’s nor­mal operation
- <kbd>vendor/</kbd> — this is where Com­pos­er puts its PHP pack­ages for your project
- <kbd>web/cpresources/</kbd> — this is a cache direc­to­ry for AdminCP resources
- Any des­ig­nat­ed <kbd>asset</kbd> 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:

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

That’s it! You can check out the [Craft CMS 3 Instal­la­tion Instruc­tions](http://docs.craftcms.com/v3/installation.html#step-1-download-craft) in more depth if you like. I con­tin­ue to rec­om­mend that you don’t allow updates to be done on <kbd>live</kbd> pro­duc­tion or stag­ing servers, via the fol­low­ing in your <kbd>config/general.php</kbd> file:

```

'allowUpdates' => false,

```

This is cov­ered in-depth in the [Set­ting up a New Craft CMS 3 Project](https://nystudio107.com/blog/setting-up-a-craft-cms-3-project) arti­cle, but the basic premise is that we update and test in <kbd>local</kbd> devel­op­ment, and once we know every­thing works, we deploy the updates to <kbd>live</kbd> 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](https://github.com/nystudio107/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](https://github.com/nystudio107/craft-scripts) git repo
2. Copy the <kbd>scripts</kbd> fold­er into the root direc­to­ry of your Craft CMS project
3. Dupli­cate the <kbd>example.env.sh</kbd> file, and rename it to <kbd>.env.sh</kbd>
4. Add <kbd>.env.sh</kbd> to your <kbd>.gitignore</kbd> file
5. Then open up the <kbd>.env.sh</kbd> file into your favorite edi­tor, and replace <kbd>REPLACE_ME</kbd> with the appro­pri­ate settings.

There are a num­ber of set­tings in this <kbd>.env.sh</kbd> 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_ROOT_PATH="REPLACE_ME"
LOCAL_ASSETS_PATH=$LOCAL_ROOT_PATH"REPLACE_ME"

# Local user & group that should own the Craft CMS install
LOCAL_CHOWN_USER="admin"
LOCAL_CHOWN_GROUP="apache"

# Local directories that should be writeable by the $CHOWN_GROUP
LOCAL_WRITEABLE_DIRS=(
                "craft/storage"
                "public/assets"
                )

```

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

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

<kbd>LOCAL_CHOWN_USER</kbd> is the local user that is the <kbd>o</kbd>wner of your entire Craft install, as dis­cussed previously.

<kbd>LOCAL_CHOWN_GROUP</kbd> is the local web­serv­er <kbd>g</kbd>roup, usu­al­ly either <kbd>nginx</kbd> or <kbd>apache</kbd>.

<kbd>LOCAL_WRITEABLE_DIRS</kbd> is a quot­ed list of direc­to­ries _rel­a­tive_ to <kbd>LOCAL_ROOT_PATH</kbd> that should be write­able by your webserver.

So for exam­ple, here’s what part of my <kbd>.env.sh</kbd> looks like for this webserver:

```

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

# Local path constants; paths should always have a trailing /
LOCAL_ROOT_PATH="/home/forge/nystudio107.com/"
LOCAL_ASSETS_PATH=$LOCAL_ROOT_PATH"public/img/"

# Local user & group that should own the Craft CMS install
LOCAL_CHOWN_USER="forge"
LOCAL_CHOWN_GROUP="forge"

# Local directories relative to LOCAL_ROOT_PATH that should be writeable by the $CHOWN_GROUP
LOCAL_WRITEABLE_DIRS=(
                "${GLOBAL_CRAFT_PATH}storage"
                "public/assets"
                )

```

The rea­son that both the <kbd>o</kbd>wner and the <kbd>g</kbd>roup are both <kbd>forge</kbd> is because there is both a <kbd>forge</kbd> user, and a <kbd>forge</kbd> group when using Lar­avel Forge.

You might won­der why all of this is in a <kbd>.env.sh</kbd> 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 <kbd>local</kbd> dev, <kbd>staging</kbd>, and <kbd>live</kbd> pro­duc­tion with­out mod­i­fi­ca­tion. We just cre­ate a <kbd>.env.sh</kbd> file in each envi­ron­ment, and keep it out of our git repo via <kbd>.gitignore</kbd>.

_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](https://dev.to/gaijinity/multi-environment-config-for-craft-cms-3952-temp-slug-5514305) article.

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

```

./set_perms.sh

```

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

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

```

#!/bin/bash

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

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

# Include files
INCLUDE_FILES=(
            "common/defaults.sh"
            ".env.sh"
            "common/common_env.sh"
            )
for INCLUDE_FILE in "${INCLUDE_FILES[@]}"
do
    if [-f "${DIR}/${INCLUDE_FILE}"]
    then
        source "${DIR}/${INCLUDE_FILE}"
    else
        echo 'File "${DIR}/${INCLUDE_FILE}" is missing, aborting.'
        exit 1
    fi
done

# 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}"
chown -R ${LOCAL_CHOWN_USER}:${LOCAL_CHOWN_GROUP} "${LOCAL_ROOT_PATH}"
chmod -R ${GLOBAL_DIR_PERMS} "${LOCAL_ROOT_PATH}"
find "${LOCAL_ROOT_PATH}" -type f ! -name "*.sh" -exec chmod $GLOBAL_FILE_PERMS {} \;

for DIR in ${LOCAL_WRITEABLE_DIRS[@]}
    do
        FULLPATH=${LOCAL_ROOT_PATH}${DIR}
        if [-d "${FULLPATH}"]
        then
            echo "Fixing permissions for ${FULLPATH}"
            chmod -R $WRITEABLE_DIR_PERMS "${FULLPATH}"
            find "${FULLPATH}" -type f ! -name "*.sh" -exec chmod $WRITEABLE_FILE_PERMS {} \;
        else
            echo "Creating directory ${FULLPATH}"
            mkdir "${FULLPATH}"
            chmod -R $WRITEABLE_DIR_PERMS "${FULLPATH}"
        fi
    done

# Normal exit
exit 0

```

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

Once you have a <kbd>.env.sh</kbd> set up for each envi­ron­ment, you can set the per­mis­sions in each the exact same way.

So grab [craft-scripts](https://github.com/nystudio107/craft-scripts) and give ​’em a whirl. Now relax, and enjoy.

## Per­mis­sions and Git

If you use <kbd>git</kbd>, and change file per­mis­sions on your remote serv­er, you may encounter git com­plain­ing about <kbd>overwriting existing local changes</kbd> 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 <kbd>fileMode</kbd> 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](https://git-scm.com/docs/git-config#git-config-corefileMode) for details.

The oth­er way to fix this is to set the per­mis­sion using <kbd>set_perms.sh</kbd> in <kbd>local</kbd> 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 <kbd>local</kbd> dev and on <kbd>live</kbd> production.

## Further Reading

_If you want to be notified about new articles, follow [nystudio107](https://twitter.com/nystudio107) on Twitter._

<small>Copyright ©2020 nystudio107. Designed by nystudio107</small>