This article presents how to deploy continuously from a Git repository with high security, by creating a UNIX user whose only purpose and ability is to update a repository and execute commands from a script within the repository upon successful SSH connections.
Your server has at least Git and some SSH agent installed, and you are connected to it as root.
Just to rephrase: all these commands are to be executed on your server, as root. ssh root@YOUR_SERVER
now!
If your server is a blank Debian or Ubuntu and you don't use a configuration management system such as Puppet or Ansible, you probably want to
sudo apt-get install git
.
These instructions refer to some variables. You can interpret them manually as you go, or define them up front.
You have to set these two variables with values specific to your deployment:
export REPO_NAME=your_repo_name
export [email protected]:username/your_repo_name.git # make sure this is an SSH URL and not an HTTPS URL
I recommend to use the following values for the rest of the commands:
export USERNAME=deploy
export ABSOLUTE_PATH_TO_DEPLOY_SCRIPT=/home/$USERNAME/$REPO_NAME/scripts/deploy.sh
Create a user that can only log in through SSH, with no password authentication possible:
adduser $USERNAME --shell $ABSOLUTE_PATH_TO_DEPLOY_SCRIPT --disabled-password --gecos ''
- The empty
gecos
option means no interactive prompt for non-applicable details, such as full name, room number…- The
shell
option changes the login shell (the command that is executed when the user logs in) from the default interactive shell (something likebash
) to your deployment script.- The
disabled-password
option means this user can only log over SSH.
Now, let's allow that user to log in over SSH:
ssh-keygen -m PEM -f ~/.ssh/${USERNAME} -N '' # set no passphrase, use PEM and RSA for better compatibility with CI providers (CircleCI needs it for example)
mkdir /home/$USERNAME/.ssh
cat ~/.ssh/${USERNAME}.pub >> /home/$USERNAME/.ssh/authorized_keys
chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh
You should probably also add your own and your coworkers' public keys to
authorized_keys
, in order to mitigate disruption if you ever need to disable that newly created robot key.
We have created an SSH key to log in as the deploy user, but we have not given it a key to pull the code yet. Let's create that key now:
runuser -u $USERNAME -- ssh-keygen -f /home/$USERNAME/.ssh/id_ed25519 -t ed25519 -N '' # use more secure elliptic curve over RSA
Then, we need to add the deploy key to the list of keys that are allowed to pull our code. You can usually do that through the user interface of your Git hosting provider. For example, with GitHub, you can do that in Settings → Deploy keys. You should add the deploy key by copying the contents of /home/$USERNAME/.ssh/id_ed25519.pub
.
For maximum security, do not give push access to that key, only pull access.
Your deployment script could be stored in scripts/deploy.sh
in your repository and look something like the deploy.sh
example file given in this gist, concatenated with some language and deployment-specific elements. Make sure to chmod u+x scripts/deploy.sh
, as it is this file that will be executed upon deployment! This technique also means your deployment script will auto-update.
For an NPM-based stack, check out
deploy-js.sh
, for example.
The following instructions assume you have pushed your deployment script to your master
branch. If you want to work on a temporary cd
branch to try them out, make sure you git checkout cd
on the server and change the TARGET_BRANCH
in the deploy script, otherwise each deployment will force the change to master
.
Let's clone the repository:
cd /home/$USERNAME
runuser -u $USERNAME -- git clone $REPO_URL
You can now try your deployment with su $USERNAME
: you should not be prompted for any login information, but that command should trigger a pull.
If that works, congratulations, you're all set! You should double-check that your SSH login works by ssh $USERNAME@$SERVER
from your machine.
One of the great advantages of this method is that it allows for fine-grained access control. You can create one SSH key per allowed server, and revoke them individually, simply by adding and removing from .ssh/authorized_keys
.
The first thing you should do, though is add a private deploy key to your CI environment. You can get the first one we created from /home/root/.ssh/${USERNAME}
.
You then need to add the deployment step to your CI config, which really means simply making an SSH connection to deploy@your_server
.
For example, with CircleCI:
deploy:
docker:
- image: circleci/node:9.9
steps:
- add_ssh_keys:
fingerprints:
- $(ssh-keygen -E md5 -lf $DEPLOY_BOT_KEY.pub)
- run: ssh-keyscan -H $YOUR_SERVER >> ~/.ssh/known_hosts
- run: ssh $DEPLOY_USERNAME@$YOUR_SERVER
If you need to update something like the Nginx config in your deployment script that then requires using sudo
(such as sudo service nginx reload
), the safe way to do this is to whitelist only these commands so that deploy
can execute them without needing actual superuser rights.
In order to do this, add the following file in /etc/sudoers.d/deploy
:
# Allow deploy user to reload and restart Nginx
deploy ALL=NOPASSWD: /usr/sbin/service nginx reload, /usr/sbin/service nginx restart
You can then add the following in your deploy.sh
:
### Update NGinx conf
# This only gives the deploy user the right to edit their site in `sites-available`, never `sites-enabled`: only the root user should be able to edit those, with `ln -s /etc/nginx/sites-available/$REPO_NAME /etc/nginx/sites-enabled
cp conf/nginx /etc/nginx/sites-available/$REPO_NAME # this assumes the `deploy` user has write rights on this file (not the case by default, give them with `touch` and `chown`)
sudo /usr/sbin/service nginx reload # this assumes the `deploy` user has rights to `sudo` at least for reloading the Nginx config (not the case by default)
I recommend using
adduser --shell
option instead ofusermod -s
.