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-clientbinary — a single staticlinux/amd64Go binary (CGO_ENABLED=0), no shell-outs tocosmian/openssl/jq/curl; std-lib crypto/HTTP plusgolang.org/x/crypto/ssh. - Local-only decryption — the host decrypts in-process with its own private key (
/etc/ssh/ssh_host_ecdsa_keyby 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 registeredopensshHostPubkey, using a pinned ECIES envelope: P-256 ECDH + HKDF-SHA256 + AES-256-GCM, framingephemeral-pubkey || nonce || ciphertext || tag. The KMSCreateKeyPair/Encrypt/Decryptpath andssh_hosts.kms_pubkey_idare retired, with existing hosts migrated. - Fail-closed security posture — verifies the detached CA signature (DER ECDSA-P256 over
sha256(krl)) before install, enforceshost_idbinding, and rejects anykrl_version/krl_numberthat is not strictly newer than the installed one (anti-rollback via--state-dir). - Correct conditional caching —
If-None-Matchcarries the server's opaquekrl_versiontoken (echoed fromX-KRL-Version), not a hash of the local file, so the steady-state poll is a cheap304no-op. - Atomic, safe install — write-temp → fsync → chmod
0444→ chown0: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 generated60-ssh-ca.confsshd drop-in runs with only--server-url. - Operationally complete — flags/env/config-file resolution, stable exit codes, structured
sloglogging (text/json), a singlerun_summaryevent per run, systemd unit+timer and cron packaging, and akrl-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
oneshotbinary 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-nistp256SSH 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 keygencreates a standalone0600keypair, and--host-keyselects 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_summaryevent per run (outcome=up_to_date/updated/error, plushttp_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=oneshotsystemd unit (NoNewPrivileges,ProtectSystem=strict, capabilities dropped, orderedAfter=time-sync.target), a 15-min.timerwithRandomizedDelaySecjitter andPersistent=true, a cron fallback, and the man page — all underkrl-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 binaryVerify 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-amd64Inspect the SBOM:
# it is SPDX-JSON — read it directly, or scan it for vulnerabilities
grype sbom:./krl-client-linux-amd64.spdx.jsonExit 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(default300s) with exit code5, 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 aftertime-sync.target.
Host-CA vs User-CA KRL asymmetry. The encrypted
/krlendpoint currently resolves the host's Host CA and serves that CA's KRL, but sshd checksRevokedKeysagainst 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-keyfor 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