How to Create a Restricted SSH Key for CI/CD Pipelines Using rrsync

When deploying applications through CI/CD pipelines, we often need to transfer files to a remote server using SSH. However, giving a pipeline unrestricted SSH access is not always a good idea. In most cases, the pipeline only needs to upload files to a specific directory. It doesn’t need an interactive shell or permission to execute arbitrary commands.

We can achieve this by combining SSH authorized key restrictions with a tool called rrsync (restricted rsync).

In this guide, we’ll create an SSH key that can only be used for rsync transfers within a specific project directory.

What is rrsync?

rrsync (restricted rsync) is a script distributed with rsync that allows administrators to restrict file synchronization over SSH. It validates incoming rsync commands and limits transfers to a predefined directory.

Its main capabilities include:

  • Directory restriction – limit file transfers to a specific directory and its permitted subdirectories.
  • Read-only access (-ro) – allow downloading files without uploading or modifying them.
  • Write-only access (-wo) – allow uploading files without downloading them.
  • Safer backups and deployments – reduce the permissions available to an SSH key used by automated processes.

By combining rrsync with the SSH command and restrict options, we can create a key that only permits restricted rsync operations.

Step 1: Generate an SSH key

First, generate a dedicated SSH key pair on your local machine:

ssh-keygen -t ed25519 \
  -f ~/.ssh/rsync_deploy \
  -N "" \
  -C "rsync-deploy"

This command creates two files:

  • ~/.ssh/rsync_deploy – private key
  • ~/.ssh/rsync_deploy.pub – public key

We use the Ed25519 algorithm, which is a modern and widely supported choice for SSH authentication.

The -N "" option creates the private key without a passphrase, which is convenient for non-interactive CI/CD jobs. However, it also means that anyone who obtains the private key can use it without an additional secret.

Store the private key securely in your CI/CD platform’s secret management system. Never commit it to your repository.

Step 2: Install the public key on the server

Next, copy the public key to the remote server:

ssh-copy-id \
  -i ~/.ssh/rsync_deploy.pub \
  -p 22 \
  [email protected]

The ssh-copy-id command installs the public key in the remote user’s:

~/.ssh/authorized_keys

This file contains the public keys authorized to authenticate as that user. You need existing SSH access to perform this step, such as password authentication or another authorized SSH key.

Important: At this point, the newly installed key is not restricted. Depending on the user’s account configuration, it may allow normal SSH access. We will restrict it in the next steps.

Step 3: Prepare the server

Connect to the server using your existing administrative credentials. First, create the directory where the application files will be deployed:

mkdir -p /home/myuser/myproject

The myuser user must have the appropriate filesystem permissions to write to this directory. Next, make sure rsync and rrsync are installed and available.

Check the location of rrsync:

command -v rrsync

For this example, we’ll assume it is available at:

/usr/bin/rrsync

The actual path may vary depending on your operating system and installation method. Some distributions provide rrsync as part of an additional rsync package or support scripts.

If the command is not found, install rrsync from your distribution’s packages or the official rsync distribution before continuing.

Step 4: Restrict the SSH key

Now comes the most important part. Log in using your existing administrative access and open the authorized_keys file:

nano ~/.ssh/authorized_keys

Find the public key generated earlier. You can identify it by its comment:

rsync-deploy

By default, the entry looks something like this:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... rsync-deploy

Add the following options at the beginning of that same line:

restrict,command="/usr/bin/rrsync -wo /home/myuser/myproject" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... rsync-deploy

Keep the original public key value unchanged.

What do these restrictions mean?

restrict

Enables a set of SSH restrictions, including:

  • Disabling interactive terminal allocation (PTY).
  • Disabling SSH port forwarding.
  • Disabling SSH agent forwarding.
  • Disabling X11 forwarding.
  • Disabling execution of the user’s ~/.ssh/rc file.

command="/usr/bin/rrsync -wo /home/myuser/myproject"

Forces SSH to execute rrsync whenever this particular key is used.

The user’s requested command is not executed directly. Instead, rrsync examines the original SSH command and verifies that it is an allowed rsync operation.

The parameters specify that:

  • /home/myuser/myproject is the restricted directory.
  • -wo enables write-only mode, allowing uploads but preventing downloads.

This means the key can be used to deploy files through rsync, but it cannot be used to execute arbitrary shell commands such as whoami, cat, or bash. Note that these restrictions apply to this specific SSH key, not automatically to every key authorized for the myuser account.

Step 5: Test the restricted SSH key

Before adding the private key to your CI/CD pipeline, verify that the restrictions work as expected.

Test 1: Upload files to the allowed directory

Run the following command locally:

