Skip to content

Inbound SSH to infra-services: humans (1Password) and agents (Cursor)

Use this runbook when you want both:

  1. You — interactive terminal, 1Password as SSH identity, normal ergonomics.
  2. Cursor / automation — non-interactive tools that must ssh infra-services without prompts, without 1Password touch approvals, and without you typing a password.

Those are two different trust paths. Keep them separate keys and separate Host aliases so OpenSSH never offers the wrong identity first.

Related: outbound Git identities on the VM (ansible-pull, git push) are documented in ssh-keys-and-infra-services.md. This document is inbound only (your workstation → someone@192.168.6.17).


Target end state

Who Command Key material Agent
You ssh infra-services 1Password-managed key (.pub path in SSH config) 1Password / OpenSSH agent (IdentityAgent in your global config)
Cursor / scripts ssh infra-services-cursor File-backed key pair on disk (e.g. ~/.ssh/cursor-infra) None (IdentityAgent none on that host stanza)

Host: infra-services is 192.168.6.17, user someone (see inventory / workspace rules).


Prerequisites on your workstation

  1. OpenSSH client available in the same environment Cursor uses (Windows OpenSSH is fine).
  2. ~/.ssh/config (Windows: C:\Users\<you>\.ssh\config) includes:
  3. A Host infra-services block: HostName, User someone, 1Password IdentityFile (often a ~/.ssh/1Password/*.pub path), IdentitiesOnly yes.
  4. A Host infra-services-cursor block: same HostName / User, IdentityFile pointing at a normal private key file on disk, IdentityAgent none, IdentitiesOnly yes.
  5. Global Host * behavior you already use: IdentitiesOnly yes plus Include ~/.ssh/1Password/config and IdentityAgent pointing at the Windows OpenSSH agent pipe is OK — the infra-services-cursor stanza overrides the agent for that alias only.

Adjust paths to match your machine; the names infra-services / infra-services-cursor are the important part for muscle memory and for agents.


Path A — Your normal way (terminal + 1Password)

Goal

ssh infra-services opens a shell using the key 1Password injects via the agent / IdentityFile wiring in ~/.ssh/1Password/config.

Steps

  1. Confirm 1Password is running and SSH agent integration is enabled for this vault/item (per 1Password docs for your platform).
  2. Confirm Match Host 192.168.6.17 User someone (or equivalent) exists in ~/.ssh/1Password/config and matches the IdentityFile referenced by Host infra-services.
  3. Test from a normal terminal (outside Cursor if you are debugging):
ssh -o BatchMode=yes infra-services 'echo ok'
  • If this fails, fix 1Password/agent first — do not move on to Path B until it passes.

Daily use

ssh infra-services
scp something infra-services:

Path B — Cursor / automation (no human in the loop)

Goal

From Cursor’s non-interactive shell (or CI later), this must succeed with no password and no 1Password UI:

ssh -o BatchMode=yes -o ConnectTimeout=10 infra-services-cursor 'echo ok'

Steps

  1. Key pair dedicated to automation (example name cursor-infra):
  2. Private key on disk: %USERPROFILE%\.ssh\cursor-infra (no extension).
  3. Public key: cursor-infra.pub alongside it.
  4. Generate if missing:

    ssh-keygen -t ed25519 -f ~/.ssh/cursor-infra -C "cursor-infra-infra-services"
    
  5. SSH configHost infra-services-cursor must include at minimum:

Host infra-services-cursor
    HostName 192.168.6.17
    User someone
    IdentityFile ~/.ssh/cursor-infra
    IdentitiesOnly yes
    IdentityAgent none
  1. Authorize the public key on the server for someone (see next section).

  2. Verify from the same environment Cursor uses (important: paths differ between WSL vs Windows OpenSSH):

ssh -o BatchMode=yes -o ConnectTimeout=10 infra-services-cursor 'hostname'

Why not use 1Password for Path B?

Cursor’s integrated terminal often cannot complete 1Password prompts or see the same agent state as your interactive login. A file key plus IdentityAgent none on infra-services-cursor removes that dependency.


Installing the automation key on infra-services

The public half must be in /home/someone/.ssh/authorized_keys on infra-services.

Ansible note: the common role manages authorized_keys from GitHub with exclusive: false by default (common_ssh_authorized_keys_exclusive: false), so adding another line for cursor-infra will not wipe GitHub keys — but do not set exclusive: true unless you intend to replace keys wholesale.

Pick one approach:

Option 1 — One-time append (fastest)

While logged in the human way (ssh infra-services):

mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo 'PASTE_CONTENT_OF_cursor-infra.pub_ONE_LINE' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Option 2 — Pipe from your workstation

From a machine that already has cursor-infra.pub:

type %USERPROFILE%\.ssh\cursor-infra.pub | ssh infra-services "mkdir -p .ssh && chmod 700 .ssh && cat >> .ssh/authorized_keys && chmod 600 .ssh/authorized_keys"

(PowerShell: Get-Content ... | ssh ... equivalent.)

Option 3 — Publish the pubkey via GitHub

If the automation pubkey is attached to the GitHub user listed in common_github_ssh_users, the next successful ansible-pull / common role run will deploy it like your other operator keys. This is optional and has broader blast radius than a dedicated per-host key.


Verification matrix (when you return)

Run these in order; all should succeed before you declare “done”.

# Where Command Expect
1 Your terminal ssh -o BatchMode=yes infra-services 'echo human-ok' human-ok
2 Cursor terminal ssh -o BatchMode=yes infra-services-cursor 'echo agent-ok' agent-ok
3 Cursor terminal ssh -o ConnectTimeout=10 infra-services-cursor 'hostname' infra-services (or VM hostname)

If 1 fails: 1Password / agent / Match Host — not a server problem.

If 2 fails but 1 works: missing cursor-infra private key in the path Cursor uses, wrong IdentityFile, or pubkey not in authorized_keys.


Troubleshooting

Permission denied (publickey) only from Cursor

  • Confirm Test-Path $env:USERPROFILE\.ssh\cursor-infra (private) is true in PowerShell inside Cursor, not only in WSL or another profile.
  • Confirm IdentityAgent none is set on infra-services-cursor so OpenSSH does not skip your file key while chasing an empty agent.

Hangs instead of failing

  • Remove interactive prompts: use BatchMode=yes in tests.
  • Add -o ConnectTimeout=10 so stuck TCP does not block forever.

Too many authentication failures

  • Keep IdentitiesOnly yes on both stanzas.
  • Do not use bare ssh someone@192.168.6.17 for automation — use infra-services-cursor so only the intended key is offered.

Policy: what agents should use

For any automated command in this repo’s context (Cursor agent, scripts), use:

ssh -o BatchMode=yes -o ConnectTimeout=10 infra-services-cursor "<remote command>"

Reserve ssh infra-services for you.


Optional: document for Cursor rules

If you want the agent to default correctly in this workspace, add a short rule: “For SSH to infra-services from tools, use Host infra-services-cursor, not the raw IP.” (Optional — this runbook is the source of truth.)