This content originally appeared on DEV Community and was authored by Eunyoung Jeong
How We Fixed SSH in GitHub Actions (Real Use Cases)
The Problem: SSH in CI/CD Workflows
If you’re using GitHub Actions to deploy to your servers, you’re probably doing something like this:
- name: Execute remote SSH commands
uses: <ssh-action>
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.PORT }}
script: |
cd /app
git pull
npm install
pm2 restart app
This approach has worked for years, but it brings significant security and operational challenges that get worse as your infrastructure grows.
Security Risks
Port 22 Exposed to the Internet
Every server with SSH enabled is under constant attack:
- Bots scan for open port 22 thousands of times per day
- Automated brute force attempts against your SSH daemon
- Zero-day vulnerabilities in SSH daemon itself become attack vectors
- Even with fail2ban and strict firewall rules, the attack surface exists
Check your auth logs, you’ll see hundreds of failed login attempts from random IPs. That’s the reality of exposing SSH to the internet.
Full Server Access with SSH Keys
SSH keys are all-or-nothing:
- Grant complete access to the server, no way to restrict which commands can be executed
- Read-only operations require the same access level as destructive ones
- If a key leaks (committed to git, logged, stolen laptop), attackers have full control
- No built-in mechanism to limit what GitHub Actions workflows can do
SSH Keys Scattered Across GitHub Secrets
Managing SSH keys at scale becomes exponential pain:
- One SSH key per server: 10 servers = 10 different keys to track
- Each repository needs its own copy of these secrets
- No centralized view of which keys have access to which servers
- Organizational secrets help, but you’re still managing one key per server
Operational Pain Points
Key Rotation is a Nightmare
When you need to rotate SSH keys (security incident, compliance requirement, or just good practice):
- Generate new key pairs for each server
- Update
authorized_keys
on every server - Update GitHub Secrets in every repository that deploys to those servers
- Hope you didn’t miss anything
- Deal with broken deployments when you inevitably did
For a team with 10 servers and 20 repositories, this is hours of work that nobody wants to do. So keys don’t get rotated as often as they should.
No Centralized Audit Trail
When something goes wrong in production:
- Logs are scattered across multiple servers
- SSH logs show IP addresses (GitHub Actions runners), not which repo/workflow ran what
- Manual investigation required: “Who deployed to prod last Tuesday at 3pm?”
- No easy way to trace which workflow executed which commands
You end up cross-referencing GitHub Actions logs, SSH auth logs, bash history, and application logs across multiple servers. It’s tedious and error-prone.
Per-Repository Secret Management
The same SSH keys get duplicated across repositories:
- Update one key → Update it in dozens of places
- Revoking access means touching multiple repositories
- No fine-grained control over what each repository can do
- Junior developer’s first deploy has the same access as senior engineer’s emergency fix
The Real Cost
These aren’t just theoretical concerns. Here’s what actually happens:
- Security incidents: Leaked SSH keys in git history require emergency rotation across all servers and repos
- Broken deployments: Missed secret updates during key rotation cause failed releases at 2am
- Compliance failures: Auditors ask “Who ran this command?” and you can’t easily answer
- Operational overhead: DevOps teams spend hours managing SSH keys instead of building features
- Security compromise: Keys don’t get rotated because it’s too painful, increasing risk over time
And the worst part? This scales badly. Double your servers, and you double your key management burden.
The Solution: Alpacon
Alpacon replaces SSH with API tokens and provides four purpose-built GitHub Actions. Here’s what changes:
Authentication:
- SSH keys → API tokens
- Port 22 exposure → Reverse connection (zero open ports)
- Full access → Command-level ACLs
What you gain:
- Command ACL (Access Control Lists): Tokens only run commands you explicitly allow, even if leaked, only permitted commands work
- Workspace-level token: One token manages multiple servers at once
- Automatic audit trail: Know exactly which repo/workflow accessed which server
- Flexible token management: Update token permissions or rotate without touching GitHub Secrets
- Zero open ports: Reverse connection, servers never listen for incoming connections
How It Works in Detail
1. Command ACL
Unlike SSH keys (which give full access), Alpacon tokens support fine-grained command ACLs.
How it works:
When creating a token, you specify exactly which commands it can execute. Commands with operators (&&
, ||
, |
, >
, ;
) are blocked. This prevents command chaining or shell injection.
Prevention benefits:
Even if the token leaks, attackers cannot execute unauthorized commands. The token can ONLY run commands you explicitly allowed.
2. Zero Exposed Ports (Reverse Connection)
Alpacon uses a reverse connection model:
- Your server initiates an outbound connection to your workspace
- No inbound ports opened on your server
- Server is not discoverable from the internet
Threats eliminated: SSH port scanning, brute force attacks, direct connection attempts.
3. Centralized Audit Trail
All GitHub Actions activity is automatically tracked:
- Operator (user/token) and target server
- Exact command executed with full output
- File transfers with size and timestamps
Unlike SSH, where logs are scattered across servers, everything is centralized.
4. Simple Token Rotation
Alpacon: Update token in workspace → Update GitHub Secrets once → Done
SSH: Generate keys for each server → Update authorized_keys
everywhere → Update Secrets in every repo → Debug failures
Usage: Familiar Workflow, Better Security
Remote Command Execution (alpacon-websh-action)
Execute shell commands on remote servers, deployments, restarts, health checks, anything you’d do over SSH.
- name: Setup Alpacon CLI
uses: alpacax/alpacon-setup-action@v1.0.0
- name: Deploy application
uses: alpacax/alpacon-websh-action@v1.0.0
with:
workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
api-token: ${{ secrets.ALPACON_API_TOKEN }}
target: 'your-server'
script: |
cd /app
git pull
npm install
pm2 restart app
- name: Restart system service (with root access)
uses: alpacax/alpacon-websh-action@v1.0.0
with:
workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
api-token: ${{ secrets.ALPACON_API_TOKEN }}
target: 'your-server'
script: systemctl restart nginx
as-root: true
File Transfer (alpacon-cp-action)
Upload build artifacts, download logs, sync directories, everything you’d use SCP for.
- name: Setup Alpacon CLI
uses: alpacax/alpacon-setup-action@v1.0.0
# Upload files to server
- name: Upload build artifacts
uses: alpacax/alpacon-cp-action@v1.0.0
with:
workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
api-token: ${{ secrets.ALPACON_API_TOKEN }}
source: './dist/'
target-server: 'prod-server'
target-path: '/var/www/app/'
recursive: true
# Download files from server
- name: Download logs for analysis
uses: alpacax/alpacon-cp-action@v1.0.0
with:
workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
api-token: ${{ secrets.ALPACON_API_TOKEN }}
source: '/var/log/app/error.log'
target-server: 'prod-server'
target-path: './logs/'
mode: download
Getting Started
Prerequisites: Register Your Servers
Before using these GitHub Actions, you need to register your servers with Alpacon:
- Create a workspace at alpacon.io
- Install Alpacon agent on your servers using the provided install script
- Verify servers are connected in your workspace dashboard
Full guide: Alpacon User Documentation – Creating Workspace & Connecting Servers
Once your servers are registered and showing as “CONNECTED” in your workspace, you can use them from GitHub Actions.
1. Generate API Token
In your workspace settings, create an API token with appropriate command ACLs.
2. Add Secrets to GitHub
Add these secrets to your repository (or organization for shared access):
ALPACON_WORKSPACE_URL: https://alpacon.io/your-workspace
ALPACON_API_TOKEN: your-token-here
3. Use the Actions
Start with alpacon-setup-action
, then add other actions as needed.
The Four Actions
Action | Purpose | Example Use Case |
---|---|---|
alpacon-setup-action | Install Alpacon CLI | Always run first |
alpacon-websh-action | Execute remote commands | Deploy, restart services |
alpacon-cp-action | Transfer files | Upload builds, download logs |
alpacon-common-action | Run any Alpacon command | List servers, check status |
Resources
- Alpacon CLI Documentation
- GitHub Marketplace – Setup Action
- GitHub Marketplace – Websh Action
- GitHub Marketplace – CP Action
- GitHub Marketplace – Common Action
- Previous Article: SSH Problems
Conclusion
SSH served us well for 30 years. But modern infrastructure demands modern solutions:
- API tokens instead of SSH keys
- Command ACLs instead of full access
- Audit logs instead of manual investigation
- Zero exposed ports instead of internet-facing SSH
- Centralized management instead of per-repo configuration
Alpacon GitHub Actions make these benefits accessible in your CI/CD pipelines today.
Try it in your next deployment and see the difference.
Questions of feedback?
We’d love to hear from you in our Discord community.
Join our Discord: [https://discord.gg/wadWh8VsYB]
or drop a comment below!
Want hands-on experience with extra rewards?
We’re running a beta testing program where you can explore GitHub Actions integration and earn rewards while helping us improve.
Join the beta: [https://forms.gle/t1rcgXgyPfZewYgF8]
This content originally appeared on DEV Community and was authored by Eunyoung Jeong