Managing file and folder permissions when deploying with Git

Preface

I use Git as a version control and deployment system. When a website gets pushed to a server, all files get pulled into the web root (i.e. htdocs) by a user named git executing git pull in the post-receive hook.

By default, all files and folders git creates have 664 and 775 permissions, respectively, and are owned by that user. 664 translates to the user and group being able to read and write, and everyone else only being able to read, and 775 translates to the user and group being able to read, write and execute, and everyone else only being able to read and execute. (That’s a mouthful!)

1
2
-rw-rw-r-- 1 git  git   30 Aug  15  23:04 test-file.txt
drwxrwxr-x 1 git  git  102 Aug  15  23:04 test-directory
-rw-rw-r-- 1 git  git   30 Aug  15  23:04 test-file.txt
drwxrwxr-x 1 git  git  102 Aug  15  23:04 test-directory

Now, in an instance where you need a folder in htdocs writable by another user, like apache, for let’s say a caching system, you need to be able to set those particular permissions accordingly.

To accomplish this, you really only have two options:

  1. Set permissions of files to 666 and folders to 777
  2. Set the owner or group to apache (or a group that apache is a member of)

Personally, I favor restrictive permissions over convenience, so option #1 is out, which means we’re going to take a look at how to implement option #2.

Goals

  • Keep website files and folders owned by git, which essentially makes them read-only to apache and the public
  • Allow apache to create and modify files and folders only in specified folders
  • Maintain a streamlined deployment, meaning all permission modifications need to happen in the post-receive hook using only the permissions the git user already has

Note: I’m running CentOS/RedHat, meaning that some commands may differ if you’re using a different operating system.

1. Set group ID (setgid bit)

The first thing we need to do is ensure that everything inside htdocs remains owned by git, in other words, ensure only git can create and modify files and folders. This leaves us with having to change the group of the folders that apache needs to write to, but the problem is that git doesn’t have the proper permissions to use the chgrp command.

What we’ll do instead is create a new group called web, add git and apache as members of that group, and then enforce that all files and folders created in htdocs are automatically placed in the web group.

Let’s elevate our permissions to root and create a new group:

1
2
[ryan@vps ~]$ sudo su -
[root@vps ~]$ groupadd web
[[email protected] ~]$ sudo su -
[[email protected] ~]$ groupadd web

Next we’ll add both git and apache to that group:

1
2
[root@vps ~]$ usermod -a -G web git
[root@vps ~]$ usermod -a -G web apache
[[email protected] ~]$ usermod -a -G web git
[[email protected] ~]$ usermod -a -G web apache

Lastly we’ll set the setgid bit on the htdocs folder:

1
2
3
4
5
6
7
[root@vps ~]$ ll /var/www/domains/domain.com/www
drwxrwxr-x 5 git  git  4096  Aug 15 23:56  htdocs
[root@vps ~]$ chgrp web /var/www/domains/domain.com/www/htdocs
[root@vps ~]$ chmod g-w /var/www/domains/domain.com/www/htdocs
[root@vps ~]$ chmod g+s /var/www/domains/domain.com/www/htdocs
[root@vps ~]$ ll /var/www/domains/domain.com/www
drwxr-sr-x 5 git  web  4096  Aug 15 23:56 htdocs
[[email protected] ~]$ ll /var/www/domains/domain.com/www
drwxrwxr-x 5 git  git  4096  Aug 15 23:56  htdocs
[[email protected] ~]$ chgrp web /var/www/domains/domain.com/www/htdocs
[[email protected] ~]$ chmod g-w /var/www/domains/domain.com/www/htdocs
[[email protected] ~]$ chmod g+s /var/www/domains/domain.com/www/htdocs
[[email protected] ~]$ ll /var/www/domains/domain.com/www
drwxr-sr-x 5 git  web  4096  Aug 15 23:56 htdocs
  1. Print out current permissions for htdocs
  2. Set the group of htdocs to web
  3. Remove write permissions for the group on htdocs
  4. Set the setgid bit on htdocs
  5. Print out new permissions (note the ‘s’ flag in the group)

Going forward, any file or folder that’s created in htdocs will automatically set web as the group. If you already have files and folders in htdocs, you can recursively put them in the web group, but be conscious about the effect this will have on everything you have in that folder.

1
chgrp -R web /var/www/domains/domain.com/www/htdocs
chgrp -R web /var/www/domains/domain.com/www/htdocs

At first it my seem tempting to simply set the group to apache, however when git later modifies a folder to allow apache to create files and folders inside, the setgid bit is unset. The reason for this is that git does not belong to the apache group. To circumvent this particular issue, we created the new web group that both users could be a member of, which allows git to effectively give apache write permissions without unsetting the setgid bit.

