Connect to GitHub runners (Windows, Linux & macOS) via SSH with jump host tunneling, bore tunneling, or direct connections (e.g., Tailscale, Ngrok), and remote access capabilities
Greatly inspired by mxschmitt/action-tmate but takes a completely different approach by installing an OpenSSH server on the runner, enabling standard SSH connections with full port forwarding, VS Code remote development support, and flexible connectivity through your own jump hosts, public free services (serveo.net, ssh-j.com, pinggy.io), bore tunneling, or direct connections (e.g., Tailscale).
- 🖥️ Multi-Platform Support - Works on Linux, Windows, and macOS runners
- 🔧 OpenSSH Server - Installs and configures OpenSSH server on the runner for standard SSH access
- 🌉 Flexible Connectivity - Use your own jump hosts, public free services (serveo.net, ssh-j.com, pinggy.io), bore tunneling, or direct connections (e.g., Tailscale)
- 🕳️ Bore Tunneling - Built-in support for bore.pub and custom bore servers for easy access
- 🔐 Flexible Authentication - Support for SSH keys, authorized keys, and GitHub actor keys
- ⚡ Detached Mode - Run SSH sessions in background while continuing workflow
- 🔄 Session Management - Configurable timeouts and termination controls
- 📖 Rich Help Messages - Beautiful connection guides with VS Code integration tips
- 🛠️ Developer Friendly - Perfect for debugging CI/CD issues and testing
- 💻 VS Code - Full remote development support with port forwarding
⚠️ Public Runner Focused - Designed for GitHub-hosted runners in isolated environments
Set up the following environment variables in your GitHub repository settings (Settings → Secrets and variables → Actions → Variables):
name: Debug Workflow
on: workflow_dispatch
# Define environment variables in GitHub UI (Repository Settings → Variables)
env:
SSH_JUMP_HOST: 'your-jump-host.net'
SSH_JUMP_PORT: '22'
SSH_JUMP_USER: 'your-user'
SSH_JUMP_FORWARD: '2222'
SSH_JUMP_HOST_KEYS: |
your-jump-host.net ssh-rsa AAAAB3NzaC1yc2E...
jobs:
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH Debug Session
uses: lexbritvin/ssh-session-action@v1
with:
ssh-jump-host: ${{ env.SSH_JUMP_HOST }}
ssh-jump-port: ${{ env.SSH_JUMP_PORT }}
ssh-jump-user: ${{ env.SSH_JUMP_USER }}
ssh-jump-host-keys: ${{ env.SSH_JUMP_HOST_KEYS }}
ssh-jump-forward: ${{ env.SSH_JUMP_FORWARD }}
ssh-jump-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # Add to GitHub Secrets (not needed for public hosts)
ssh-server-authorized-keys: ${{ secrets.SSH_PUBLIC_KEYS }} # Add to GitHub Secrets
use-actor-ssh-keys: 'true'For the simplest setup, use bore tunneling which requires no additional configuration:
name: Debug with Bore
on: workflow_dispatch
jobs:
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH Debug Session (Bore)
uses: lexbritvin/ssh-session-action@v1
with:
use-bore: 'true'
use-actor-ssh-keys: 'true'
ssh-server-authorized-keys: ${{ secrets.SSH_PUBLIC_KEYS }}Check the Runner output for connection instructions.
Public jump hosts don't require a private key to connect and are great for quick debugging sessions.
env:
SSH_JUMP_HOST: 'ssh-j.com'
SSH_JUMP_USER: ':generate' # Generates a scoped user for host alias
SSH_JUMP_FORWARD: ':generate' # Generates a unique host alias within the jump host
SSH_JUMP_HOST_KEYS: |
ssh-j.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC...env:
SSH_JUMP_HOST: 'free.pinggy.io'
SSH_JUMP_PORT: '443'
SSH_JUMP_USER: 'tcp' # Use 'tcp' to forward any TCP connection
SSH_JUMP_FORWARD: '0' # Use random port allocation
SSH_JUMP_HOST_KEYS: |
[free.pinggy.io]:443 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC...env:
SSH_JUMP_HOST: 'serveo.net'
SSH_JUMP_PORT: '22'
SSH_JUMP_FORWARD: ':generate' # Generates a unique host alias (can also use '0' for random public port)
SSH_JUMP_HOST_KEYS: |
serveo.net ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC...Bore tunneling provides an easy way to expose your SSH server without requiring jump host configuration.
- name: Setup SSH with Bore (Default)
uses: lexbritvin/ssh-session-action@v1
with:
use-bore: 'true'
use-actor-ssh-keys: 'true'- name: Setup SSH with Custom Bore Server
uses: lexbritvin/ssh-session-action@v1
with:
use-bore: 'true'
bore-server: 'your-bore-server.com:2200'
bore-secret: ${{ secrets.BORE_SECRET }}
use-actor-ssh-keys: 'true'- Install the Remote - SSH extension in VS Code
- Open Command Palette (
Ctrl+Shift+PorCmd+Shift+Pon macOS) - Select "Remote-SSH: Connect to Host..."
- Enter the SSH command from the action outputs
- Open the workspace directory on the remote host
- Use VS Code's integrated terminal and port forwarding features
# Forward local port 3000 to remote port 8080
ssh -L 3000:localhost:8080 user@hostname -p 2222 -N -T
# Access forwarded service
curl http://localhost:3000
# Multiple port forwards
ssh -L 3000:localhost:8080 -L 5000:localhost:5000 user@hostname -p 2222# Method 1: Create file (Linux/macOS)
touch ./end-session
# Method 2: PowerShell (Windows)
New-Item -ItemType File -Name './end-session' -Force
# Method 3: Cancel workflow in GitHub UI (Actions tab → Cancel)- 🚫 Public Access: Be cautious with public jump host services and bore tunneling - they may expose your session to others
- 🔐 Private Keys: Always store SSH private keys in GitHub Secrets, never in code
- 🔒 Authorized Keys: Limit authorized keys to trusted public keys only
- ⏱️ Session Timeouts: Set appropriate timeout values to prevent long-running sessions
- 🌐 Network Access: Consider using private networks like Tailscale for enhanced security
- 🕳️ Bore Security: When using bore tunneling, consider using the
bore-secretparameter for authentication ⚠️ Self-Hosted Runners: Use with caution on self-hosted runners - this action is intended for GitHub-hosted runners that start in clean, isolated environments
| Parameter | Description | Default | Required |
|---|---|---|---|
ssh-server-host |
SSH server hostname or IP address for client connections | '' |
No |
ssh-server-port |
SSH server port | 2222 |
No |
ssh-server-user |
SSH username (use :current for current user) |
:current |
No |
ssh-server-authorized-keys |
Authorized public keys (one per line) | '' |
No |
| Parameter | Description | Default | Required |
|---|---|---|---|
ssh-jump-host |
SSH jump host server | '' |
No |
ssh-jump-port |
SSH jump host port | 22 |
No |
ssh-jump-user |
Jump host username (:generate for auto-generation) |
'' |
No |
ssh-jump-forward |
Port forwarding config (:generate, 0, port, host:port) |
'' |
No |
ssh-jump-private-key |
Private key content for jump host authentication | '' |
No |
ssh-jump-private-key-path |
Private key file path for jump host authentication | '' |
No |
ssh-jump-host-keys |
SSH host keys for server verification | '' |
No |
ssh-jump-extra-flags |
Additional SSH flags for jump host connection | '' |
No |
| Parameter | Description | Default | Required |
|---|---|---|---|
use-bore |
Use bore tunneling instead of SSH port forwarding | false |
No |
bore-server |
Bore server address (e.g., bore.pub:2200). Required when use-bore is true | bore.pub |
No |
bore-secret |
Secret key for bore server authentication. Optional but recommended | '' |
No |
| Parameter | Description | Default | Required |
|---|---|---|---|
use-actor-ssh-keys |
Authorize the triggering user's SSH keys | false |
No |
| Parameter | Description | Default | Required |
|---|---|---|---|
wait-file |
File path to monitor for session end. To disable waiting, set '' |
./end-session |
No |
wait-timeout |
Maximum session duration (seconds) | 1800 |
No |
detached |
Run SSH session in background | false |
No |
display-help-message |
Show connection instructions in output | true |
No |
| Output | Description |
|---|---|
ssh-host |
SSH server hostname/IP address |
ssh-port |
SSH server port number |
ssh-user |
SSH username for connection |
ssh-jump-host |
Jump host hostname |
ssh-jump-port |
Jump host port number |
ssh-jump-user |
Jump host username |
ssh-host-keys |
SSH server host keys for verification |
ssh-command |
Complete SSH connection command |
help-message |
Formatted connection guide |
name: Debug on Failure
on: [ push, pull_request ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Tests
run: npm test
continue-on-error: true
- name: Setup Debug Session on Failure
if: failure()
uses: lexbritvin/ssh-session-action@v1
with:
use-bore: 'true'
ssh-server-authorized-keys: ${{ secrets.SSH_PUBLIC_KEYS }}
use-actor-ssh-keys: 'true'- name: Start SSH Debug Session (Background)
id: ssh-session
uses: lexbritvin/ssh-session-action@v1
with:
use-bore: 'true'
ssh-server-authorized-keys: ${{ secrets.SSH_PUBLIC_KEYS }}
use-actor-ssh-keys: 'true'
display-help-message: 'false'
detached: true
- name: Send Slack Notification
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
"text": "🚀 SSH Debug Session Active",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```${{ steps.ssh-session.outputs.ssh-command }}```"
}
}
]
}
- name: 👉 How to connect 👈
env:
HELP_MESSAGE: ${{ steps.ssh-session.outputs.help-message }}
EXTRA_HELP: |
╔══════════════════════════════════════════════════════════════════════════════════════════╗
🐛 GO DEBUGGING WITH DELVE
╚══════════════════════════════════════════════════════════════════════════════════════════╝
\033[1;32m┌─ 🔄 PORT FORWARDING FOR DELVE\033[0m
\033[1;32m│\033[0m \033[1mssh -L 2345:localhost:2345 ...\033[0m
\033[1;32m└─\033[0m
\033[1;33m┌─ 🛠️ DEBUG COMMANDS\033[0m
\033[1;33m│\033[0m \033[1;35mRun all tests:\033[0m
\033[1;33m│\033[0m \033[1mgo test ./...\033[0m
\033[1;33m│\033[0m \033[1;35mRun specific test:\033[0m
\033[1;33m│\033[0m \033[1mgo test -v -run TestFooBar ./...\033[0m
\033[1;33m│\033[0m \033[1;35mDebug specific test:\033[0m
\033[1;33m│\033[0m \033[1mdlv --listen=:2345 --headless --api-version=2 test ./... -- -test.run TestFooBar\033[0m
\033[1;33m│\033[0m \033[1;35mDebug application:\033[0m
\033[1;33m│\033[0m \033[1mdlv debug --headless --listen=:2345 --api-version=2 ./cmd/foo -- [args...]\033[0m
\033[1;33m└─\033[0m
\033[1;34m┌─ 🔗 IDE INTEGRATION\033[0m
\033[1;34m│\033[0m \033[1;36mConnect to Delve on port 2345:\033[0m
\033[1;34m│\033[0m \033[1;36m•\033[0m \033[1;35mVS Code:\033[0m https://github.com/golang/vscode-go/blob/master/docs/debugging.md
\033[1;34m│\033[0m \033[1;36m•\033[0m \033[1;35mGoLand/IntelliJ:\033[0m https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#attach-to-a-process-on-a-remote-machine
\033[1;34m└─\033[0m
\033[1;36m📚 Delve Documentation:\033[0m https://github.com/go-delve/delve/tree/master/Documentation
shell: bash
run: |
echo "SSH Debug Session Started!"
# The help-message output contains ANSI color codes (\033[1;32m etc.)
# Use printf to properly display colored output
printf "%b\n%b\n" "$HELP_MESSAGE" "$EXTRA_HELP"Connect directly to your GitHub runners through your private Tailscale network for secure, NAT-traversal-free access without requiring jump hosts.
- Go to Tailscale Admin Console
- Generate OAuth credentials for GitHub Actions with appropriate scopes
- Configure ACL tags for CI runners (recommended:
tag:ci) - Add the following to your GitHub repository secrets:
TS_OAUTH_CLIENT_ID- Your Tailscale OAuth client IDTS_OAUTH_SECRET- Your Tailscale OAuth secret
Add this to your Tailscale ACL policy to allow SSH access to CI runners:
{
"tagOwners": {
"tag:ci": [
"[email protected]"
]
},
"acls": [
{
"action": "accept",
"src": [
"[email protected]"
],
"dst": [
"tag:ci:22"
]
}
]
}name: Debug with Tailscale
on: workflow_dispatch
jobs:
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Connect to Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Get Tailscale IP
id: tailscale-ip
run: |
TAILSCALE_IP=$(tailscale ip -4)
echo "ip=$TAILSCALE_IP" >> $GITHUB_OUTPUT
echo "🌐 Runner accessible at: $TAILSCALE_IP"
- name: Setup SSH Debug (Tailscale Direct)
uses: lexbritvin/ssh-session-action@v1
with:
# No jump host needed - direct connection via Tailscale
ssh-server-host: ${{ steps.tailscale-ip.outputs.ip }}
ssh-server-port: 2223
use-actor-ssh-keys: true
wait-timeout: 3600These are non-sensitive configuration values that can be shared:
SSH_JUMP_HOST- Your jump host hostnameSSH_JUMP_PORT- Jump host SSH port (usually 22)SSH_JUMP_USER- Username for jump host connectionSSH_JUMP_FORWARD- Port forwarding configurationSSH_JUMP_HOST_KEYS- SSH host keys for verification
These are sensitive values that should be encrypted:
SSH_PRIVATE_KEY- Private key for jump host authenticationSSH_PUBLIC_KEYS- Authorized public keys for runner accessBORE_SECRET- Secret key for bore server authentication (if using bore)TS_OAUTH_CLIENT_ID- Tailscale OAuth client ID (if using Tailscale)TS_OAUTH_SECRET- Tailscale OAuth secret (if using Tailscale)
- name: SSH Debug with Skipped Host Check
uses: lexbritvin/ssh-session-action@v1
with:
ssh-jump-host: ${{ env.SSH_JUMP_HOST }}
# Do not check a server key and use verbose SSH output
ssh-jump-extra-flags: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -vvv"
use-actor-ssh-keys: trueThis project is licensed under the MIT License - see the LICENSE file for details.
⭐ Star this repo if you find it useful!
Made with ❤️ for the GitHub Actions community