This began as the private checklist I use when changing computers. At first I only wanted git push to stop asking for my passphrase. Then I added multiple devices, multiple keys, and a Linux server that was itself connecting to GitHub. What follows is not an encyclopedia of SSH; it is the path I want to be able to retrace.

The one direction that explains everything

I used to think of SSH as “something servers use to talk to one another.” That model is only half right. SSH is a client–server exchange, and the roles are different:

[client: initiates and holds the private key]
                    ──SSH──>
[server: accepts and stores the public key]

The sentence I remember is:

The side that initiates the connection keeps the private key. The side that accepts the connection stores the public key.

“Client” and “server” describe one connection, not a machine’s permanent identity:

Windows laptop ──SSH──> Linux server
Linux server   ──SSH──> GitHub

In the first connection, the laptop owns the private key and the server stores its public key in authorized_keys. In the second, the Linux server has become the client; it needs a separate private key whose public half is registered with GitHub.

Two files that are easy to confuse therefore point in opposite directions:

  • id_ed25519: the identity I use to connect outward
  • authorized_keys: the identities allowed to connect to me

I give each device or purpose its own key. A new machine gets a new identity. I do not move private keys through chat, cloud storage, password vaults, or USB drives. When a device disappears, I revoke that device’s public key without disturbing the others.

Before starting

  • Windows 10/11 with OpenSSH, or a Linux system with an OpenSSH client
  • Git, if the destination is GitHub
  • Access to the destination’s public-key settings

1. Generate a key for this machine

Windows and Linux both support:

ssh-keygen -t ed25519 -C "describe-this-device"

With one identity, the default ~/.ssh/id_ed25519 is convenient. With several, I use names such as id_ed25519_github_laptop. I normally protect personal-device keys with a passphrase; it adds a boundary around the private key stored on disk.

The examples below use id_ed25519 for readability. If you choose another filename, change every matching IdentityFile, ssh-add, and .pub path.

When an old device is retired, remove its public key from GitHub or the target server. I preserve the setup process, not the old private key.

2. Start ssh-agent

ssh-agent keeps an unlocked key available in memory and performs signatures on behalf of SSH clients. I enter the passphrase once, then reuse the key while the agent holds it.

Windows

In an elevated PowerShell window:

Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
Get-Service ssh-agent

The Windows agent is a system service, so it is not tied to one terminal window. I verify its real state rather than relying on assumptions about a particular OpenSSH version:

ssh-add -l

Linux desktop

GNOME, KDE, and other desktop environments may already provide an agent or keyring. First check:

echo "$SSH_AUTH_SOCK"

If it prints a socket path, the current shell is connected to an agent. Whether passphrases persist across logins depends on the desktop and keyring configuration.

Headless Linux server

I used to call my server setup “one agent per SSH session.” That was inaccurate. A separately started ssh-agent can outlive the login that created it, and an environment file lets later sessions find the same process.

My actual policy is: one reusable agent per Unix user, recreated after a reboot, with keys loaded for a limited time.

Create ~/.ssh/agent-init.sh:

cat > ~/.ssh/agent-init.sh <<'EOF'
#!/usr/bin/env bash

SSH_ENV="$HOME/.ssh/agent-env"

if [ -r "$SSH_ENV" ]; then
    . "$SSH_ENV" >/dev/null
fi

ssh-add -l >/dev/null 2>&1
status=$?

case "$status" in
    0|1)
        # 0: identities loaded; 1: agent works but is empty
        ;;
    2)
        umask 077
        ssh-agent -s > "$SSH_ENV"
        . "$SSH_ENV" >/dev/null
        ;;
esac

export SSH_AUTH_SOCK SSH_AGENT_PID
EOF

chmod 700 ~/.ssh/agent-init.sh

Load it from ~/.bashrc:

[ -f "$HOME/.ssh/agent-init.sh" ] && . "$HOME/.ssh/agent-init.sh"

Then test it:

source ~/.bashrc
echo "$SSH_AUTH_SOCK"
ssh-add -l

The agent has no identities. is not a failure; it means the agent is reachable but empty.

Load the key for one working day:

ssh-add -t 8h ~/.ssh/id_ed25519

Eight hours is my convenience boundary, not a security standard. Shorten or lengthen it to match the session. ssh-add -D clears all loaded identities.

This also works well with tmux:

source ~/.bashrc
ssh-add -t 8h ~/.ssh/id_ed25519
tmux new -s dev

One trap I hit was replacing .bash_profile just to start the agent. Bash then skipped the previous .profile path, and my prompt and aliases disappeared. If .bash_profile exists, make sure it loads the existing interactive setup:

