{*}SecureCodeHQ
SSH for Developers: Keys, Servers, and AI Agents
·by Juan Isidoro·9 min read

SSH for Developers: Keys, Servers, and AI Agents

Set up SSH step by step: server connections, GitHub deploy keys, and protecting your keys when using AI agents like Claude Code or Cursor.

sshsecuritydevopsai-agentsbest-practices

SSH authenticates you without passwords using asymmetric cryptography. Instead of sending a secret over the network, you prove you have the private key without ever revealing it.

This tutorial covers three scenarios you will run into sooner or later:

  1. Connecting from your PC to a server
  2. Your server cloning private repos from GitHub
  3. Using SSH with AI agents (Claude Code, Cursor, Copilot Workspace...)

Once you understand these three, you understand SSH.


How it works: key pairs

You always work with a pair:

KeyWhere it livesWhat it does
PrivateNever leaves where it was generatedSigns messages to prove identity
PublicGets copied to wherever you want accessVerifies the signature is valid

Think of it this way: the private key is your ID card. The public key is a photocopy you leave at the front desk of places you want to get into.


Case 1: Connecting your PC to a server

The most common scenario. You want to run ssh user@server without typing a password every time.

Step 1: Generate the pair on your PC

ssh-keygen -t ed25519 -C "your-email@example.com"

This creates two files in ~/.ssh/:

~/.ssh/
├── id_ed25519       # Private key — never share this
└── id_ed25519.pub   # Public key — this one gets copied

Step 2: Copy the public key to the server

# Automatic option (recommended)
ssh-copy-id user@server-ip

# Manual option
cat ~/.ssh/id_ed25519.pub | ssh user@server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Step 3: Connect

ssh user@server-ip

No password. SSH signs with your private key, the server verifies with the public key it has stored.

What's on the server

~/.ssh/
└── authorized_keys   # One line per authorized PC/person

The content is straightforward:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your-email@example.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... other-dev@company.com

Want to grant someone access? Add their .pub as a new line. Revoke? Delete the line.

Flow diagram

YOUR PC                            SERVER
───────                            ──────
~/.ssh/id_ed25519      ──signs──►
~/.ssh/id_ed25519.pub             ~/.ssh/authorized_keys
                       ◄─verifies─

Case 2: Your server needs access to GitHub

The server needs to clone private repos or run git pull in automated deployments.

Step 1: Generate the pair on the server

ssh-keygen -t ed25519 -C "deploy@my-server"

Step 2: Add the public key to GitHub

cat ~/.ssh/id_ed25519.pub

Copy the output and go to:

  • A specific repo: Settings > Deploy Keys > Add deploy key
  • All your repos: Profile > Settings > SSH Keys

Step 3: Test the connection

ssh -T git@github.com

If it works:

Hi username! You've successfully authenticated...

Step 4: Clone or pull

git clone git@github.com:username/private-repo.git

No credentials needed.

Diagram

SERVER                             GITHUB
──────                             ──────
~/.ssh/id_ed25519      ──signs──►
~/.ssh/id_ed25519.pub             Deploy Key
                       ◄─verifies─

Case 3: SSH with AI agents

This is where a lot of developers make mistakes without realizing it.

Tools like Claude Code, Cursor, Aider, or Copilot Workspace execute commands on your behalf. When you say "deploy this to production" or "clone the repo and fix the bug," the agent needs SSH access.

The problem

Your SSH private key is the most sensitive thing you have. If an agent has access to ~/.ssh/id_ed25519, it has access to everything that key opens: production servers, private repos, critical infrastructure.

It's not that the agent is malicious. It's that:

  1. Context errors: you ask it to "clean up temp files" and it deletes ~/.ssh/ because it misunderstood
  2. Prompt injection: malicious code in a repo it clones injects instructions
  3. Logs and telemetry: some agents log executed commands, including keys if you pass them inline
  4. Excessive scope: you give it production access for a task that only needed staging

Rules for using SSH with agents

1. Never expose your main key

Create agent-specific keys with minimal permissions:

# Key only for the agent, only for staging
ssh-keygen -t ed25519 -f ~/.ssh/id_agent_staging -C "agent-staging-only"

2. Use read-only deploy keys

On GitHub, don't check "Allow write access" unless the agent actually needs to push.

3. Limit scope in ~/.ssh/config

# The agent can only use this key for staging
Host staging-for-agent
    HostName staging.mycompany.com
    User deployer
    IdentityFile ~/.ssh/id_agent_staging
    # Production doesn't even appear in its config

4. Use passphrase + ssh-agent with timeout

# Key protected with passphrase
ssh-keygen -t ed25519 -f ~/.ssh/id_agent -C "agent"
# (it will ask for a passphrase)

# Add it to the SSH agent with 1 hour expiration
ssh-add -t 3600 ~/.ssh/id_agent

The AI agent can use the key while it's in memory, but cannot extract it or use it after the timeout.

5. Audit what commands it runs

Before granting SSH access to an agent, understand what it will do. If the task is "format this code," it doesn't need SSH. If it's "deploy to production," think twice.

6. Separate environments radically

~/.ssh/
├── id_personal           # Your use, never for agents
├── id_work               # Manual use at work
├── id_agent_staging      # Agents only, staging only
└── id_agent_ci           # CI/CD automation only

7. Consider a vault for sensitive keys