2. Set umask to 022

By doing the above, we’ve violated one of our goals, and that’s the fact that apache will now have write permissions on all files and folders after git pull is run for the first time.

I don’t know if you’ve ever noticed, but when you create a new file or folder using root, the permissions are actually set to 644 and 755, respectively. Those default permissions are set via a command called umask, and we’ll now emulate this behavior for the git user.

As a side note, if you want to learn more about umask, there is a great article about what umask is and how it works.

There is a file called profile which sets a default umask value for all system users:

1
2
3
4
5
6
7
8
9
10
11
12
[root@vps ~]# cat /etc/profile
...
# By default, we want umask to get set. This sets it for login shell
# Current threshold for system reserved uid/gids is 200
# You could check uidgid reservation validity in
# /usr/share/doc/setup-*/uidgid file
if [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then
    umask 002
else
    umask 022
fi
...
[[email protected] ~]# cat /etc/profile
...
# By default, we want umask to get set. This sets it for login shell
# Current threshold for system reserved uid/gids is 200
# You could check uidgid reservation validity in
# /usr/share/doc/setup-*/uidgid file
if [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then
    umask 002
else
    umask 022
fi
...

You’ll notice that by default, all users with a user ID of 200 or greater will receive a umask of 002, and all users with ID 199 or less, a umask of 022. Looking at the chart below, you can look up what this translates to:

  • 0: read, write and execute
  • 1: read and write
  • 2: read and execute
  • 3: read only
  • 4: write and execute
  • 5: write only
  • 6: execute only
  • 7: no permissions

002 means rwx for owner, rwx for group, and r-x for everyone else. (The default for new system users).

022 means rwx for owner, r-x for group, and r-x everyone else. (The default for built-in system users).

We’ll use the git user’s .bashrc file to automatically set the umask to 022. The .bashrc file is executed every time a user logs in or opens a new terminal window. Here is a brief explanation from linux.die.net:

Today, it is more common to use a non-login shell, for instance when logged in graphically using X terminal windows. Upon opening such a window, the user does not have to provide a user name or password; no authentication is done. Bash searches for ~/.bashrc when this happens, so it is referred to in the files read upon login as well, which means you don’t have to enter the same settings in multiple files.

Let’s edit the file now:

1
2
3
4
5
[root@vps ~] su git
[git@vps ~]$ vi ~/.bashrc
...
# User specific aliases and functions
umask 022
[[email protected] ~] su git
[[email protected] ~]$ vi ~/.bashrc
...
# User specific aliases and functions
umask 022
  1. Switch over to the git user
  2. Edit the .bashrc file in the home directory
  3. Add umask 022 to the bottom

3. Use post-receive hook to update permissions

The last step, and this will be specific to your needs, we’ll use the post-receive hook to set permissions to allow apache to write to specified folders. I’ve pulled out and modified the relevant commands from my post-receive hook to provide an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Set path to htdocs
htdocs="/var/www/domains/domain.com/www/htdocs"
 
# Unset global GIT_DIR so script can leave repository
unset GIT_DIR
 
# Move into htdocs
cd $htdocs
 
# If cache directory does not exist
if [ ! -d "$htdocs/cache" ]; then
 
  # Create cache directory for cache files
  mkdir $htdocs/cache
 
  # Allow Apache to write to cache
  chmod g+w $htdocs/cache
 
fi
# Set path to htdocs
htdocs="/var/www/domains/domain.com/www/htdocs"

# Unset global GIT_DIR so script can leave repository
unset GIT_DIR

# Move into htdocs
cd $htdocs

# If cache directory does not exist
if [ ! -d "$htdocs/cache" ]; then

  # Create cache directory for cache files
  mkdir $htdocs/cache

  # Allow Apache to write to cache
  chmod g+w $htdocs/cache

fi

That’s pretty much it, and it meets all of our goals. If you have any questions or comments, leave them below.

December 30, 2013 Update: I recently wrote an article on how to deploy WordPress as a submodule that showcases a more complete post-receive hook, which may be of interest to you.

15 thoughts on “Managing file and folder permissions when deploying with Git

  1. Zahari M

    Hi Ryan

    Greetings from Kuala Lumpur, Malaysia

    Merry Xmas and a happy new year!

    This is a really well written and well explained article. An awesome work!

    Thanks!

    Reply
  2. Thiago

    Hi again Ryan!
    I followed your guide and had it all working last night. I am not sure what I did but now for some reason I can’t get access into anything past /var/www/vhosts/ as the git user (eg I cannot cd into vhosts/domain.com). I can see everything in there and for some reason, the current setup is:

    drwx--x--- 6 clientname psaserv 4096 Dec 5 12:49 domain.com

    As the git user I cannot cd into this folder, nor can I cd into the httpdocs folder inside:

    drwxr-sr-x 2 git web 4096 Dec 5 12:49 httpdocs

    Should I change the permissions on the webroot folder?

    Reply
  3. Thiago

    I tried changing permissions to git:web and that gave the git user permissions alright. Unfortunately that completely screwed up Plesk’s permissions meaning I couldn’t manage the server via Plesk anymore so I reset all of the settings for domain permissions. I am baffled here, I have added the git user to Plesk’s default groups – psacln and psaserv but this still does not give the git user any permissions to cd into the directories!

    Reply
    1. Ryan Sechrest Post author

      Hi Thiago,

      I run this on a server that doesn’t have any management software that tries to access and/or modify anything in those folders, which is why the setup described above works for me. My guess is that something like Plesk has a cron job or some other way to periodically ensure that all the permissions are properly set, which could explain why it stopped working.

      This is a tricky situation, because the GIt user exists to prevent anything like a user (or Plesk) to modify version-controlled files, but if Plesk requires that it has those permissions, then creating a Git user defeats the purpose.

      That said, you don’t have to create a Git user. You could also push your code as the clientname user, so that all the existing permissions setup by Plesk remain in place.

      Lastly, if you still want to use something more universal like a Git user, you might just give the Plesk Forum a try, to see if someone there can help you with the Plesk permissions and provide better insight on how that is setup.

      Reply
  4. Miles

    Happy New Year Ryan!
    I have been looking for content on this subject and yours is hitting the nail best of all right now.
    Except I have to twist it a bit for my own purposes and I too am a junior web developer type who is learning about 3 times as many things as is natural for a human… but anyway.

    I am using this setup with Aegir (for managing Drupal sites). So Aegir is a user, which I’m pretty sure wants to be the main user in control of 99% of the files. In my instance, when I push changes to the server (gitolite), I am happy for Aegir to become the owner and group most of the time, being able to set an exception for some folders would be nice too, but I could get around that by having more specific git repositories.

    So your comment above relating to Plesk, where the changes get pushed (or I guess checked-out?) as a specific user (Aegir in my case) sounds promising! Am I understanding that correctly? How do I do that? Or is there a better way for my particular situation?

    Thanks! And thanks generally for the help you are providing me and the others here – it’s good stuff!

    Reply
    1. Ryan Sechrest Post author

      Hi Miles,

      Happy New Year to you, too!

      Yes, you understood that correctly. If you perform all of the Git operations as the Aegir user, that user becomes the owner of all the files in the repo. Using the method I describe, you can then give write permissions to the group, which another user could be a member of.

      Now, in your case, you might create a remote user (git) that you can use to push your files, but then in the post-receive hook, you change the permissions to Aegir (or in case of the 1%, not). That way, you’re not logging into the system as Aegir over SSH (if that’s even possible).

      Reply
  5. Malte

    Hi Ryan,

    I followed your tutorial, but now my file ownership is the following:

    drwxr-s---  4 git web  4096 Apr 15 22:02 .
    -rw-r--r--  1 git web   427 Apr 15 22:02 index.php

    and my web group /apache user is not allowed to write to any file (e.g. wp-content). What do I need to change?

    Reply
    1. Ryan Sechrest Post author

      Hi Malte,

      The file permissions (-rw-r--r--) are correct and directories should be drwxr-sr-x.

      First, I’d double check that apache has been added to the web group:

      $ groups apache
      apache : apache web

      Second, add write permissions to any directory that you want Apache to write to, such as wp-content, if you want to make the entire directory writable (I personally would limit that even further):

      chmod g+w wp-content
      Reply
  6. chakatz

    Great post, lots of useful information!
    I use github webhooks to trigger automatic site updating. The git pulls are run by the ‘apache’ user which is a restricted system user (UID 48) and owns all the files and dirs with umask of 022 and can’t run any shell commands. I need to make some dirs 775 so they can be written to by non apache users. Is there any way a scenario with git hooks like you outlined above will work in this case? (Otherwise I may have to just run a cron to make the change every minute and I hate that idea.)
    Thanks!!

    Reply
    1. Ryan Sechrest Post author

      Hi Chakatz! Just to make sure I captured this right, you push your code to Github, which triggers a web hook, which makes a request to an endpoint on your site, which then runs a script (as Apache) to pull down the latest copy from Github? Is that right?

      If so, what is the problem you’re trying to solve?

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *