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 outwardauthorized_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 groupConnection refused: the host is reachable, but nothing listens on that portPermission denied (publickey): user, private key, orauthorized_keysmismatchToo 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
| Symptom | What I inspect |
|---|---|
ssh-add cannot connect on Windows | Get-Service ssh-agent |
ssh-add cannot connect on Linux | echo "$SSH_AUTH_SOCK"; reload .bashrc for the server setup |
| Private-key permissions are too open | chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519 |
| PowerShell works but Git asks again | Compare where.exe ssh with core.sshCommand |
| GitHub rejects the key | Compare ssh -G, ssh-add -l, and the public key registered on GitHub |
| I can enter the server, but the server cannot reach GitHub | Those are two identity chains; the server needs its own GitHub credential |
| Agent is empty after a Linux reboot | Reload .bashrc, then run ssh-add -t 8h |
Creating .bash_profile removed aliases | Make it source the existing .bashrc |
Condensed workflows
Windows to GitHub
- Generate a new key for this Windows device
- Start the Windows
ssh-agentservice - Run
ssh-add $env:USERPROFILE\.ssh\id_ed25519 - Add the public key to GitHub and test
ssh -T git@github.com - Only if Git still prompts, inspect
core.sshCommand
Headless Linux server to GitHub
- Generate a key on the server; do not copy the laptop’s private key
- Add the public key to GitHub, or use a repository-scoped deploy key
- Install
agent-init.shand load it from.bashrc - Run
ssh-add -t 8h ~/.ssh/id_ed25519 - Verify with
ssh -T git@github.comandgit 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.
