Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Hardening Craft CMS Permissions
An important part of hardening Craft CMS from a security point of view is getting the file permissions right
Update: This article has been updated to cover both Craft CMS 2.x and Craft CMS 3.x
Part of hardening Craft CMS is ensuring that the file permissions are as strict as possible, while still allowing for the proper functioning of Craft CMS itself. File permissions are just one part of the larger discussion of Securing Craft.
We want the webserver to be able to write to specific directories so that things like asset uploading works, but we don’t want the webserver to be able to modify things that it shouldn’t. If a security exploit happens, we want to mitigate and contain the damage as much as possible. Additionally, proper permissions are needed for Craft CMS to even work.
Before we get into the nitty gritty, let’s review Unix file permissions.
Link Unix File Permissions Primer
Here is an infographic showing Unix file permissions:
With standard Unix POSIX permissions, every file/directory has different permissions for the file owner, group, and all (everyone else). So for example, 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 other users might not be able to access it at all.
You don’t need to know the gory details, but here’s how the permissions are expressed numerically:
For for example, this file:
-rw-r--r-- 1 admin nginx 9275 Nov 18 17:50 gulpfile.js
…is writeable & readable by the owner admin, but can only be read by the user in the group nginx, and all others similarly can only read it. No one can execute it (run it as a script or other executable binary). Expressed numerically, the permissions would be 644.
Here’s a directory with similar permissions:
drwxr-xr-x 12 admin nginx 4096 Nov 18 18:21 public
You’ll notice that the execute permission is set for the directory owner, group, and all others. The x flag for directories simply means that those with permission can list the files in that directory. Expressed numerically, the permissions would be 755.
Link A Permissions Strategy for Craft CMS
Still with me? Okay, great. Now let’s look at how we might apply this knowledge to Craft CMS permissions so that our Craft install is secure, but still functions properly.
The owner of our entire Craft CMS install should be a user other than the webserver user. It might be the admin account, it might be the user account you access the server with, or it might be forge if you’re using server provisioning software like Laravel Forge.
The owner should be the only user that is able to write to every file in your Craft CMS install.
The group of our entire Craft CMS install should be the webserver group. We allow it to read any of the files in our Craft install so that it can serve up our website, but it can only write to a few specific directories.
Finally, all other users can only read the files in our Craft install. If you’re really paranoid, you could disallow even reading, but it seems a bit overkill unless you’re using a shared hosting environment (which you really shouldn’t be these days).
Nothing in our Craft CMS install (other than directories, and any shell scripts you might be using) needs to be executable. This is because .php files aren’t actually executed, they are read in and parsed by either php or php-fpm.
Craft CMS 2.x and Craft CMS 3.x both have very similar permissions requirements, but the folder structure is slightly different. Check out the Setting up a New Craft CMS 3 Project article for details on the differences.
Link Craft CMS 2.x Permissions
The webserver group needs to be able to write to:
- craft/storage for Craft’s normal operation
- Any designated asset directories, so that the client can upload images & other assets
That’s it! The Installing Craft CMS 2.x Instructions state that the webserver also needs to be able to write to craft/config and craft/app, however write access to craft/config is only needed to install the license.key file, and write access to craft/app is only needed to allow for one-click updates.
Instead, I recommend that you install the license.key file in local dev, and use whatever deployment tool you use to push it to your staging and live production servers. Similarly, I recommend that you update & test any Craft CMS updates in local dev, and then push them to staging and live production. Then disable one-click updates on staging and live production by adding this to your craft/config/general.php file:
'allowAutoUpdates' => false,
Yes, auto-updates are convenient; and you can still do them in local dev. But we really want a way to test updates before deploying them to live production. And giving the webserver write access to the craft/app and craft/config directories potentially allows some as-yet-undiscovered exploit to do bad things to our website.
If you prefer or require that craft/app and craft/config are writeable, that’s fine. Just go into it with eyes wide open.
Link Craft CMS 3.x Permissions
The webserver group needs to be able to write to the following directories:
- storage/ — for Craft’s normal operation
- vendor/ — this is where Composer puts its PHP packages for your project
- web/cpresources/ — this is a cache directory for AdminCP resources
- Any designated asset directories, so that the client can upload images & other assets
Then due to Craft CMS 3 using Composer, it also needs to be able to write to a few specific files as well:
- .env — for your environment-specific variables like passwords, etc.
- composer.json — a list of Composer packages that your project requires
- composer.lock — a list of Composer packages that are installed
- config/license.key — your Craft CMS 3 license file
That’s it! You can check out the Craft CMS 3 Installation Instructions in more depth if you like. I continue to recommend that you don’t allow updates to be done on live production or staging servers, via the following in your config/general.php file:
'allowUpdates' => false,
This is covered in-depth in the Setting up a New Craft CMS 3 Project article, but the basic premise is that we update and test in local development, and once we know everything works, we deploy the updates to live production and/or staging.
Link Shell Scripts to Make it Simple!
Don’t worry, you’re not going to have to do all of this by hand. I’ve created some handy craft-scripts shell scripts to make setting Craft CMS install permissions easy. To use them, you’ll need to do the following:
- Download or clone the craft-scripts git repo
- Copy the scripts folder into the root directory of your Craft CMS project
- Duplicate the example.env.sh file, and rename it to .env.sh
- Add .env.sh to your .gitignore file
- Then open up the .env.sh file into your favorite editor, and replace REPLACE_ME with the appropriate settings.
There are a number of settings in this .env.sh file, but we only need to concern ourselves with the following for setting 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"
)
LOCAL_ROOT_PATH is the absolute path to the root of your local Craft install, with a trailing / after it.
LOCAL_ASSETS_PATH is the path to your assets directories relative to LOCAL_ROOT_PATH, with a trailing / after it.
LOCAL_CHOWN_USER is the local user that is the owner of your entire Craft install, as discussed previously.
LOCAL_CHOWN_GROUP is the local webserver group, usually either nginx or apache.
LOCAL_WRITEABLE_DIRS is a quoted list of directories relative to LOCAL_ROOT_PATH that should be writeable by your webserver.
So for example, here’s what part of my .env.sh 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 reason that both the owner and the group are both forge is because there is both a forge user, and a forge group when using Laravel Forge.
You might wonder why all of this is in a .env.sh file, rather than in the script itself. The reason is so that the same scripts can be used in multiple environments such as local dev, staging, and live production without modification. We just create a .env.sh file in each environment, and keep it out of our git repo via .gitignore.
Tangent: For a more in-depth discussion of multiple environments, check out the Multi-Environment Config for Craft CMS article.
Alright, now that we have our .env.sh all filled out, to set our file permissions we just ssh into our server, cd to the scripts directory, and type:
./set_perms.sh
That’s it! If it complains about permission errors, you might need to type sudo ./set_perms.sh instead (and you will need to type your sudo password to authenticate).
For the curious, 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 create any directories you specified in LOCAL_WRITEABLE_DIRS if they don’t exist, which is handy because craft/storage, for instance, should always be excluded from your git repo via .gitignore, but Craft won’t function unless it exists (and is writeable).
Once you have a .env.sh set up for each environment, you can set the permissions in each the exact same way.
So grab craft-scripts and give ’em a whirl. Now relax, and enjoy.
Link Permissions and Git
If you use git, and change file permissions on your remote server, you may encounter git complaining about overwriting existing local changes when you try to deploy. This is because git considers changing the executable flag to be a change in the file, so it thinks you changed the files on your server (and the changes are not checked into your git repo).
To fix this, we just need to tell git to ignore permission changes on the server. You can change the fileMode setting for git on your server, telling it to ignore permission changes of the files on the server:
git config --global core.fileMode false
See the git-config man page for details.
The other way to fix this is to set the permission using set_perms.sh in local dev, and then check the files into your git repo. This will cause them to be saved with the correct permissions in your git repo to begin with.
The downside to the latter approach is that you must have matching user/groups in both local dev and on live production.