Stop Using SSH Keys in GitHub Actions (Here’s What to Use Instead)



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

  1. Generate new key pairs for each server
  2. Update authorized_keys on every server
  3. Update GitHub Secrets in every repository that deploys to those servers
  4. Hope you didn’t miss anything
  5. 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:

  1. Command ACL (Access Control Lists): Tokens only run commands you explicitly allow, even if leaked, only permitted commands work
  2. Workspace-level token: One token manages multiple servers at once
  3. Automatic audit trail: Know exactly which repo/workflow accessed which server
  4. Flexible token management: Update token permissions or rotate without touching GitHub Secrets
  5. 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:

  1. Create a workspace at alpacon.io
  2. Install Alpacon agent on your servers using the provided install script
  3. 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

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