github oriolrius/pki-manager-web v3.4.0
v3.4.0 — SSH KRL Client Distribution

latest releases: v3.12.3, v3.12.2, v3.12.1...
3 months ago

SSH KRL Client Distribution (krl-client)

This release delivers the SSH KRL Client Distribution milestone (KRLC-01..KRLC-14): a new standalone, statically linked linux/amd64 Go binary, krl-client, that keeps each host's OpenSSH RevokedKeys (/etc/ssh/revoked_keys) current. On every run it POSTs the per-host encrypted endpoint (POST /api/v1/external/ssh/krl with If-None-Match conditional caching), decrypts the KRL payload entirely on the host using the machine's own ecdsa-sha2-nistp256 SSH host key — never via the KMS — verifies the detached CA signature, enforces host-id binding and anti-rollback, then atomically installs the KRL 0444 root:root. Alongside the client, the backend was rebuilt (KRLC-02) to encrypt to each host's already-registered public key with a pinned, cross-implementation ECIES envelope, retiring the previous KMS-resident decryption path. This is the monorepo version bump 3.3.1 → 3.4.0 (root/frontend/backend).

Highlights

  • New krl-client binary — a single static linux/amd64 Go binary (CGO_ENABLED=0), no shell-outs to cosmian/openssl/jq/curl; std-lib crypto/HTTP plus golang.org/x/crypto/ssh.
  • Local-only decryption — the host decrypts in-process with its own private key (/etc/ssh/ssh_host_ecdsa_key by default). The private key never leaves the box and the KMS is never contacted.
  • Backend rebuild (KRLC-02) — per-host KRL encryption now runs natively (node:crypto) against the host's registered opensshHostPubkey, using a pinned ECIES envelope: P-256 ECDH + HKDF-SHA256 + AES-256-GCM, framing ephemeral-pubkey || nonce || ciphertext || tag. The KMS CreateKeyPair/Encrypt/Decrypt path and ssh_hosts.kms_pubkey_id are retired, with existing hosts migrated.
  • Fail-closed security posture — verifies the detached CA signature (DER ECDSA-P256 over sha256(krl)) before install, enforces host_id binding, and rejects any krl_version/krl_number that is not strictly newer than the installed one (anti-rollback via --state-dir).
  • Correct conditional caching — If-None-Match carries the server's opaque krl_version token (echoed from X-KRL-Version), not a hash of the local file, so the steady-state poll is a cheap 304 no-op.
  • Atomic, safe install — write-temp → fsync → chmod 0444 → chown 0:0 → rename → dir fsync, so sshd never observes a half-written KRL.
  • Provisioning is one flag — every on-host path default derives from the backend's single source of truth (backend/src/services/ssh-config.ts), so a host provisioned from the generated 60-ssh-ca.conf sshd drop-in runs with only --server-url.
  • Operationally complete — flags/env/config-file resolution, stable exit codes, structured slog logging (text/json), a single run_summary event per run, systemd unit+timer and cron packaging, and a krl-client(8) man page.
  • Supply-chain CI/CD — the release binary ships with SHA-256 checksums, a keyless cosign signature, and an SPDX SBOM.

What's included (krl-client)

  • Run model: a oneshot binary designed for a 15-minute jittered cron/systemd-timer schedule, comfortably under the endpoint rate limit (120 req / 60 s per source IP) and inside ssh-mon's ~30-minute staleness window.
  • Decryption key: reuses the existing ecdsa-sha2-nistp256 SSH host key by default (no keygen, no new registration for hosts already registered with an ecdsa key). ed25519-only hosts either generate an ecdsa host key or use a dedicated ECIES key; krl-client keygen creates a standalone 0600 keypair, and --host-key selects it.
  • Configuration: every setting resolves as flag > env KRL_CLIENT_* > config file > built-in default. Canonical path defaults (--host-key, --ca-pubkey /etc/ssh/ssh-user-ca.pub, --krl-file /etc/ssh/revoked_keys) are asserted in CI so they can never drift from the sshd drop-in and Ansible role.
  • Config file: a deliberately small flat YAML subset (key: value, comments, quoted scalars); nested mappings, sequences, unknown/duplicate keys are rejected. No secret is ever passed on the command line.
  • Observability: logs to stderr; exactly one run_summary event per run (outcome = up_to_date / updated / error, plus http_status, krl_version, krl_number, host_id, dry_run, exit_code). Secret material (host key, ciphertext, decrypted payload) is never logged at any level.
  • Packaging: hardened Type=oneshot systemd unit (NoNewPrivileges, ProtectSystem=strict, capabilities dropped, ordered After=time-sync.target), a 15-min .timer with RandomizedDelaySec jitter and Persistent=true, a cron fallback, and the man page — all under krl-client/packaging/.

Release artifacts & verification

The tagged release attaches the following assets:

Asset Purpose
krl-client-linux-amd64 static, CGO_ENABLED=0, -trimpath linux/amd64 binary
checksums.txt SHA-256 checksum of the binary
krl-client-linux-amd64.sig keyless cosign signature
krl-client-linux-amd64.pem cosign certificate (Fulcio)
krl-client-linux-amd64.spdx.json SPDX SBOM (syft)

Verify the checksum:

sha256sum -c checksums.txt        # run alongside the downloaded binary

Verify the keyless cosign signature (Sigstore/Fulcio/Rekor, OIDC identity = this repo's release workflow):

cosign verify-blob \
  --certificate krl-client-linux-amd64.pem \
  --signature   krl-client-linux-amd64.sig \
  --certificate-identity-regexp 'https://github.com/.*/krl-client\.yml@refs/tags/v.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  krl-client-linux-amd64

Inspect the SBOM:

# it is SPDX-JSON — read it directly, or scan it for vulnerabilities
grype sbom:./krl-client-linux-amd64.spdx.json

Exit codes

Code Meaning
0 up-to-date (304) or a newer KRL was verified and installed
1 usage / configuration error
2 network — DNS/connect/TLS/timeout, retries exhausted, or 5xx
3 local ECIES decryption failed
4 CA signature verification failed
5 payload expired (valid_until in the past beyond clock-skew)
6 host mismatch (payload host_id ≠ our host-id)
7 atomic install of the KRL file failed
8 version / integrity / anti-rollback failure
9 not provisioned / feature disabled (400/404/501)
10 rate limited (429) — back off and retry later

Operational caveats

NTP is a hard prerequisite. Each payload carries a valid_until; the client rejects one that is in the past beyond --clock-skew (default 300s) with exit code 5, so a host whose clock has drifted will fail every run. Keep a time daemon (chrony/systemd-timesyncd) running — the systemd unit already orders itself after time-sync.target.

Host-CA vs User-CA KRL asymmetry. The encrypted /krl endpoint currently resolves the host's Host CA and serves that CA's KRL, but sshd checks RevokedKeys against the user certificates presented at login and so semantically wants the User-CA KRL. Until this is reconciled, the installed KRL revokes host-CA-signed material rather than the user certificates sshd authenticates — do not rely on this path alone to revoke user access. The guaranteed revocation mechanism remains the bare, CA-signed, TLS-served public KRL plus short certificate TTLs. Reusing the SSH host key for ECIES also couples KRL confidentiality to host-key rotation; use a dedicated --host-key for isolation.

Decisions

decision-015 (SSH KRL Client Decryption Model) records this local-key model and supersedes the KMS-resident adopted model of decision-013; decision-013's guaranteed-revocation floor — the bare, CA-signed, TLS-served public KRL — is unchanged and remains the primary revocation mechanism. Milestone scope and runtime flow are documented in doc-007.


Full Changelog: v3.3.1...v3.4.0

Don't miss a new pki-manager-web release

NewReleases is sending notifications on new releases.