| |

LFCS 114 🐧 SSH Agent and Config File

⏱️ Estimated read: 11 min | |

The previous chapter deployed the public key to the server’s authorized_keys. This chapter covers two client-side mechanisms that make key-based authentication practical at scale: the SSH agent, which caches decrypted private keys in memory so the passphrase is typed once per session, and the SSH config file, which stores per-host connection settings so the command line is not a series of flags. The LFCS exam tests both. The agent is the answer to the question “how do I avoid typing my passphrase every time?” The config file is the answer to “how do I avoid typing the hostname, port, user, and key path every time?”

Key point: The SSH agent holds decrypted keys in memory. ssh-add loads a key into the agent. The config file at ~/.ssh/config defines per-host settings and is read before the command line. A host alias defined in the config can be used as ssh <alias>, and the corresponding User, HostName, Port, and IdentityFile are applied automatically.


Why the agent and config file exist

The passphrase problem. A private key protected by a passphrase is useless to an attacker who steals the key file. But typing the passphrase for every ssh, scp, git fetch, and rsync invocation defeats the purpose. The SSH agent solves this: the key is decrypted once, loaded into the agent’s memory, and the agent handles the authentication. The private key on disk remains encrypted. The agent provides the decrypted key to the SSH client on demand .

The multi-key problem. A developer may have a personal key, a work key, and a key for a specific bastion host. The server rejects the wrong key and accepts the right one, but the client offers keys in order. The agent can hold multiple keys, and the config file can specify which key to use for which host. The IdentityFile directive in the config file tells the client which key to offer first .

The long command problem. Without the config file, a connection to a non-standard port on a jump host with a specific key requires a long command line: ssh -p 2222 -i ~/.ssh/work_key -J bastion.example.com user@internal.example.com. The same connection in the config file is a few lines, and the command becomes ssh internal. The config file is read before the command line, so its settings apply unless overridden by flags .

The agent forwarding problem. A bastion host is the only entry point to an internal network. A developer logs into the bastion and from there connects to internal servers. Without agent forwarding, the developer must copy the private key to the bastion—a security violation. With agent forwarding, the developer’s local agent is forwarded to the bastion, and the bastion can authenticate to internal servers using the developer’s keys without ever storing them. The ForwardAgent yes directive in the config enables this for a specific host .

The trade-off. The agent holds decrypted keys in memory. A process with sufficient privileges on the client machine can extract them. The config file is a plaintext file; it contains hostnames, usernames, and key paths, but not private keys. Neither mechanism is a security risk on a properly configured workstation. Both are essential for a usable SSH workflow.


a. The SSH agent and ssh-add

The ssh-agent is a process that holds decrypted keys. It is usually started automatically by the desktop session or the shell profile. The SSH_AUTH_SOCK environment variable points to the agent’s socket; the SSH client uses this variable to find the agent.

# Start the agent (if not already running)
eval "$(ssh-agent -s)"

# The output sets SSH_AUTH_SOCK and SSH_AGENT_PID.

# Add the default key (prompts for passphrase once)
ssh-add

# Add a specific key
ssh-add ~/.ssh/id_ed25519_work

# List loaded keys
ssh-add -l

# Remove a key
ssh-add -d ~/.ssh/id_ed25519_work

# Remove all keys
ssh-add -D

The ssh-add command without arguments loads the default keys (~/.ssh/id_rsa, ~/.ssh/id_ed25519, etc.). The -l flag lists the fingerprints of the loaded keys. The -d flag removes a specific key. The -D flag removes all keys .

The ssh-add -t <seconds> flag sets a lifetime for the loaded key. After the lifetime expires, the key is removed from the agent. This is a security measure for keys that should not remain decrypted indefinitely. The -t 3600 sets a one-hour lifetime .

On macOS, the keychain can store the passphrase, so the agent is repopulated automatically after a reboot. On Linux, the GNOME Keyring or KDE Wallet can serve the same function. The exam does not test the desktop integration, but it tests the ssh-add and ssh-add -l commands.


b. The SSH config file: ~/.ssh/config

The ~/.ssh/config file defines per-host settings. The file is a sequence of Host blocks. Each block starts with a Host line that names one or more patterns, followed by directives that apply when the pattern matches.

# ~/.ssh/config
Host work
    HostName git.work.example.com
    User alice
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_work

Host bastion
    HostName bastion.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519_work
    ForwardAgent yes

Host internal
    HostName 10.0.0.50
    User app
    ProxyJump bastion

The Host work block defines an alias. The command ssh work connects to git.work.example.com on port 2222 as user alice, using the key ~/.ssh/id_ed25519_work. The HostName directive is the actual address. The Host name is the alias that appears in the command .

The ProxyJump directive is the mechanism for connecting through a bastion. The ssh internal command connects to 10.0.0.50 by first establishing a connection to bastion, then tunneling through it. This is the modern replacement for the older ProxyCommand with nc .

The Host * block matches every host and is used for global defaults. It must appear at the end of the file, because the first matching value for each directive wins.

# Global defaults
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes

ServerAliveInterval 60 sends a keepalive packet every 60 seconds. ServerAliveCountMax 3 disconnects after three unanswered keepalives. AddKeysToAgent yes automatically loads the key into the agent when it is used. IdentitiesOnly yes tells the client to offer only the keys specified in the config, not every key in the agent. This is the fix for the “too many authentication failures” error, which occurs when the server rejects the client after too many offered keys .


c. Command-line overrides and precedence

The config file is read before the command line. A flag passed to ssh overrides the config file. The precedence order is: command-line flags, then the user’s config file, then the system-wide config file (/etc/ssh/ssh_config). Within the config file, the first matching value for each directive wins.

# Override the port for one connection
ssh -p 2200 work

# Override the user
ssh -l bob work

# Override the key
ssh -i ~/.ssh/id_rsa_backup work

The ssh -G <host> command dumps the effective configuration for a host, including the values from the config file and the defaults. This is the diagnostic tool when the config file does not seem to be applying.

# Show effective config for host "work"
ssh -G work

The output includes every directive that applies, not just the ones in the config file. The grep filter narrows it to the directives that matter.

ssh -G work | grep -E 'hostname|user|port|identityfile'

Complete Example Session

# ============================================
# PART 1: START THE AGENT
# ============================================

eval "$(ssh-agent -s)"
# Agent pid 12345
# ============================================
# PART 2: LOAD A KEY
# ============================================

ssh-add ~/.ssh/id_ed25519_work
# Enter passphrase for /home/user/.ssh/id_ed25519_work: (type passphrase)
# Identity added: /home/user/.ssh/id_ed25519_work
# ============================================
# PART 3: LIST LOADED KEYS
# ============================================

ssh-add -l
# 256 SHA256:abcdef... user@example.com (ED25519)
# ============================================
# PART 4: CREATE THE CONFIG FILE
# ============================================

mkdir -p ~/.ssh
cat > ~/.ssh/config <<'EOF'
Host work
    HostName git.work.example.com
    User alice
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_work

Host bastion
    HostName bastion.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519_work
    ForwardAgent yes

Host internal
    HostName 10.0.0.50
    User app
    ProxyJump bastion

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    IdentitiesOnly yes
EOF

chmod 600 ~/.ssh/config
# ============================================
# PART 5: TEST THE ALIAS
# ============================================

ssh work
# Connects to git.work.example.com on port 2222 as alice.
# ============================================
# PART 6: TEST THE BASTION ALIAS
# ============================================

ssh bastion
# Connects to bastion.example.com as admin.
# ============================================
# PART 7: TEST THE PROXY JUMP
# ============================================

ssh internal
# Connects to 10.0.0.50 through bastion.
# ============================================
# PART 8: SHOW EFFECTIVE CONFIG
# ============================================

ssh -G internal | grep -E 'hostname|user|proxyjump'
# hostname 10.0.0.50
# user app
# proxyjump bastion
# ============================================
# PART 9: REMOVE A KEY
# ============================================

ssh-add -d ~/.ssh/id_ed25519_work
# Identity removed

The nine parts trace the agent and config workflow: agent start, key load, key list, config creation, alias test, bastion test, proxy jump test, effective config, and key removal.


Quick Reference

Agent Commands

CommandPurpose
eval "$(ssh-agent -s)"Start the agent
ssh-addLoad default keys
ssh-add <key>Load specific key
ssh-add -lList loaded keys
ssh-add -d <key>Remove specific key
ssh-add -DRemove all keys
ssh-add -t 3600 <key>Load with one-hour lifetime

Config Directives

DirectivePurpose
Host <alias>Define a host block
HostName <address>Actual address
User <name>Username
Port <port>Port number
IdentityFile <path>Private key path
ProxyJump <host>Jump through a bastion
ForwardAgent yesForward the agent to the host
IdentitiesOnly yesOffer only the specified keys
ServerAliveInterval <s>Keepalive interval
ServerAliveCountMax <n>Keepalive retry count

