Crystalline blue padlock split in half with glowing encrypted tunnel and golden token card on a grid background

SSH and the Boundaries of macOS Security

software development

March 2026.
Srđan Marković's profile image

Srđan Marković

CTO

Addressing the SSH complexity in macOS secret management - biometric blocking, keychain session isolation, and the 1Password Service Account solution for headless remote sessions.

Parts 1 and 2 work flawlessly for local GUI terminal sessions. The Keychain unlocks automatically on login, 1Password authenticates via Touch ID, and secrets load invisibly at shell startup. But the moment you SSH into your workstation remotely, the architecture hits the deliberate security boundaries of macOS.

This is an increasingly common scenario. Developers run AI coding agents on powerful desktop machines and manage them remotely from laptops, tablets, or mobile SSH clients like Termius. The local setup from Parts 1 and 2 breaks in three distinct ways over SSH:

  1. Biometric blocking. 1Password Touch ID opens a dialog on the machine’s physical display. Over SSH, there is no one to tap it. The shell blocks indefinitely.
  2. Keychain session isolation. macOS Keychain unlock state is per-login-session. Unlocking the keychain from a local GUI terminal does not carry over to SSH sessions. There is no PAM module, no sshd_config option, and no launchd mechanism to bridge this gap.
  3. Silent failures. Over SSH, security find-generic-password fails silently unless the keychain is explicitly unlocked in that session.

These are not bugs. They are intentional security constraints designed to prevent remote exfiltration. But they break automated shell startup.

The Solution: 1Password Service Accounts#

The approach that worked for us was 1Password Service Accounts. These provide token-based authentication with read-only access to a specific vault, requiring no biometric input and no interactive password.

The setup is a one-time operation from a local GUI terminal:

# One-time setup per machine (from local GUI terminal):
# 1. Sign in to 1Password.com > "Developer Tools" > "Service Accounts".
# 2. Create a service account (e.g., "CLI SSH Access").
# 3. Grant it "Read" access to your "CLI" vault.
# 4. Copy the generated token, then run:
mkdir -p ~/.config/op
read -s "token?Enter 1Password Service Account Token: "
echo
echo "$token" > ~/.config/op/.sa-token
chmod 400 ~/.config/op/.sa-token
unset token

The token file is chmod 400, readable only by the owning user. The read -s prompt ensures the token never appears in shell history.

The Hybrid Loading Logic#

The final .zshrc detects the session type via $SSH_CONNECTION and routes to the appropriate authentication path. This uses _keychain_get and the _SECRETS array defined in Part 1, and the op inject pattern from Part 2:

# ~/.zshrc - session-aware secret loading

if [[ -n "$SSH_CONNECTION" ]]; then
  if [[ -r ~/.config/op/.sa-token ]]; then
    export OP_SERVICE_ACCOUNT_TOKEN="$(cat ~/.config/op/.sa-token)"
    _tpl=""
    for _key in "${_SECRETS[@]}"; do
      _tpl+="export ${_key}=\"op://${OP_VAULT}/${_key}/password\""$'\n'
    done
    eval "$(echo "$_tpl" | op inject)" 2>/dev/null || _SECRETS=()
  else
    # Fallback: unlock login Keychain (prompts for password once)
    if security unlock-keychain ~/Library/Keychains/login.keychain-db 2>/dev/null; then
      for _key in "${_SECRETS[@]}"; do
        export "$_key"="$(_keychain_get "$_key")"
      done
    fi
    _SECRETS=()
  fi
elif [[ "$SECRET_BACKEND" == "op" ]]; then
  # Local + 1Password
  _tpl=""
  for _key in "${_SECRETS[@]}"; do
    _tpl+="export ${_key}=\"op://${OP_VAULT}/${_key}/password\""$'\n'
  done
  eval "$(echo "$_tpl" | op inject)"
else
  # Local + Keychain
  for _key in "${_SECRETS[@]}"; do
    export "$_key"="$(_keychain_get "$_key")"
  done
fi
Full secret management architecture with SSH detection

Figure 1: The complete loading logic. SSH sessions use a service account token; local sessions route through the configured backend.

The Complete Matrix#

The resulting behavior covers every combination:

SessionBackendAuthenticationStartup Time
LocalKeychainAuto-unlocked on GUI login~0.9s
Local1PasswordTouch ID per session~1.5s
SSHService AccountToken file, no prompts~1.5s
SSHKeychain (fallback)macOS password once per login~0.9s

The SSH + Service Account path is the cleanest: the token file authenticates silently, op inject resolves all keys in a single call, and the shell starts without any interactive prompt.

The SSH + Keychain fallback exists for engineers who have not set up 1Password. It prompts for the macOS login password once via security unlock-keychain, then loads normally. Not as smooth, but functional.

The End of Plaintext#

Across these three articles, the architectural shift amounts to under 100 lines of shell logic. It maintains exactly the same developer experience. printenv works, scripts execute flawlessly, and AI agents have the environment variables they need in runtime memory. But the structural vulnerability is gone. Your credentials live in a secure store, invisible to the code assistants operating in your terminal.

That is worth 100 lines of shell.

Share