[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"

Do not let a small agent snippet take ownership of the entire shell configuration.

3. Add the private key

For Windows or a desktop Linux session:

# Windows
ssh-add $env:USERPROFILE\.ssh\id_ed25519
# Linux
ssh-add ~/.ssh/id_ed25519

Enter the passphrase chosen when the key was generated, then confirm the fingerprint appears:

ssh-add -l

On a headless server using the policy above, keep the expiry and use ssh-add -t 8h instead.

4. Confirm which SSH Git is using

On Windows, PowerShell and Git for Windows can invoke different ssh.exe binaries. First inspect the path and any override:

where.exe ssh
git config --global --get core.sshCommand

If ssh -T works in PowerShell but git push still asks for a passphrase, point Git at the Windows OpenSSH client:

git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"

This is a troubleshooting fix, not a ritual I apply to every machine. If the current chain works, I leave global configuration alone.

Linux normally uses the system OpenSSH client and inherits SSH_AUTH_SOCK directly.

5. Install the public key on GitHub

# Windows
Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub | clip
# Linux: print it and copy the complete line
cat ~/.ssh/id_ed25519.pub

In GitHub, open Settings → SSH and GPG keys → New SSH key. Give the key a title that identifies the device, then paste the public key.

For a Linux login rather than GitHub, append the public key to the destination user’s ~/.ssh/authorized_keys.

Verify the full chain

ssh -T git@github.com
git remote -v
git fetch

GitHub’s success message says that authentication worked but shell access is unavailable; that is expected. git fetch verifies repository read access without manufacturing a commit merely to test git push.

For a server connection, verbose mode shows which stage failed:

ssh -v my-server
  • Connection timed out: address, network, firewall, or security group
  • Connection refused: the host is reachable, but nothing listens on that port
  • Permission denied (publickey): user, private key, or authorized_keys mismatch
  • Too many authentication failures: the client offered too many unrelated keys

Keep GitHub-specific configuration scoped

If I want SSH to load the GitHub key on first use, I limit the rule to GitHub:

Host github.com
    User git
    AddKeysToAgent yes
    IdentitiesOnly yes
    IdentityFile ~/.ssh/id_ed25519

I avoid Host * for a single GitHub identity. A narrow rule sends fewer irrelevant keys to other hosts and is easier to debug.

Everyday use

On Windows, I normally add the key once and let the service handle it. After a reboot or configuration change, I check rather than guess:

Get-Service ssh-agent
ssh-add -l

On a headless Linux server, the key remains reusable across SSH logins and tmux only until its -t lifetime expires, I clear it, or the server reboots.

While a key is loaded, a process able to access my agent may ask it to sign. A passphrase protects the file at rest; it does not make a currently unlocked agent invulnerable.

Troubleshooting

SymptomWhat I inspect
ssh-add cannot connect on WindowsGet-Service ssh-agent
ssh-add cannot connect on Linuxecho "$SSH_AUTH_SOCK"; reload .bashrc for the server setup
Private-key permissions are too openchmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519
PowerShell works but Git asks againCompare where.exe ssh with core.sshCommand
GitHub rejects the keyCompare ssh -G, ssh-add -l, and the public key registered on GitHub
I can enter the server, but the server cannot reach GitHubThose are two identity chains; the server needs its own GitHub credential
Agent is empty after a Linux rebootReload .bashrc, then run ssh-add -t 8h
Creating .bash_profile removed aliasesMake it source the existing .bashrc

Condensed workflows

Windows to GitHub

  1. Generate a new key for this Windows device
  2. Start the Windows ssh-agent service
  3. Run ssh-add $env:USERPROFILE\.ssh\id_ed25519
  4. Add the public key to GitHub and test ssh -T git@github.com
  5. Only if Git still prompts, inspect core.sshCommand

Headless Linux server to GitHub

  1. Generate a key on the server; do not copy the laptop’s private key
  2. Add the public key to GitHub, or use a repository-scoped deploy key
  3. Install agent-init.sh and load it from .bashrc
  4. Run ssh-add -t 8h ~/.ssh/id_ed25519
  5. Verify with ssh -T git@github.com and git fetch

What I will do next time

On a new machine I will generate a new key, register its public half, and revoke the old device when it is gone. Private keys do not travel, and one key does not have to carry every machine.

The rest is ordinary troubleshooting: the passphrase protects the key on disk; the agent defines an unlocked window; Windows failures may be two different SSH clients; Linux failures are often permissions or a dead agent.

I used to want SSH to become a once-and-for-all configuration. Now I prefer something I can regenerate, verify, and revoke without fear. That is a more useful kind of permanence.