Precedence

PrioritySource
1 (highest)Command-line flags
2User config (~/.ssh/config)
3System config (/etc/ssh/ssh_config)

Best Practices

✅ Do This:

ssh-add ~/.ssh/id_ed25519_work
<!-- Load keys into the agent -->                     # ✅
Host work
    HostName git.work.example.com
    User alice
    IdentityFile ~/.ssh/id_ed25519_work
<!-- Use aliases for frequently used hosts -->        # ✅
Host *
    IdentitiesOnly yes
<!-- Prevent too many authentication failures -->     # ✅

❌ Don’t Do This:

# Type the passphrase for every command
<!-- Load the key into the agent instead -->          # ❌
# Forward the agent to every host
<!-- Only forward to bastion hosts -->                # ❌
# Leave the config file at 644
<!-- The config file must be 600 -->                  # ❌

Common Pitfalls

PitfallWhy It HappensFix
Passphrase prompts every timeKey not loaded in agentssh-add ~/.ssh/id_ed25519
Too many authentication failuresToo many keys offeredIdentitiesOnly yes in config
Config not appliedFile permissions wrongchmod 600 ~/.ssh/config
ProxyJump failsBastion unreachableVerify bastion connection first
Agent lost after rebootSession-boundStart agent in shell profile or use keychain

Real-World Examples

1. Start Agent

eval "$(ssh-agent -s)"

2. Load Key

ssh-add ~/.ssh/id_ed25519

3. List Keys

ssh-add -l

4. Remove Key

ssh-add -d ~/.ssh/id_ed25519

5. Create Config

Host work
    HostName git.work.example.com
    User alice
    IdentityFile ~/.ssh/id_ed25519_work

6. Bastion with ProxyJump

Host internal
    HostName 10.0.0.50
    ProxyJump bastion

7. Forward Agent

Host bastion
    ForwardAgent yes

8. Global Defaults

Host *
    IdentitiesOnly yes

9. Override Port

ssh -p 2200 work

10. Show Effective Config

ssh -G work | grep -E 'hostname|user|port'

Visual

Agent Workflow

┌──────────────────────────────────────────────┐
│  Private key (encrypted on disk)             │
│  ~/.ssh/id_ed25519                           │
└──────────────────┬───────────────────────────┘
                   │
                   │ ssh-add
                   ▼
┌──────────────────────────────────────────────┐
│  Agent memory                                │
│  Decrypted key                               │
│  Serves authentication without passphrase    │
└──────────────────┬───────────────────────────┘
                   │
                   │ ssh work
                   ▼
┌──────────────────────────────────────────────┐
│  SSH client uses agent's socket              │
│  Signs authentication challenge              │
└──────────────────────────────────────────────┘

Config File Structure

┌──────────────────────────────────────────────┐
│  ~/.ssh/config                               │
│                                              │
│  Host work                                   │
│    HostName git.work.example.com             │
│    User alice                                │
│    Port 2222                                 │
│    IdentityFile ~/.ssh/id_ed25519_work       │
│                                              │
│  Host internal                               │
│    HostName 10.0.0.50                        │
│    ProxyJump bastion                         │
│                                              │
│  Host *                                      │
│    IdentitiesOnly yes                        │
└──────────────────────────────────────────────┘

Summary

ItemValue
Agent starteval "$(ssh-agent -s)"
Load keyssh-add <key>
List keysssh-add -l
Remove keyssh-add -d <key>
Config file~/.ssh/config
Config permission600
Bastion jumpProxyJump <host>
Key selectionIdentityFile <path>
Too many failures fixIdentitiesOnly yes

Key takeaways:

  • The agent caches decrypted keys. The passphrase is typed once per session. The private key on disk remains encrypted. The agent provides the decrypted key to the SSH client on demand .
  • ssh-add loads keys. The -l flag lists them, the -d flag removes a specific key, and the -D flag removes all of them .
  • The config file defines per-host settings. A Host block with HostName, User, Port, and IdentityFile turns a long command into a short alias .
  • ProxyJump is the bastion mechanism. It connects through an intermediate host without copying private keys to the bastion .
  • IdentitiesOnly yes prevents authentication failures. Without it, the client offers every key in the agent, and the server may reject the connection after too many attempts .

Remember: The agent is the memory of the SSH client. The config file is the shortcut. The passphrase is typed once, the key is loaded, and the agent handles the rest. The host alias is defined once, and the config file applies the settings for every command. The two mechanisms complement each other: the agent handles the credential, and the config file handles the destination.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!