Community Documentation

Securing file permissions using Linux ACLs

Last updated October 29, 2012. Created by OnkelTem on October 29, 2012.
Log in to edit this page.

Theory

You can grab enough ACLs theory from http://users.suse.com/~agruen/acl/linux-acls/online/. Or just google for "Linux ACL".

Script for setting permissions

Ok, I'm gonna share my script which I use to set permissions for Drupal websites. It is ready-for-use just for me, not for your. It's not a production-ready thing, you should understand what script does and probably edit it to accommodate your needs.

The script is called setacl and is supposed to be located in same directory where you have your website's root (DOCUMENT_ROOT) - let's refer to it as to project root. The script is using simple configuration file project.conf so let's start from it.

Configuration file: project.conf

I'm using many scripts for site and database management, for this reason there are lines which are not used by the setacl script but I won't remove them for now to provide you the whole picture.

SITE=MySite
DATABASE=mysite
PREFIX=ms_
USER=root
HOST=localhost
HOSTCODE=LO
PASSWORD=password

ACL_SETS="DEV WEB"

# Developers
ACL_DEV_USERS="onkeltem"
ACL_DEV_GROUPS="devteam"
ACL_DEV_WRITES="."

# Web server
ACL_WEB_USERS="www-data"
# path:number notation is used to specify exact depth for a path.
ACL_WEB_NOACCESS=".:1"
ACL_WEB_READS="www .:0"
ACL_WEB_WRITES="www/sites/default/files"

The subject of our interest are the variables which names start from "ACL_". Since bash doesn't support associative arrays I have to keep meta-data right into variable names.

ACL_SETS="DEV WEB"
This defines a list of suffixes each one corresponds to some specific permission rules. Here are defined two sets: for developers and for web server.

ACL_DEV_USERS="onkeltem"
ACL_DEV_GROUPS="devteam"
These two lines defines who belongs to DEV set. Supported both users and groups.

ACL_DEV_WRITES="."
This line tells that developers have write access to any file and any directory from the current one down to the deepest recursively.

More illustrative is another part, for the WEB set:

ACL_WEB_NOACCESS=".:1"
This will revoke any access to any directory or file in the project root. So you can safely keep any files or directories apart from 'www' directory itself in the project root: scripts, database backups, additional dev directories etc. Recursion now is limited by specifying exact depth of operation: 1.

ACL_WEB_READS="www .:0"
This list consist of two pieces separated by white-space: www and .:0. The former will grant read permissions from the site's root recursively down. The latter will affect only current directory and no files - i.e. project root directory for ":0" spec is used, meaning exact depth of 0.

ACL_WEB_WRITES="www/sites/default/files"
As with DEV's write spec, this grants right access for the files and directories starting from www/sites/default/files.

That's it!

The script

The script runs fine w/o root privileges for newly uploaded websites when you own all the files and thus have full access to them. So upon the first run it will set permissions according to the configuration. All directories acquire "inheritance" (default, -d) flag for any new file or directory will inherit permissions and you don't need actually to rerun the script later. If you do though, you will probably get a bunch of "Operation not permitted" error messages. This doesn't mean that script completely failed - it just can't set permissions on files you are not owning - those are created by web server inside sites/default/files.

Now the script:

#!/bin/bash

. ./project.conf

OPTIONS=$1

ROOT=`readlink -f .`/

function msg {
  test "$OPTIONS" = '-q' && echo -n || echo -e "\e[01;34m"$1"\e[00m"
}

function exec {
  cmd=$1
  IFS='%' && msg "      executing $cmd" && unset IFS
  $cmd
}

msg 'set: ALL'
# Remove all extended ACLs
exec "setfacl -R -b $ROOT"

# Set correct permissions on directories and files
exec "find "`readlink -f $ROOT`" -type d -exec chmod 751 {} +"
exec "find "`readlink -f $ROOT`" -type f ! -perm -u=x -exec chmod 640 {} +"
exec "find "`readlink -f $ROOT`" -type f -perm -u=x -exec chmod 751 {} +"
exec "find "`readlink -f $ROOT`" -type d -exec chmod g-s {} +"
exec "find "`readlink -f $ROOT`" -type f -exec chmod g-s {} +"

function grant {
  if [ "$4" == "read" ]; then
    dperm="r-x"
    fperm="r"
    fpermx="r-x"
  else
    if [ "$4" == "write" ]; then
      dperm="rwx"
      fperm="rw"
      fpermx="rwx"
    else
      dperm="--x"
      fperm="---"
      fpermx="--x"
    fi
  fi
  if [ "$2" == "group" ]; then
    who="g"
  else
    if [ "$2" == "user" ]; then
      who="u"
    else
      exit
    fi
  fi
  IFS='%' && msg "    granting ${4:-noaccess} on "`readlink -f $ROOT${1}`" to $3 $2" && unset IFS
  # Existing directories
  path=${1%:*}
  depth=${1#*:}
  [[ ! $path == $depth ]] && depth_spec="-mindepth $depth -maxdepth $depth" || depth_spec=""
  mkdir -p $ROOT${path}
  exec "find "`readlink -f $ROOT${path}`" $depth_spec -type d -exec setfacl -m ${who}:$3:${dperm} {} +"
  # Existing non executable files
  exec "find "`readlink -f $ROOT${path}`" $depth_spec -type f ! -perm -u=x -exec setfacl -m ${who}:$3:${fperm} {} +"
  # Existing executable files
  exec "find "`readlink -f $ROOT${path}`" $depth_spec -type f -perm -u=x -exec setfacl -m ${who}:$3:${fpermx} {} +"
  # New directories and files
  [[ $path == $depth ]] && exec "find "`readlink -f $ROOT${path}`" $depth_spec -type d -exec setfacl -d -m ${who}:$3:${dperm} {} +"
}

for s in $ACL_SETS
do
  msg "set: ${s}"
  U=ACL_${s}_USERS
  G=ACL_${s}_GROUPS
  N=ACL_${s}_NOACCESS
  R=ACL_${s}_READS
  W=ACL_${s}_WRITES
  for u in ${!U}
  do
    IFS='%' && msg '  user:' ${u} && unset IFS
    for d in ${!N}; do grant $d "user" $u ""; done
    for d in ${!R}; do grant $d "user" $u "read";  done
    for d in ${!W}; do grant $d "user" $u "write"; done
  done
  for g in ${!G}
  do
    IFS='%' && msg '  group:' ${g} && unset IFS
    for d in ${!N}; do grant $d "group" $g ""; done
    for d in ${!R}; do grant $d "group" $g "read";  done
    for d in ${!W}; do grant $d "group" $g "write"; done
  done
done

If you still need to run the script to reset everything on an existing website, then you will either need to run it under root (easiest way) or own the files from the beginning. For example you can:

  1. add group spec to the configuration: ACL_WEB_GROUPS="www-data"
  2. add your user account to www-data group
  3. change default permissions which are now hardcoded in the script (lines 24-28)
  4. change umask for web server process to 0002 or something.

Page status

About this page

Drupal version
Drupal 6.x, Drupal 7.x, Drupal 8.x
Audience
Site administrators
Level
Advanced
Drupal’s online documentation is © 2000-2013 by the individual contributors and can be used in accordance with the Creative Commons License, Attribution-ShareAlike 2.0. PHP code is distributed under the GNU General Public License. Comments on documentation pages are used to improve content and then deleted.
nobody click here