rsync -avz \
  -e "ssh -p 22 -i ~/.ssh/rsync_deploy -o IdentitiesOnly=yes" \
  ./ \
  [email protected]:/

This command should work. The remote path / is interpreted by rrsync as the root of the restricted directory, not the server’s actual filesystem root. In our case, files will be transferred to /home/myuser/myproject. The IdentitiesOnly=yes option ensures that SSH uses the explicitly specified identity rather than trying additional keys offered by an SSH agent.

Test 2: Try uploading outside the allowed directory

Now, attempt to transfer files to another project directory:

rsync -avz \
  -e "ssh -p 22 -i ~/.ssh/rsync_deploy -o IdentitiesOnly=yes" \
  ./ \
  [email protected]:/home/myuser/otherproject

This should not provide access to /home/myuser/otherproject or any other directory outside the restricted root. With rrsync, the remote path is interpreted relative to /home/myuser/myproject. Therefore, /home/myuser/otherproject refers to: failed: No such file or directory

Test 3: Try executing another SSH command

Now, try running a regular command over SSH:

ssh -T \
  -p 22 \
  -i ~/.ssh/rsync_deploy \
  -o IdentitiesOnly=yes \
  [email protected] \
  "whoami"

Expected output: /usr/bin/rrsync error: SSH_ORIGINAL_COMMAND does not run rsync. The exact error prefix may vary depending on the installed rrsync version.

Instead of executing whoami, SSH invokes the forced rrsync command. Because whoami is not an allowed rsync operation, the request is rejected. This confirms that the key cannot be used to execute arbitrary SSH commands.

Test 4: Verify that downloading is disabled

Since we configured rrsync with -wo, downloading files should also be rejected.

Try:

rsync -avz \
  -e "ssh -p 22 -i ~/.ssh/rsync_deploy -o IdentitiesOnly=yes" \
  [email protected]:/ \
  ./download-test/

The transfer should fail because the SSH key only permits uploads.

Step 6: Use the key in a CI/CD pipeline

Once the tests pass, the restricted key can be used in your deployment pipeline. Store the private key in your CI/CD platform as a protected secret, for example: SSH_DEPLOY_KEY. Configure your pipeline to use this key for SSH authentication and execute an rsync upload.

A deployment command might look like:

rsync -az --exclude='.git/' --exclude='.github/' --exclude='node_modules/' \
   -e "ssh -p $SSH_PORT -i ~/.ssh/rsync_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" \
   ./ "$SSH_USER@$SSH_HOST:$DEPLOY_PATH/"

The CI/CD system must also verify the server’s SSH host key using a trusted known_hosts entry. Avoid disabling host key verification with StrictHostKeyChecking=no.

Additional security considerations

A restricted SSH key significantly reduces the capabilities available to a CI/CD pipeline, but it does not replace other security controls. For production environments, consider the following:

  • Use a dedicated deployment user. Avoid using root for automated file transfers.
  • Apply filesystem permissions. Make sure the deployment account can only modify files it actually needs to deploy.
  • Protect the private key. Store it in your CI/CD secret manager and limit which jobs or branches can access it.
  • Restrict the source IP when possible. The SSH from="..." option can limit where a key may be used, provided your pipeline uses predictable source addresses.
  • Protect shell startup files. The rrsync documentation warns about Bash startup files potentially being executed before the forced command. Ensure the deployment process cannot modify the user’s shell configuration, or use an appropriate simpler login shell such as dash.
  • Consider the risk of overwrites and deletions. Write-only mode prevents downloading, but it does not prevent modification of existing files. Options such as -no-del and -no-overwrite can provide additional restrictions where appropriate.
  • Keep rsync and OpenSSH updated. Security restrictions depend on the behavior of the installed software versions.

Also remember that rrsync restricts rsync operations; it is not a filesystem sandbox. The myuser account’s underlying permissions and access to other authentication methods still matter.

Conclusion

Using rrsync with SSH key restrictions is a simple and effective way to implement least-privilege file transfers in CI/CD pipelines. Instead of giving your pipeline unrestricted SSH access, you can limit it to a single task: synchronizing files within a predefined directory.

With this configuration:

  • The pipeline can upload files using rsync.
  • File transfers are restricted to the project directory.
  • Arbitrary SSH commands are rejected.
  • Interactive SSH access and forwarding features are disabled for the restricted key.
  • Downloading files can be disabled using write-only mode.

This approach is particularly useful for application deployments, static website publishing, automated backups, and other workflows where file synchronization is the only required SSH operation.

Author: Joanna

I write code that (usually) works. I tame WordPress, learn React, explore the world of DevOps, and click around in the Linux console. I optimize code as a hobby, because laziness is the purest form of productivity. I do my best not to lose my mind somewhere between a bug and a deadline.

You can also like

Related posts