Managing file and folder permissions on CentOS when deploying with Git

GitHub's mascot sitting on top of a MacBook's trackpad.
This one doesn't leave hair all over your MacBook.

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!)

-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.

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:

[ryan@vps ~]$ sudo su -
[root@vps ~]$ groupadd web

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

[root@vps ~]$ usermod -a -G web git
[root@vps ~]$ usermod -a -G web apache

Last, we’ll set the setgid bit on the htdocs folder:

[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
  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.

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.

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:

[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
...

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:

[root@vps ~] su git
[git@vps ~]$ 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

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:

# 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.

Featured image by Roman Synkevych.


Comments (16)

Previously posted in WordPress and transferred to Ghost.

Zahari M
December 29, 2013 at 6:43 am

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!

Ryan Sechrest
December 29, 2013 at 8:10 pm

Thanks, glad it was helpful!

Peter Caesar
March 26, 2014 at 11:47 am

Hello Ryan,
thanks, that was what I’m looking for!
Und …
St. Louis ist eine gute Wahl – wunderschöne Stadt.
All the best from cold Bremen, Germany

Ryan Sechrest
March 26, 2014 at 11:57 am

Great, glad it was helpful! Come to think of it, I haven’t been back to Germany in 7 something years. I really do miss it!

Thiago
December 5, 2014 at 7:05 am

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?

Thiago
December 5, 2014 at 7:49 am

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!

Ryan Sechrest
December 5, 2014 at 11:26 am

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.

geoidesic
December 13, 2014 at 5:37 am

Sounds great… only problem is my debian server doesn’t have a “git” user.

Ryan Sechrest
December 13, 2014 at 9:10 pm

Ah, good point, I should have mentioned that this user can just be created. For Debian, take a look at this: http://www.debian.org/doc/manuals/system-administrator/ch-sysadmin-users.html

Miles
December 31, 2014 at 6:16 pm

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!

Ryan Sechrest
January 1, 2015 at 3:19 am

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).

Malte
April 15, 2015 at 5:04 pm

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?

Ryan Sechrest
April 16, 2015 at 9:24 am

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

chakatz
March 24, 2016 at 4:31 am

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!!

Ryan Sechrest
March 24, 2016 at 5:41 pm

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?