Remote SSH Access for GitHub Actions Runner Debugging

The modern CI/CD landscape has seen a massive shift toward GitHub Actions due to its tight integration with the GitHub ecosystem and the inherent simplicity of its configuration. However, as workflows grow in complexity, developers frequently encounter a specific class of failure: the "CI-only bug." These are anomalies that manifest exclusively within the GitHub Actions environment and remain stubbornly unreproducible on local development machines or dedicated staging servers. The inability to interact with the live state of a running job creates a visibility gap that can lead to hours of tedious "print-statement debugging," where a developer must push dozens of commits just to inspect a single variable or directory structure.

Historically, other CI providers like CircleCI offered a "rerun job with SSH" feature, which allowed an engineer to pause a job and enter the environment via a secure shell. GitHub Actions does not provide this as a native, one-click feature, necessitating the use of third-party actions, gateway servers, or tunneling software to achieve the same result. This capability is not merely a luxury for debugging failures; it is a critical tool for the initial setup phase of a workflow. When a developer is unsure of the exact software versions available in the ubuntu-latest image—the most common and documented environment for GitHub Actions—the ability to SSH into the runner allows them to "fiddle around" in real-time to discover the correct instructions and dependencies required for a successful build.

The shift toward containerization via Docker has mitigated some of these environment discrepancies, as containers ensure a consistent runtime. Nevertheless, the vast majority of workflows still run directly on the host VM provided by GitHub, meaning the need for direct shell access remains a primary requirement for high-velocity engineering teams.

The Gateway Server Architecture for Runner Access

One sophisticated method for establishing a connection to a GitHub Actions runner involves the use of an intermediary gateway server. This architecture is designed to bridge the gap between the public internet and the private network of the GitHub runner VM. In this model, the runner does not expose itself directly to the web; instead, it establishes a connection to a gateway server that acts as a proxy.

To implement this via the edipizarro/runner-open-ssh action, the gateway server must be accessible from the internet and configured specifically to allow the transit of SSH traffic. This requires modifications to the gateway's SSH daemon configuration, typically found at /etc/ssh/sshd_config. Specifically, the following directives must be enabled:

AllowTcpForwarding yes
GatewayPorts yes

Without these settings, the gateway cannot forward the TCP traffic from the external user to the internal tunnel created by the runner.

The implementation requires a specific set of secrets and parameters to ensure the connection is secure and functional. The following table outlines the required configuration inputs:

Parameter Description Requirement/Default
sshPublicKey The public SSH key that will be authorized within the GitHub runner environment. Mandatory
gatewayPrivateKey The private key used by the action to authenticate and open the proxy on the gateway. Mandatory
gatewayIP The public IP address of the intermediary gateway server. Mandatory
gatewayUser The specific username used for SSH authentication on the gateway server. Mandatory
maxLifeTime The duration the connection remains open in seconds. Default: 3600 (1 hour)
port The port used for the SSH connection. Default: 2222

The operational flow of this setup involves the runner executing a sleep command based on the maxLifeTime value (e.g., sleep 3600). This keeps the action active while the user debugs. To maintain the session, the action attaches to a tmux terminal. This is a critical design choice because it prevents the session from terminating prematurely. If a developer wishes to end the debugging session and allow the workflow to proceed to the next step, they must execute the command tmux kill-session within the terminal. Alternatively, the action can be cancelled manually via the GitHub UI.

To actually connect to the runner once the proxy is established, the developer must perform a two-step SSH jump:

  1. First, SSH into the gateway server: ssh gatewayUser@gatewayIP
  2. From the gateway, SSH into the local proxy port: ssh -p 2222 runner@localhost

Specialized SSH Execution with appleboy/ssh-action

While the gateway method is focused on interactive debugging, there are scenarios where the goal is not to enter a shell but to execute specific commands on a remote host as part of the CI pipeline. The appleboy/ssh-action provides a robust framework for this, built with Golang and drone-ssh. This tool is designed for multi-host execution and advanced authentication, moving away from interactive debugging toward automated remote management.

The configuration for this action is highly granular, allowing for precise control over the connection protocol and security. The primary parameters include:

  • host: The address of the remote SSH host.
  • port: The SSH port number, defaulting to 22.
  • username: The SSH username for authentication.
  • password: The SSH password, if not using key-based auth.
  • protocol: The network protocol version, supporting tcp, tcp4, and tcp6, with tcp as the default.
  • sync: A boolean flag that determines if commands should run synchronously when multiple hosts are targeted (defaults to false).
  • timeout: The connection timeout, defaulting to 30s.
  • key: The raw content of the SSH private key (e.g., the contents of ~/.ssh/id_rsa).
  • key_path: An alternative to the key parameter, providing the specific path to the private key.
  • passphrase: The password used to decrypt the private key.
  • fingerprint: The SHA256 fingerprint of the host public key to prevent man-in-the-middle attacks.
  • useinsecurecipher: A boolean to allow less secure ciphers (defaults to false).
  • cipher: The specific allowed cipher for the connection.

This action is primarily used for deployment tasks, such as triggering a script on a production server after a successful build, rather than the "live-debugging" of the runner itself.

Managed Debugging Environments and Lifecycle Hooks

Advanced CI platforms, such as Blacksmith, have implemented a more integrated approach to SSH debugging that removes the need for manual gateway configuration or VPNs. This method leverages GitHub Actions hooks for lifecycle management to provide "instant" secure access.

