Software developer stories
en de

More secure deployments via ssh

If we deploy an application automatically we have to grant the CI (Continuous Integration) access to the server. Common practice is to do that via a GitLab Runner or an ssh account on the server.

Personally I would not recommend using a GitLab Runner for deployments, because you have to maintain it. Another potential issue is, that you normally register runners for your whole GitLab instance or groups. That results in a scenario in which everyone can use that runner and accidentally (or not) destroy, for example, your production server. To avoid that you have to register the GitLab Runner in the Project it belongs to only. But even then your production server can be misused as a build worker and therefore create performance issues.

This post focuses on deployments via ssh.

A normal ssh deployment often contains copying files to the server and executing some commands. This can be done manually or via CI. Have you ever thought of what happens when the credentials of that user leak? Even if you have a special deployment user and do not use root. Nearly every file on the server can be read with a normal user (including configs containing credentials). And even if the deployment user only has access to the application folder it can read and write all files. When you are using docker and the user has permission to execute docker commands he has effectively root access.

One solution to that problem is to set up a chroot environment for that user, but it does not solve the docker “problem” as docker normally runs as a (root) daemon and therefore not in the chroot environment.

The solution I currently use is a combination of a custom shell for the ssh user and disallowing file-transfers. By defining what commands the ssh user can execute the potential damage can be minimized.

In the following steps you learn how to setup a custom shell and disable file-transfer. Disabling the file-transfer changes the role of the server, as it needs to retrieve the files, eg. for an update, itself. If you want to learn more about this concept you can read this post: Handling server configurations.



Lines that start with # are either comments or command-outputs

I assume you already created a user which the CI should use. In this tutorial the username is application-deploy.

Change the default shell

sudo touch /home/application-deploy/ 
sudo chown root:root /home/application-deploy/ 
sudo chmod a+x,a+r,g-w,o-w /home/application-deploy/ 
sudo chsh -s /home/application-deploy/ application-deploy 
sudo touch /home/application-deploy/.hushlogin 
  • create the shell file
  • set owner and group to root
  • set file permissions: everyone can execute but only root can edit
  • change the shell of the user to the script
  • disable the login banner

For a first test change the content of and connect with that user to the server.


#!/usr/bin/env bash

echo "Custom Shell called with Parameters: $@"

Login Example output.

ssh application-deploy@my-server
# application-deploy@my-server's password:
# Custom Shell called with Parameters:
# Connection to my-server closed.
ssh application-deploy@my-server abc def klajfglkjdfg6546
# application-deploy@my-server's password:
# Custom Shell called with Parameters: -c abc def klajfglkjdfg6546

Our custom shell is working.

If you want to log in as the user application-deploy you have to connect to the server with another user and then switch users:

su --login --shell /bin/bash application-deploy

Hardening ssh

In order to harden the ssh server we need to disable some features of ssh for the user application-deploy.

To do so add the following to /etc/ssh/sshd_config and then restart the sshd service.

Match User application-deploy
        PasswordAuthentication no 
        AllowTcpForwarding no 
        X11Forwarding no 
        PermitTunnel no 
        GatewayPorts no 
        AllowAgentForwarding no 
  • disable password authentication - remove this line if you have to use passwords instead of ssh-keys
  • disable port forwarding
  • disable X11 forwarding
  • disable VPN capability
  • disable ssh-agent forwarding

Changing the shell script

Maybe you noticed the -c at our test if parameters where sent. In this step we will remove that and finish the custom shell.

First replace the content of the shell with the following:


#!/usr/bin/env bash

if [[ "$1" == "-c" ]]; then

Now the -c will be removed from the parameters if it is the first parameter of our custom shell. The changed parameters are available in the variable PARAMS.

From here it’s up to you and what you want to do. You can type your commands right into the shell file or as I did delegate to the applications runcontrol-script.

Example of handling everything in the custom shell.

#!/usr/bin/env bash

if [[ "$1" == "-c" ]]; then

set -e

log() {
    printf "%s INFO: %s\n" "$(date +"%Y-%m-%dT%H:%M:%S,%N")" "$*"

if [[ "${PARAMS[0]}" == "update" ]]; then
    log "Starting update"
    # ...
elif [[ "${PARAMS[0]}" == "backup" ]]; then
    log "Starting backup"
    # ...
    log "Unknown Parameters $@"
    exit 1

Example of delegating to another script.

#!/usr/bin/env bash

if [[ "$1" == "-c" ]]; then

/home/application-deploy/server-config/ ${PARAMS}