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