Securing Remote Authentication and Deployment via SSH in GitLab CI/CD

The integration of Secure Shell (SSH) keys within GitLab CI/CD pipelines represents a critical architectural requirement for any organization aiming to automate the movement of code from a build environment to a production or staging server. While GitLab provides a robust orchestration engine through its runners, it does not offer built-in native management of SSH keys within the transient build environment itself. This design choice necessitates a manual but structured injection process where the practitioner must bridge the gap between the GitLab CI/CD variable store and the ephemeral nature of the runner's file system. When a GitLab Runner executes a job, it typically operates within a clean slate—often a Docker container—which lacks the necessary identity markers to authenticate against remote servers. By implementing a strategic SSH key injection workflow, developers can enable a wide array of essential operations, including the checkout of internal submodules, the acquisition of private packages via managers like Bundler, and the seamless deployment of applications to external platforms such as Heroku or self-managed Linux servers.

The fundamental challenge in this process is the secure handling of the private key. Because the build environment is ephemeral, any key used must be provided at runtime without being permanently baked into the Docker image, which would constitute a catastrophic security failure. The industry standard for achieving this in GitLab is the use of CI/CD variables, which act as a secure vault. However, the implementation requires precision; failure to correctly handle line breaks, file permissions, or the initialization of the SSH agent will result in common pipeline failures, such as the dreaded "No such file or directory" error when the system attempts to access a non-existent .ssh directory. To achieve a successful deployment, the pipeline must not only possess the key but also trust the remote host, typically achieved through the ssh-keyscan utility, which populates the known_hosts file and prevents the pipeline from hanging on an interactive "authenticity of host" prompt.

Operational Requirements for SSH in GitLab CI/CD

The ability to use SSH keys is a universal feature across the GitLab ecosystem, ensuring that regardless of the organizational scale, the deployment pipeline remains consistent. This functionality is available across all service tiers and delivery models.

Tier/Offering Availability Support Level
Free Supported Full
Premium Supported Full
Ultimate Supported Full
GitLab.com (SaaS) Supported Full
GitLab Self-Managed Supported Full
GitLab Dedicated Supported Full

The specific use cases for this implementation are diverse and cover the entire software development lifecycle.

  • Internal Submodule Management: When a project depends on other private repositories as submodules, the GitLab Runner requires an SSH identity to clone these dependencies during the build phase.
  • Private Package Acquisition: Many package managers require authentication to download proprietary libraries. SSH keys allow the runner to authenticate with these private registries.
  • Remote Application Deployment: Moving a compiled artifact or a Docker image to a target server often requires a secure shell to execute deployment scripts or trigger a restart of services.
  • Command Execution: Beyond file transfers, SSH keys allow the pipeline to run remote commands, such as systemctl restart nginx or docker-compose pull.
  • Data Synchronization: The use of rsync for efficient file transfers between the build environment and the remote server depends entirely on a valid SSH handshake.

Strategic Implementation of SSH Keys

The most resilient and widely supported method for implementing SSH authentication is the injection of keys via .gitlab-ci.yml extensions. This strategy is executor-agnostic, meaning it functions identically whether the runner is utilizing a Docker executor, a Shell executor, or a Kubernetes pod.

Key Generation and Distribution

The process begins with the creation of a cryptographically secure key pair. While RSA is common, the ed25519 algorithm is highly recommended for its superior security and smaller key size.

To generate a key pair on a machine, the following command is utilized:

ssh-keygen -t ed25519 -C "[email protected]"

Upon execution, the user is prompted for a file location. Accepting the default path results in the creation of two critical files:

  • ~/.ssh/id_ed25519: The private key, which must remain secret and be stored in GitLab.
  • ~/.ssh/id_ed25519.pub: The public key, which must be distributed to all target environments.

The public key must be appended to the ~/.ssh/authorized_keys file on the remote server to permit access. If the target is another private GitLab repository, the public key must also be added as a "Deploy Key" within that specific project's settings to grant the runner read-only or read-write access.

Configuring GitLab CI/CD Variables

GitLab allows the storage of the private key in two primary formats: as a "File" type variable or a "Regular" variable.

The File Type Variable Approach

Adding the key as a file type variable is the preferred method for many. In this configuration, GitLab stores the content and automatically creates a temporary file on the runner's disk, providing the path to that file via an environment variable.

  • Visibility Setting: The visibility must be set to "Visible". This is a mandatory requirement because SSH keys contain whitespace characters. GitLab's "Masked" variable feature cannot handle variables containing whitespace, making the "Masked" option incompatible with raw SSH keys.
  • Content Integrity: The value must end with a newline (LF character). If the key is pasted without a final return/enter, the SSH agent may fail to parse the key correctly.
  • Security Warning: Because these variables cannot be masked, users must never use commands like cat or tee on the variable, as this would print the private key in plain text to the job logs, exposing the secret to anyone with pipeline access.

The Regular Variable Approach

If a regular variable is used, the key is stored as a string. This requires the pipeline to manually pipe the variable into a file and set the appropriate permissions. This method is often used in conjunction with tr -d '\r' to ensure that Windows-style line endings do not corrupt the key when processed in a Linux-based Docker image.

Technical Configuration and Pipeline Architecture

The actual implementation within the .gitlab-ci.yml file requires a precise sequence of operations. The failure to perform these steps in the correct order—specifically creating the directory before adding the key—will lead to execution errors.

The Pre-Execution Phase (before_script)

The before_script section is where the environment is prepared. The following sequence is critical for a successful SSH handshake.

First, the environment must ensure the ssh-agent is installed. In many minimal Docker images (like ubuntu or alpine), the client is missing.

