This specification outlines the architecture for PR 2, building upon the configurable ssh-agent lifetime support introduced in PR 1.
The goal of PR 2 is to transform opkssh login --auto-refresh from a simple foreground sleep loop into a resilient, background-capable Token Refresh Daemon. The daemon automatically maintains active OIDC PK Tokens and keeps loaded certificates in ssh-agent synchronized without interrupting SSH connections.
- File-Free State Discovery: No custom state files (
state.json) on disk. All active sessions are re-derived directly from certificates loaded inssh-agentand client configuration (~/.opk/config.yml). - Multi-Provider Support: Concurrently manage multiple OpenID Provider sessions (e.g. Google, Azure, GitLab) across different identities.
- Live
ssh-agentKey Rotation: Utilizegolang.org/x/crypto/ssh/agent'sList()andRemove()APIs to delete expired/stale OPKSSH certs and replace them with freshly minted certificates. - Resilient Loop Handling: Handle system sleep/wake cycles, transient network drops (with exponential backoff), and graceful expiration.
- Secure Credential Handling: Keep OIDC refresh tokens in-memory (or optionally backed by OS Keyring / Secret Service).
When the daemon starts up or scans for active sessions, it re-derives all session facts directly from ssh-agent:
┌────────────────┐ 1. List Keys ┌──────────────────────┐
│ ├────────────────────────►│ │
│ │ │ ssh-agent │
│ │◄────────────────────────┤ │
│ │ 2. SSH Certs └──────────────────────┘
│ │
│ OPKSSH Daemon │ 3. Match Issuer ┌──────────────────────┐
│ ├────────────────────────►│ │
│ │ │ ~/.opk/config.yml │
│ │◄────────────────────────┤ │
│ │ 4. Provider Config └──────────────────────┘
└───────┬────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Re-hydrated Session: │
│ - Identity: alice@company.com │
│ - Provider Alias: google (https://accounts.google.com) │
│ - Token exp: 2026-08-18T18:00:00Z │
└─────────────────────────────────────────────────────────────────┘
- Scan
ssh-agent: Queriesssh-agentviaagent.List()and inspects loaded*ssh.Certificatekeys. - Inspect Extensions: Reads the embedded compact OpenPubkey PK Token (
openpubkey-pkt) fromcert.Permissions.Extensions. - Parse Claims: Extracts Issuer (
iss), Client ID (aud), Email,exp, andiat. - Map to Client Config: Matches
issagainst~/.opk/config.yml(ClientConfig.GetByIssuer(iss)) to map running keys to configured provider aliases and settings.
The daemon runs a central Session Manager that supervises individual Provider Worker goroutines per active login session:
┌──────────────────────────────────────────────┐
│ OPKSSH Refresh Daemon │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Worker: Google │ │ Worker: Azure │ │
│ │ (alice@co.com) │ │ (alice@corp.org) │ │
│ └───────┬────────┘ └────────┬─────────┘ │
└──────────┼──────────────────────┼────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ Target ssh-agent │
│ │
│ [Key 1] opkssh:google:alice@co.com │
│ [Key 2] opkssh:azure:alice@corp.org │
└──────────────────────────────────────────────┘
Keys in ssh-agent are tagged with structured comments to prevent key collisions across providers:
opkssh:<provider_alias>:<identity_email>
(e.g., opkssh:google:alice@company.com or opkssh:azure:alice@corp.org)
On each refresh cycle (1 minute prior to ID Token expiration):
- Token Refresh: Call
provider.Refresh(ctx)to exchange the refresh token for a new PK Token. - Mint SSH Certificate: Construct the updated SSH Certificate with
ssh.CertTimeInfinity. - Key Replacement in Agent:
- Query
agent.List(). - Find existing key matching
opkssh:<provider_alias>:<identity_email>or public key. - Call
agent.Remove(existingCert)to purge the stale certificate. - Call
agent.Add(newCert)to insert the fresh certificate intossh-agent.
- Query
- Disk Sync: Write updated keys/certs to
~/.ssh/id_ecdsaas today.
To robustly handle laptop sleep/wake states and transient network failures:
- Sleep/Wake Detection: Use periodic interval checks (every 30s) instead of long static timers to detect system clock jumps.
- Exponential Backoff: If OIDC refresh encounters a network error (e.g. offline/Wi-Fi reconnecting), retry at intervals (
10s,30s,1m,5m) up until token expiration. - Graceful Expiration: If network is unavailable through token expiration, remove the expired key from
ssh-agentand send a desktop notification (opkssh session for google expired).
Daemon settings are configured under a top-level daemon: section in ~/.config/opk/config.yml (or ~/.opk/config.yml):
daemon:
# Path to target ssh-agent socket or Windows named pipe.
# Defaults to $SSH_AUTH_SOCK or \\.\pipe\openssh-ssh-agent on Windows
ssh_agent_sock: "/run/user/1000/keyring/ssh"
# Interval between full ssh-agent scans and identity re-evaluations
full_scan_interval: "30s"
# Logging level: debug, info, warn, error
log_level: "info"
# Log file location. Defaults to $XDG_STATE_HOME/opk/daemon.log or ~/.opk/opkssh.log
log_file: "~/.opk/opkssh.log"
# Maximum backoff duration for failed OIDC network calls (e.g., laptop offline)
backoff_max: "5m"
# Automatically purge un-refreshed or expired opkssh certs from ssh-agent
prune_stale_keys: true
# Send OS desktop notifications on session expiration or critical errors
desktop_notifications: trueWindows has unique platform differences for ssh-agent, IPC sockets, and background processes that are accounted for in the daemon design:
| Mechanism | Linux / macOS | Windows |
|---|---|---|
ssh-agent Transport |
Unix Domain Socket ($SSH_AUTH_SOCK) |
Named Pipe (\\.\pipe\openssh-ssh-agent) or $SSH_AUTH_SOCK (via winio.DialPipe) |
| Daemon IPC Socket | Unix Socket ($XDG_RUNTIME_DIR/opkssh.sock) |
AF_UNIX socket (Windows 10 1803+) or Named Pipe (\\.\pipe\opkssh-daemon) |
| Service Persistence | systemd --user unit or launchd plist |
Windows Service (winio/svc package) or Task Scheduler (schtasks) |
| Config Location | $XDG_CONFIG_HOME/opk/config.yml |
%APPDATA%\opk\config.yml |
- Named Pipe Support: Use
golang.org/x/sys/windows/winiowhen connecting to\\.\pipe\openssh-ssh-agentif$SSH_AUTH_SOCKis formatted as a Windows Named Pipe. - IPC Path Fallback: Use
%LOCALAPPDATA%\opk\opkssh.sockor\\.\pipe\opkssh-daemonwhenXDG_RUNTIME_DIRis not set on Windows.
To support OpenID Key-Binding refresh flows (where the client must sign a random OIDC challenge to prove private key possession during refresh):
┌────────────────┐ 1. Request Signer ┌──────────────────────┐
│ ├────────────────────────►│ │
│ OPKSSH Daemon │ │ ssh-agent │
│ (Refresh Loop) │◄────────────────────────┤ │
└───────┬────────┘ 2. Agent Signer └──────────────────────┘
│ (No Private Key Exposure)
│ 3. Sign Challenge
▼
┌────────────────┐
│ OpenID │
│ Provider (OP) │
└────────────────┘
ssh-agentBacked Signer:golang.org/x/crypto/ssh/agentprovidesagent.NewClient(conn).Signer(pubkey, alg), which implements Go'scrypto.Signerinterface.- Key Isolation: The refresh daemon passes this
crypto.Signerto OpenPubkey's client (opkClient). When the OpenID Provider requests a challenge signature during refresh,opkClientdelegates the signing operation tossh-agent. - Security Advantage: Private keys stay isolated inside
ssh-agent(or YubiKey / hardware tokens). The refresh daemon never needs to hold or read raw private keys off disk or in process memory.
- Daemon Mode:
opkssh login --auto-refresh --daemon(or-d) detaches into the background. - Status Query:
opkssh statusqueries running daemon worker state over the local Unix domain socket / named pipe:$ opkssh status OPKSSH Daemon (PID 12345) - Socket: /run/user/1000/opkssh.sock Active sessions (2): [google] alice@company.com - Refreshed 12m ago (Next refresh in 48m) [azure] alice@corp.org - Refreshed 5m ago (Next refresh in 55m)
- Logout Purge:
opkssh logoutremoves local key files AND purges associated OPKSSH certs fromssh-agent(agent.Remove()). - Service Installation:
opkssh service installgenerates a systemd user unit (~/.config/systemd/user/opkssh.service), launchd plist, or Windows Scheduled Task for persistent startup across logins.