Hosting your own Git-based shared repositories using SSH
Git has become one of the most important tools in a developer's toolkit. To a Drupal developer, it is even more critical as nearly everyone in the community has standardized on it. While there are many great Git hosting services out there, sometimes clients need to have only local copies and Git is all about making each copy a distinctive repo to itself... so why not create your own Git host on your own server? That is what we are here to do today.
Part of our objective here is to use the Linux/Unix file permissions scheme as intended in a way that limits access to the bare-minimum necessary. We presume each developer has their own SSH account on the git server and that is all they should ever need.
Requirements
This recipe is for any Linux host that has Git installed. It requires SSH as it will be used for managing the connections with your users. By default, SSH uses the Linux system's user accounts as an authentication system (known as "auth" method) but if your needs require it, you can also use SSH modules to plug into your local LDAP or ActiveDirectory® authentication systems. One thing that will be of great importance in this tutorial is permissioning the users correctly and setting up a deployment action that suits your needs best. The strategies we use here may be adapted to your own use cases.
Getting Started with Git as a Host
By now I'm sure you've probably heard the philosophy behind Git is that every repo contains all the history of a project and any copy can become the master copy if the original is lost. While this is great in principle, in reality, to share Git with others we will need to setup a special type of repository that is accessible to your system's users.
Installing Applications
First, let's make sure we have Git and SSH installed. On Debian or Ubuntu the command to install Git is as follows: apt-get install git-core openssh-server
There is no special version of Git to do shared repositories, the standard one will do it all.
Storage
You will need to create yourself a folder where your repositories will be stored. In my case I'm creating a new directory right in the root of the server so that my users will have a nice path to work with when I give them access to the server. The storage should *not* be your production webserver. You need to put it somewhere that is not live to your users as the shared repository has a bunch of files you don't want to put into a production environment. mkdir /projects
I actually created my projects folder under /var/projects and just created a symlink here, to better integrate with our existing backup processes.
Grouping the Users
Make sure that we have a group for our users. I'm using the group name "webmasters" but you may already have a group established for your team. If that is the case, use the group you are already using. addgroup webmasters
We will have to do additional work on the user account to make this work... but for now this is enough.
Initializing the Shared Repository
Now we will create a new project called "newsite". When your colleagues connect to the site the path will be /projects/newsite.git with this configuration. cd /projects git init --bare --shared newsite.git chgrp -R webmasters newsite.git
Adding Users to the Mix
If you already have users on your site, great. If not... adduser newdeveloper webmasters usermod -a -G webmasters newdeveloper
The usermod step is necessary so that each time your user, newdeveloper, creates a file, that it will be permissioned to the entire group. This will allow other users to modify the file if it was created by another user. There is one last step to get the permissions structure just right. By default, most Linux systems only allow user files to be edited by the user who created it, even though you have put the file into the group. There are many strategies for how to override this. My personal favourite is to change the system umask value to apply the same permission for the owner to the group as well.
To make a global change to enable "group writeable" by default in Debian or Ubuntu do the following: Edit the file /etc/profile
with your favourite text editor. Add umask 002
to the end of the file. If you already have a umask value, you can change it rather than adding a new line. You can also add the umask 002
line to the user's ~/.bashrc file if you wish to do per-user setup for this. Be sure to test that this is working by logging in as your new user by doing su newdeveloper
and then typing cd
to go to their home folder (note, be sure to login after making the change), then in the user's home directory try doing touch testfile
followed by ls -la | grep testfile
.
You should see the following output: -rw-rw-r-- 1 newdeveloper webmasters 0 2012-12-20 13:17 testfile
In particular: look at the codes at the start. If you see -rw-r--r-- then umask is not set correctly for some reason. You should also see newdeveloper and webmasters as the user and group respectively. If not, go back to the step where you set the user's group to be set to new files by default. Does it all look ok? Then rm testfile
and log out of your new user's account. The Control-D key will get you out of their account fast. ;) Keep in mind there are other methods for doing this. If you already have a different system for managing group ownership of files, you will probably want to stick to the system you are already using if it is appropriate for your use case.
Accessing the Repository
Your repository can now be accessed using the following paths. Keep in mind, if it is the first time you clone your repository it will warn you that you are cloning an empty repository. That is ok! You can add some files later and push up to the server so that the next person to clone does not get that message. From the same (local) machine: git clone file:///projects/newsite.git cd newsite
From a remote computer anywhere on the Internet: git clone newdeveloper@example.com:/projects/newsite.git cd newsite
If you are using a remote computer, you will be asked for your password unless you have added your public key from your remote computer to the user's account on the server.
For Bonus Points, auto-checkout into stage
There is one critical thing that you will want to consider before you go live. How are you going to update your staging environment? By default there IS an action performed when users push new updates to git, defined in the shared repository's hooks folder (under /projects/newsite.git/hooks in the file system), in the post-commit file. One word of warning here though - it will run as the user who does the commit. So your staging environment will constantly have permission errors. Ideally your stage environment probably has one user who is in control of it.
To fix this, a really crude way, I rigged up a script that checks for updates every 30 seconds. Eventually I'll come up with something better, an action that can be taken by any user that doesn't involve giving everyone sudo access to the stage user. Run this "daemon" as a script from cron as the user you want to be responsible for stage: #!/bin/bash cd /stage while [ 'FALSE' != 'TRUE' ] do git pull origin master sleep 30 done
WARNING: it should be obvious that this code won't scale... and will waste some resources unnecessarily; you've been warned!
In my second crude attempt at solving this issue I have taken the following approach:
- Create a
hooks/post-receive
file inside your repo - Set this file to echo your destination path into a queue file:
echo "/var/www/newsite" >> /projects/queue
- Create the
/projects/queue
file:touch /projects/queue && chown root:webmasters /projects/queue && chmod 660 /projects/queue
- Create a checkout script:
while read PATH; do cd $PATH; /bin/su target_username -c "/usr/bin/git pull origin master" done
- Then create a watcher to trigger that checkout script:
echo "" >/projects/queue # empty the queue first tail -f /projects/queue | /usr/local/bin/checkout
This solves the issue of having multiple users accessing the repository because you specify a user to run the checkout. All the users are able to write to the queue file, and the watcher just keeps an eye on that file. Since the watcher must sudo into another user's account to do the checkout, we can run the watcher as root and there is no possibility to any of our users figuring out they can sudo as someone else - because we don't use sudo at all. You should add your watcher to your startup scripts.
More Bonus Points, disable SSH interactive mode for some users, and allow logins without passwords
This can be considered a sort of cruel and unusual punishment by some... however, in some cases it is handy, for example, when you have a designer changing theme files but who shouldn't be able to get into all your databases and other things. This is really simple to accomplish: usermod newdeveloper -s /usr/bin/git-shell
The recommendation for dealing with public keys is to have the user login to SSH normally, then drop the user into git-shell. I'll be rolling this out soon so I can collect some of these bonus points. Have your user generate the public key. You may wish to avoid RSA because some server-wide sshd_config files have it off by default. I have used DSA in this example, if you use a different encoding, just make sure you use the associated id_XXX.pub file for that.
On the developer's machine, grab the existing .ssh/id_dsa.pub
or generate one using: ssh-keygen -t dsa
Be sure to leave the challenge response blank. Then copy the contents of the id_dsa.pub
file to the server. The contents of the file should be appended to the .ssh/authorized_keys
file on the server... then... the important stuff: Back on the server: chmod 700 /home/newdeveloper/.ssh chmod 600 /home/newdeveloper/.ssh/authorized_keys
That is it! Now the user should be able to log in automatically, and they will not be able to SSH into the host... only to use git to post the files.