Skip to content

Instantly share code, notes, and snippets.

@sini
Created August 18, 2026 18:40
Show Gist options
  • Select an option

  • Save sini/b645242d94b5174e306e4ab54674bf63 to your computer and use it in GitHub Desktop.

Select an option

Save sini/b645242d94b5174e306e4ab54674bf63 to your computer and use it in GitHub Desktop.
OPKSSH Refresh Daemon & ssh-agent Sync Specification (PR 2)

Technical Specification: OPKSSH Token Refresh Daemon & ssh-agent Sync

Overview

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.


Design Principles & Goals

  1. File-Free State Discovery: No custom state files (state.json) on disk. All active sessions are re-derived directly from certificates loaded in ssh-agent and client configuration (~/.opk/config.yml).
  2. Multi-Provider Support: Concurrently manage multiple OpenID Provider sessions (e.g. Google, Azure, GitLab) across different identities.
  3. Live ssh-agent Key Rotation: Utilize golang.org/x/crypto/ssh/agent's List() and Remove() APIs to delete expired/stale OPKSSH certs and replace them with freshly minted certificates.
  4. Resilient Loop Handling: Handle system sleep/wake cycles, transient network drops (with exponential backoff), and graceful expiration.
  5. Secure Credential Handling: Keep OIDC refresh tokens in-memory (or optionally backed by OS Keyring / Secret Service).

Core Architecture

1. State Discovery & File-Free Re-hydration

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                               │
└─────────────────────────────────────────────────────────────────┘
  1. Scan ssh-agent: Queries ssh-agent via agent.List() and inspects loaded *ssh.Certificate keys.
  2. Inspect Extensions: Reads the embedded compact OpenPubkey PK Token (openpubkey-pkt) from cert.Permissions.Extensions.
  3. Parse Claims: Extracts Issuer (iss), Client ID (aud), Email, exp, and iat.
  4. Map to Client Config: Matches iss against ~/.opk/config.yml (ClientConfig.GetByIssuer(iss)) to map running keys to configured provider aliases and settings.

2. Multi-Provider Session Management

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         │
└──────────────────────────────────────────────┘

Structured Agent Key Comments

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)


3. Live ssh-agent Key Rotation Lifecycle

On each refresh cycle (1 minute prior to ID Token expiration):

  1. Token Refresh: Call provider.Refresh(ctx) to exchange the refresh token for a new PK Token.
  2. Mint SSH Certificate: Construct the updated SSH Certificate with ssh.CertTimeInfinity.
  3. 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 into ssh-agent.
  4. Disk Sync: Write updated keys/certs to ~/.ssh/id_ecdsa as today.

4. Resilient Reconnect & Network Retry Loop

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-agent and send a desktop notification (opkssh session for google expired).

5. Daemon Configuration Options (config.yml)

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: true

6. Windows Support Considerations

Windows 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

Key Windows Implementation Details:

  1. Named Pipe Support: Use golang.org/x/sys/windows/winio when connecting to \\.\pipe\openssh-ssh-agent if $SSH_AUTH_SOCK is formatted as a Windows Named Pipe.
  2. IPC Path Fallback: Use %LOCALAPPDATA%\opk\opkssh.sock or \\.\pipe\opkssh-daemon when XDG_RUNTIME_DIR is not set on Windows.

7. OpenID Key-Bound Refresh & ssh-agent Signing Isolation

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-agent Backed Signer: golang.org/x/crypto/ssh/agent provides agent.NewClient(conn).Signer(pubkey, alg), which implements Go's crypto.Signer interface.
  • Key Isolation: The refresh daemon passes this crypto.Signer to OpenPubkey's client (opkClient). When the OpenID Provider requests a challenge signature during refresh, opkClient delegates the signing operation to ssh-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.

8. CLI & Process Integration

  • Daemon Mode: opkssh login --auto-refresh --daemon (or -d) detaches into the background.
  • Status Query: opkssh status queries 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 logout removes local key files AND purges associated OPKSSH certs from ssh-agent (agent.Remove()).
  • Service Installation: opkssh service install generates a systemd user unit (~/.config/systemd/user/opkssh.service), launchd plist, or Windows Scheduled Task for persistent startup across logins.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment