← Writing

Why Your 1Password SSH Keys Are Being Ignored: Stop Key Spraying, Start Pinning


I spent longer than I’d like to admit debugging this. Everything looked right. Keys were in the vault. The agent was running. ssh myserver returned Permission denied (publickey). Classic.

Added -v. The agent was returning 12 keys. SSH was collecting them and then just… not using any of them.

debug1: get_agent_identities: agent returned 12 keys
debug1: Will attempt key: /Users/you/.ssh/id_rsa
debug1: Will attempt key: /Users/you/.ssh/id_ecdsa
debug1: Trying private key: /Users/you/.ssh/id_rsa
debug1: Trying private key: /Users/you/.ssh/id_ecdsa
debug1: No more authentication methods to try.
Permission denied (publickey).

Fetched 12 keys, offered none to the server. Weird yet interesting.

This article explains exactly why SSH ignores your vault keys, why the naive fix leads to a second failure, and the correct pattern: per-host key pinning, where SSH offers exactly one key per server with the private key never leaving 1Password.


The Real Problem: SSH Is Ignoring Your Agent Keys Entirely

The standard advice when setting up 1Password SSH agent is to add IdentitiesOnly yes:

Host myserver
    HostName 1.2.3.4
    User root
    IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    IdentitiesOnly yes

Makes sense on paper — restrict SSH to only use keys you’ve explicitly configured. But read what the man page actually says:

Specifies that ssh should only use the configured authentication identity and certificate files (either the default files, or those explicitly configured in the ssh_config files or passed on the command-line), even if ssh-agent or a PKCS11Provider or SecurityKeyProvider offers more identities.

Configured identity files. That means IdentityFile directives — or, absent those, the default paths on disk: ~/.ssh/id_rsa, ~/.ssh/id_ecdsa, etc.

If your private keys live exclusively in 1Password — as they should — none of those files exist on disk. So IdentitiesOnly yes with no IdentityFile directive tells SSH: offer nothing. The agent is called, dutifully returns all 12 keys, and SSH discards every single one because there is no IdentityFile to match against.

Twelve keys fetched. Zero offered. Authentication fails. The verbose output makes it look like the agent is working fine. It is. It’s SSH that’s throwing the keys away.


Removing IdentitiesOnly Just Moves the Problem

The obvious fix is to remove IdentitiesOnly yes. Now SSH doesn’t filter anything, so it offers all 12 agent keys to the server. Except:

debug1: Offering public key: prod-db-1 ED25519 SHA256:mK7... agent
debug1: Authentications that can continue: publickey
debug1: Offering public key: dev-laptop ED25519 SHA256:pQ2... agent
debug1: Authentications that can continue: publickey
debug1: Offering public key: staging-server ED25519 SHA256:rT9... agent
Received disconnect from 1.2.3.4 port 22:2: Too many authentication failures

My correct key was 11th in the list. The server’s MaxAuthTries was 6. Disconnected before SSH even got to it.

This is key spraying — SSH blindly throwing every key in your vault at every server in vault order. A realistic vault has 10–15 keys across production, staging, cloud providers, personal machines. OpenSSH’s default MaxAuthTries is 6. You can see where this goes.

Key spraying has two compounding problems beyond just failing:

  1. Non-deterministic. Whether it succeeds depends entirely on which position your correct key happens to be in the agent’s return order — which can change as you add or reorganise keys in your vault.

  2. Audit noise. Every rejected key offer is a logged failed authentication attempt on the target server. You are generating false-positive noise in your own security logs — and if you connect to servers that don’t know you at all, in someone else’s.

Removing IdentitiesOnly yes traded one failure mode for another.


The Actual Fix: Per-Host Key Pinning

The root cause of both failures is the same: SSH doesn’t know which key to use for which server. In the first case it guesses wrong (nothing). In the second it tries everything. The fix is to tell it explicitly.

OpenSSH has a lesser-known feature that makes this work cleanly with 1Password: IdentityFile accepts a .pub file. When it points to a public key, SSH doesn’t load a private key from disk — it uses the public key’s fingerprint to locate the matching private key in the agent. The agent handles the signing. The private key never leaves 1Password.

Step 1: Export the correct public key from your 1Password agent.

# List all keys with their full public key material
SSH_AUTH_SOCK="~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" \
  ssh-add -L

# Save the one for this server
echo "ssh-ed25519 AAAA...your-key... myserver" > ~/.ssh/myserver.pub
chmod 644 ~/.ssh/myserver.pub

Step 2: Pin it in ~/.ssh/config.

Host myserver
    HostName 1.2.3.4
    User root
    IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    IdentityFile ~/.ssh/myserver.pub
    IdentitiesOnly yes

Now the flow is deterministic:

SSH reads ~/.ssh/myserver.pub
  → fingerprint matches the correct key in 1Password agent
  → 1Password signs the server's challenge
  → server verifies signature against authorized_keys
  → authenticated ✓

One key offered. One round-trip. No private key on disk. No spraying. No MaxAuthTries problem regardless of how large your vault grows.


Why This Is Safe

The .pub file on disk is just a lookup hint. It’s a public key — it’s already public by definition. There’s nothing sensitive in it.

The private key stays in 1Password and performs the signing operation inside 1Password’s own process. It never passes through SSH process memory. It never touches disk. The only thing that crosses between 1Password and SSH is a signature.

Think of it the same way you’d think about a YubiKey — the private key lives inside the hardware, the challenge goes in, a signature comes out. 1Password is just a software version of that.

The actual threat surface here is your 1Password vault credentials — not the .pub file sitting in ~/.ssh. Protect the vault with a strong master password and a hardware 2FA key and you’re in good shape.


Scale This Across Your Infrastructure

Once you have the pattern down, roll it out consistently:

# Export once per key
ssh-add -L | grep "production" > ~/.ssh/production.pub
ssh-add -L | grep "staging"    > ~/.ssh/staging.pub
ssh-add -L | grep "db-servers" > ~/.ssh/db.pub
Host prod-*
    IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    IdentityFile ~/.ssh/production.pub
    IdentitiesOnly yes

Host staging-*
    IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    IdentityFile ~/.ssh/staging.pub
    IdentitiesOnly yes

Each server group gets exactly one key offered. No ambiguity, no vault-ordering dependency, no MaxAuthTries roulette.

Enforce this as a team standard: every Host block in ~/.ssh/config must have an explicit IdentityFile. Make it a code review checklist item. With an external agent holding many keys, it’s the difference between a deterministic, auditable auth flow and one that depends on how your teammates happen to order their vault.


Debugging Checklist

If SSH auth breaks after following this setup, work through these in order:

# 1. Is the 1Password agent socket running?
ls "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

# 2. Is 1Password unlocked and SSH agent enabled?
#    Settings → Developer → Use the SSH agent
SSH_AUTH_SOCK="~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" \
  ssh-add -l

# 3. Confirm which key SSH is actually offering
ssh -v myserver 2>&1 | grep -E "Offering|denied|disconnect|attempt"

# 4. Verify the public key is in authorized_keys on the server
#    (use your provider's web console if SSH is broken)
grep "myserver" ~/.ssh/authorized_keys

Common failure modes:

  • 1Password locked → agent returns 0 keys → immediate auth failure
  • SSH agent not enabled → Settings → Developer → “Use the SSH agent”
  • Wrong key in authorized_keys → key was rotated in vault but server wasn’t updated
  • Server rebuilt → cloud-init wiped authorized_keys on redeploy (common with VPS providers)

Summary

ConfigWhat SSH Does
IdentityAgent onlySprays all agent keys — fails on MaxAuthTries
IdentityAgent + IdentitiesOnly yes, no IdentityFileIgnores all agent keys — offers nothing
IdentityAgent + IdentityFile key.pub + IdentitiesOnly yesOffers exactly one key — correct

The invariant: every Host block should have an explicit IdentityFile. With 1Password as your agent, a .pub file on disk is all it takes — no private key material, no security trade-off, completely deterministic behaviour.

Private keys belong in 1Password. Connection behaviour belongs in your config.