A core component of this security model is the dynamic fetching of SSH keys via the GitHub API. This ensures that access is sender-specific; only the individual who triggered the workflow is granted the ability to SSH into the VM. This eliminates the risk of static keys being leaked or shared across a wide team.

One of the primary challenges in interactive debugging is the "VM Teardown" problem. Normally, GitHub Actions terminates the VM as soon as the job ends. If a developer is in the middle of an SSH session when the job completes, they would be abruptly disconnected. To solve this, a grace period is implemented. The system monitors for active SSH sessions using a loop that checks the network state every 10 seconds:

```bash

Check for active SSH sessions every 10 seconds

while [ $elapsed -lt $MAXWAITSECONDS ]; do
if ss -tn state established '( sport = :22 )' | grep -q ':22'; then
hassshsessions=true
sleep 10
else

No sessions? Exit immediately

break
fi
done
```

This logic is embedded in the "Complete runner" step. The system grants a grace period of exactly 4 minutes and 50 seconds. The reason for this specific timing is due to an undocumented GitHub limitation: the job completion hook has a 5-minute timeout. If the grace period were exactly 5 minutes, the hook might be killed by GitHub before it could gracefully exit, potentially leaving orphaned processes or failing the cleanup step.

The user experience for this managed approach is streamlined. Upon job start, the logs provide a direct command:

ssh -p 38291 [email protected]

Once the job completes, the system detects the active session and notifies the user: "Job completed. SSH sessions active, waiting up to 4m50s..." This allows the developer to finish their investigation without the fear of the VM disappearing mid-command.

Tunneling and NAT Traversal via Relay Servers

For those not using a managed service or a dedicated gateway server, third-party tools like action-sshd-cloudflared offer an alternative. This approach utilizes the concept of a public relay server to solve the problem of NAT (Network Address Translation) traversal. Because GitHub runner VMs do not have public IP addresses or routable IPv6 addresses, they cannot be reached directly from the internet.

Relay servers act as a meeting point. The host (the GitHub VM) uses a client to connect to the relay server, and the developer connects to that same relay server to share a terminal session. This is functionally similar to how tools like ngrok operate, or how standard SSH TCP forwarding (using the -L and -R flags) creates a tunnel.

However, there is a significant security architectural concern with certain relay implementations. In some designs, the relay server is not merely a TCP relay—which would simply forward encrypted packets—but instead requires plaintext access to the SSH connection to implement its business logic. From a security perspective, this is highly problematic, as it means the relay server can potentially intercept or see the traffic that should be encrypted end-to-end between the client and the runner.

Comparison of SSH Access Methodologies

The following table provides a comparative analysis of the different methods used to gain shell access to GitHub Actions runners.

Method Primary Use Case Security Model Ease of Setup Persistence
Gateway Server (edipizarro) Interactive Debugging Private Key/Proxy Medium (Requires VM) Until tmux kill-session
Command Execution (appleboy) Deployment/Automation SSH Key/Password Easy Non-interactive
Managed Lifecycle (Blacksmith) Deep Debugging GitHub API/Sender-specific Very Easy 4m50s Grace Period
Relay Tunnels (cloudflared) Quick Access Relay-mediated Easy Session-based

Detailed Technical Analysis of SSH Debugging

The necessity of SSH access in CI/CD is fundamentally a battle against the "black box" nature of ephemeral runners. When a workflow fails, the only data available is the stdout and stderr logs. If a failure is caused by a race condition, a missing environment variable, or a specific file permission issue in the ubuntu-latest image, logs are often insufficient.

By establishing an SSH connection, the developer transforms the CI environment from a black box into a transparent sandbox. This allows for the use of tools that cannot be easily logged, such as top or htop for resource monitoring, netstat or ss for network debugging, and the ability to manually execute a series of commands to pinpoint the exact moment a failure occurs.

The use of tmux in the gateway model is particularly important because GitHub Actions steps are sequential. If a step merely starts an SSH server and then finishes, the runner would normally move to the next step or terminate. By using tmux and a sleep command, the action effectively "pauses" the workflow, holding the VM in a state of suspension while the developer is connected.

The security implications of these methods vary wildly. The most secure is the sender-specific key model, which ensures that the identity of the person accessing the VM is verified by GitHub's own authentication layer. The least secure are those that require the use of insecure ciphers or relay servers that decrypt traffic for business logic, as these introduce man-in-the-middle vulnerabilities.

Conclusion

The ability to SSH into a GitHub Actions runner is a critical requirement for professional software engineering, bridging the gap between local development and the final CI environment. While GitHub does not offer a native "pause and debug" feature similar to CircleCI, the community has developed several robust alternatives. These range from the appleboy/ssh-action for automated remote execution to the edipizarro/runner-open-ssh for interactive proxy-based debugging.

For those requiring high security and minimal configuration, managed solutions like Blacksmith provide a seamless experience by integrating with GitHub's API for key management and implementing a precise 4-minute and 50-second grace period to account for GitHub's internal 5-minute timeout on completion hooks. Regardless of the method chosen, the primary goal remains the same: transforming the ephemeral, isolated runner into an accessible environment where bugs can be isolated, environment versions can be verified, and the path to a successful build can be mapped with precision.

Sources

  1. Debugging a GitHub Actions workflow via SSH
  2. SSH to GitHub Action Runner - GitHub Marketplace
  3. Blacksmith Blog: SSH
  4. SSH for GitHub Actions - appleboy/ssh-action

Related Posts