which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )

Once the client is present, the agent must be initialized to manage the keys in memory.

eval $(ssh-agent -s)

The private key is then loaded into the agent. If using a regular variable, the following command is employed to handle line endings and add the key:

echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -

Alternatively, if the key is stored as a file, the agent adds the file directly:

ssh-add ~/.ssh/id_rsa

The pipeline must then establish the .ssh directory with restrictive permissions to satisfy the security requirements of the SSH client.

mkdir -p ~/.ssh
chmod 700 ~/.ssh

Finally, to prevent the pipeline from failing due to an "Unknown Host" error, the runner must scan the public key of the remote server.

ssh-keyscan -H 'your.server.hostname' >> ~/.ssh/known_hosts

Implementation Examples

Below is a comprehensive example of a pipeline configured for a staging environment, utilizing a Docker-in-Docker (DinD) setup to build and deploy a containerized application.

```yaml
image: docker:git
services:
- docker:dind
stages:
- staging

beforescript:
- docker login -u gitlab-ci-token -p $CI
BUILDTOKEN $CIREGISTRY
- mkdir -p ~/.ssh
- echo "$DEPLOYSERVERPRIVATEKEY" | tr -d '\r' > ~/.ssh/idrsa
- chmod 600 ~/.ssh/idrsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id
rsa
- ssh-keyscan -H 'your.server.hostname' >> ~/.ssh/known_hosts

staging:
stage: staging
tags:
- docker
only:
- staging
script:
- docker build --pull -t $CIREGISTRYIMAGE:staging .
- docker push $CIREGISTRYIMAGE:staging
- ssh $SERVERUSER@$SERVERHOSTNAME < deploy.sh
```

Advanced Runner Configurations

In scenarios where the GitLab Runner is self-managed and persistent (rather than using ephemeral Docker containers), the SSH configuration can be handled at the machine level.

Local Runner Setup

If a runner is installed on a dedicated machine, the keys can be generated locally.

  1. Generate the key on the runner machine:
    ssh-keygen -t ed25519 -C "[email protected]"
  2. Add the key to the local agent:
    eval $(ssh-agent -s)
    ssh-add ~/.ssh/id_ed25519
  3. Configure the runner's identity by ensuring the user running the gitlab-runner process has access to these keys.

The configuration file for the runner, located at /etc/gitlab-runner/config.toml (Linux) or C:\GitLab-Runner\config.toml (Windows), defines the environment. While the SSH key is not placed inside the config.toml, the runner must be restarted after any significant change to the system's SSH agent or key files to ensure the new environment is inherited.

systemctl restart gitlab-runner

Troubleshooting and Security Analysis

Implementing SSH in CI/CD introduces several failure points and security risks that must be mitigated.

Common Failure Points

  • Pathing Errors: A common error is /usr/bin/bash: line 157: /root/.ssh/id_rsa: No such file or directory. This occurs when the pipeline attempts to use a key file before the mkdir -p ~/.ssh command has been executed. The directory must exist before any file is written to it.
  • Permission Denied: SSH clients will ignore private keys if the permissions are too open. The private key file must be set to chmod 600 to ensure only the owner can read it.
  • Line Ending Corruption: When copying keys from Windows environments into GitLab variables, hidden carriage return characters (\r) are often introduced. These characters invalidate the key format. The use of tr -d '\r' is the standard remediation to strip these characters before passing the key to ssh-add.

Security Hardening

The use of SSH keys in automation necessitates a rigorous security posture to prevent credential leakage.

  • Key Rotation: Private keys should not be permanent. Regularly rotating keys reduces the window of opportunity for an attacker if a key is compromised.
  • Restricted Branching: To prevent unauthorized users from stealing the SSH_PRIVATE_KEY variable, the variable should be restricted to "Protected" branches or tags. This prevents a developer from creating a feature branch, modifying the .gitlab-ci.yml to echo $SSH_PRIVATE_KEY, and capturing the secret in the job logs.
  • Avoiding Personal Keys: Never reuse a personal SSH key for CI/CD. Create a dedicated "deploy key" with the minimum permissions required for the task.
  • Debugging Risks: While debug logging can help solve connection issues, it may expose the private key in the logs. Visibility of pipelines should be restricted to authorized personnel only.

Conclusion

The successful implementation of SSH within GitLab CI/CD requires a holistic understanding of both the GitLab environment and the fundamental requirements of the OpenSSH protocol. By utilizing CI/CD variables as a secure transport mechanism, the pipeline can dynamically construct a trusted identity that exists only for the duration of the job. The critical path to success involves the precise sequence of initializing the ssh-agent, cleaning the key of carriage returns, establishing the .ssh directory with strict permissions, and utilizing ssh-keyscan to bypass interactive host verification.

From an architectural perspective, the shift toward "File" type variables provides a cleaner abstraction, but the "Regular" variable approach offers more control over the key's formatting via shell manipulation. Regardless of the method chosen, the priority must always be the isolation of the secret. By restricting keys to protected branches and rotating them frequently, organizations can leverage the power of automated SSH deployments without compromising the security of their remote infrastructure. The integration of these elements transforms a GitLab Runner from a simple build tool into a powerful deployment engine capable of interacting securely with any server in the global network.

Sources

  1. GitLab Documentation: Using SSH keys with GitLab CI/CD
  2. Parvaiz Ahmad: Use SSH key with GitLab Runner
  3. Yann Howe Gist: .gitlab-ci.yml for SSH with private key
  4. Gary Bell: Using GitLab CI to deploy via SSH

Related Posts