Instead of leaving keys on disk, use a secrets manager that requires explicit authentication for each use. The agent can't use what it doesn't have.

What NOT to do

  • Give the agent your main ~/.ssh/id_ed25519
  • Run SSH commands the agent suggests without reviewing them
  • Assume "it's just development" when the key also opens production
  • Put private keys in environment variables the agent can read
  • Trust that the agent "knows" which keys are sensitive

Secure setup example for Claude Code

# 1. Create a specific key
ssh-keygen -t ed25519 -f ~/.ssh/id_claude_code -C "claude-code-dev"

# 2. Restrictive config
cat >> ~/.ssh/config << 'EOF'
Host dev-server
    HostName 192.168.1.50
    User developer
    IdentityFile ~/.ssh/id_claude_code
EOF

# 3. Only this key in authorized_keys on the dev server
# (not on production)

# 4. On GitHub: read-only deploy key on non-critical repos

Now Claude Code can work on your dev server, but has no access to production and can't push to critical repos.


Anatomy of the ~/.ssh/ directory

FileWhat it isFixed name?
id_ed25519Your private keyNo, it's the default
id_ed25519.pubYour public keyNo, it's the default
authorized_keysPublic keys allowed to connectYes
known_hostsFingerprints of known serversYes
configHost configuration and aliasesYes

The known_hosts file

The first time you connect to a server, SSH asks:

The authenticity of host '192.168.1.100' can't be established.
ED25519 key fingerprint is SHA256:xXxXxXx...
Are you sure you want to continue connecting (yes/no)?

If you say yes, it saves the fingerprint. Next time it won't ask.

If the server changes (you reinstalled it, IP was reassigned), SSH warns you:

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

Fix:

ssh-keygen -R 192.168.1.100

Multiple keys with ~/.ssh/config

If you manage several servers and services:

# Production
Host prod
    HostName 162.55.62.72
    User deployer
    IdentityFile ~/.ssh/id_production

# Development
Host dev
    HostName 192.168.1.50
    User dev
    IdentityFile ~/.ssh/id_development

# Personal GitHub
Host github.com
    IdentityFile ~/.ssh/id_github_personal

# Work GitHub (different account)
Host github-work
    HostName github.com
    IdentityFile ~/.ssh/id_github_work

Now:

ssh prod                    # instead of ssh deployer@162.55.62.72
git clone git@github-work:company/repo.git

Complete diagram

YOUR PC                        SERVER                           GITHUB
───────                        ──────                           ──────

~/.ssh/                        ~/.ssh/
├── id_ed25519 ───────────────► authorized_keys
├── id_ed25519.pub             ├── id_ed25519 ─────────────────► Deploy Key
├── id_agent_staging ─────────► authorized_keys (staging only)
├── known_hosts                ├── id_ed25519.pub
└── config                     └── known_hosts


Flow 1: PC > Server
        Signs with local private key, verifies with public key in authorized_keys

Flow 2: Server > GitHub
        Signs with server's private key, verifies with public key in Deploy Keys

Flow 3: AI Agent > Dev server
        Uses specific key with limited scope

Command reference

# Generate key pair
ssh-keygen -t ed25519 -C "comment"

# Generate with specific name
ssh-keygen -t ed25519 -f ~/.ssh/my_key -C "comment"

# Copy public key to server
ssh-copy-id user@server

# View your public key
cat ~/.ssh/id_ed25519.pub

# Test GitHub connection
ssh -T git@github.com

# Remove old server fingerprint
ssh-keygen -R server-ip

# Connect with specific key
ssh -i ~/.ssh/my_key user@server

# View key fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub

# Add key to SSH agent with timeout (1 hour)
ssh-add -t 3600 ~/.ssh/my_key

# List keys in SSH agent
ssh-add -l

# Remove all keys from SSH agent
ssh-add -D

Common errors

"Permission denied (publickey)"

  • Your public key is not in authorized_keys on the server
  • You're using the wrong private key
  • Fix: check with ssh -v user@server to see which key it tries

"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED"

  • The server changed (reinstalled, IP reassigned to a different machine)
  • Fix: ssh-keygen -R server-ip

"Permissions are too open"

  • The private key has 644 permissions or similar
  • Fix: chmod 600 ~/.ssh/id_ed25519

"Could not resolve hostname"

  • The alias in ~/.ssh/config is misspelled
  • You don't have an entry for that host

"Agent admitted failure to sign"

  • The SSH agent doesn't have the key loaded
  • Fix: ssh-add ~/.ssh/your_key

SSH security checklist

  • Each context (personal, work, CI, AI agents) has its own key
  • Private keys have 600 permissions
  • The ~/.ssh directory has 700 permissions
  • Sensitive keys have a passphrase
  • AI agents only access keys with limited scope
  • GitHub deploy keys are read-only unless you need push
  • You have backups of infrastructure keys in a vault
  • You rotate keys when someone leaves the team or a machine gets compromised
  • No private keys in repos, logs, or exposed environment variables

Conclusion

SSH isn't complicated once you understand that everything revolves around one concept: the private key signs, the public key verifies.

What does get complicated is managing multiple keys across multiple contexts, especially now that AI agents are part of the workflow. The golden rule: least privilege. Each key should only open the doors it needs to open, and automated agents should have the most restrictive keys of all.


Further reading