SSH certificate authority¶
secsy-pki includes an HSM-backed OpenSSH certificate authority: it signs
OpenSSH user and host certificates with CA keys held by the configured
key provider (PKCS#11 HSM, cloud KMS, or the software keystore), and publishes
revocations as OpenSSH Key Revocation Lists (KRLs) that sshd consumes
directly. The CA private key never leaves the backend — every signature is
produced by the device through crypto.Signer.
Why SSH certificates instead of raw authorized_keys:
- One trust anchor instead of key sprawl. Hosts trust the CA once
(
TrustedUserCAKeys); users and automation get short-lived certificates instead of long-lived keys copied to every server. - Short-lived, policy-shaped credentials. Profiles pin certificate type,
validity bounds, permitted principals, extensions, and critical options
(e.g.
source-address,force-command). - Real revocation. Every certificate carries a store-allocated serial;
revocations (by serial or by key ID) are served as a KRL over HTTP, the
artifact
sshd'sRevokedKeysoption understands. - Enterprise controls. Signing and revocation are RBAC- and tenant-gated
exactly like X.509 issuance, and every operation lands in the tamper-evident
audit log (
ssh.ca_init,ssh.sign,ssh.revoke).
Quick start (CLI)¶
# 1. Create the CA — the key is generated inside the HSM.
$ secsy-ca ssh ca-init -label ops-ssh-ca -key-type ed25519 > ops-ssh-ca.pub
# 2. Sign a user's public key into a 12h certificate (user-default profile).
$ secsy-ca ssh sign-user -ca ops-ssh-ca -pub ~/.ssh/id_ed25519.pub \
-key-id alice@corp -principals alice -out ~/.ssh/id_ed25519-cert.pub
# 3. Sign a host key into a host certificate (host-default profile, 90d).
$ secsy-ca ssh sign-host -ca ops-ssh-ca -pub /etc/ssh/ssh_host_ed25519_key.pub \
-principals web1.example.com,web1 -out /etc/ssh/ssh_host_ed25519_key-cert.pub
# 4. Revoke a certificate and regenerate the KRL.
$ secsy-ca ssh revoke -ca ops-ssh-ca -serial 3 -reason "laptop stolen"
$ secsy-ca ssh krl -ca ops-ssh-ca -out /etc/ssh/ops-ssh-ca.krl
# Inventory and profiles.
$ secsy-ca ssh list -ca ops-ssh-ca
$ secsy-ca ssh profiles
ssh-keygen -L -f id_ed25519-cert.pub shows exactly what was signed;
ssh-keygen -Q -f ops-ssh-ca.krl id_ed25519-cert.pub checks a certificate
against the KRL — the same code path sshd uses.
Deploying trust¶
User certificates — on every host that should accept them
(/etc/ssh/sshd_config):
Fetch both artifacts from the server (public endpoints, no credentials — like CRL distribution):
$ curl -o /etc/ssh/ops-ssh-ca.pub https://pki.example.com/api/ssh/cas/<ca-id>/public
$ curl -o /etc/ssh/ops-ssh-ca.krl https://pki.example.com/api/ssh/cas/<ca-id>/krl
Refresh the KRL on a timer (cron/systemd) so revocations propagate; the KRL header carries a monotonically increasing version (the revocation count) and a generation timestamp.
Host certificates — clients pin the CA in ~/.ssh/known_hosts (or a
global ssh_known_hosts):
and each host presents its certificate (/etc/ssh/sshd_config):
Signing profiles¶
A profile is the policy a certificate is signed under. Two built-ins exist:
| Profile | Type | Default / max validity | Notes |
|---|---|---|---|
user-default |
user | 12h / 30d | standard permit-* extension set; source-address, force-command, verify-required critical options permitted |
host-default |
host | 90d / 366d | no extensions or critical options (host certs carry none) |
Custom profiles are defined in config and overlay the built-ins by name:
ssh:
krl_comment: "corp ops CA" # stamped into generated KRLs
profiles:
- name: ci-deploy
description: CI deploy automation
cert_type: user
default_validity: 1h # Go duration, or 30d / 12w
max_validity: 24h # longer requests are clamped
allowed_principals: ["deploy-*"] # glob patterns; empty = any
max_principals: 2
default_extensions: # applied when the request names none
permit-pty: ""
allowed_critical_options: # request-supplied keys must be listed
- source-address
- name: prod-host
cert_type: host
default_validity: 90d
max_validity: 366d
allowed_principals: ["*.prod.example.com"]
Enforcement rules:
- Certificates must name at least one principal (a principal-less OpenSSH
certificate is valid for every user/host). Opt out per profile with
allow_empty_principals: trueonly if you understand the blast radius. - A request's extensions/critical options replace the profile defaults but
every key must be permitted (a default key or in the
allowed_*list). - Requested validity beyond
max_validityis clamped (matching the X.509 profile convention).ValidAfteris backdated 5 minutes for clock skew. - Host certificates never carry extensions or critical options; requests that ask for them are refused.
Serials and revocation¶
Serials are allocated from the CA's monotonic store counter (shared with the X.509 issuance path, so serials are unique per CA regardless of certificate kind) — this is what makes serial-based revocation trustworthy.
Revoke by serial (one certificate) or by key ID (every certificate issued for an identity):
$ secsy-ca ssh revoke -ca ops-ssh-ca -serial 42
$ secsy-ca ssh revoke -ca ops-ssh-ca -key-id alice@corp
The KRL is generated from the revocation store on demand — secsy-ca ssh krl
or GET /api/ssh/cas/{id}/krl — as a KRL_SECTION_CERTIFICATES section bound
to the CA public key (so revoked serials cannot collide with another CA's
serial space), with serial-list and key-ID subsections per OpenSSH's
PROTOCOL.krl. Interop is proven in tests against ssh-keygen -Q.
REST API¶
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/ssh/cas |
ca:manage (step-up capable) |
initialize an SSH CA |
GET /api/ssh/profiles |
read role | effective signing profiles |
POST /api/ssh/cas/{id}/sign |
issue capability on the CA | sign a user/host certificate |
POST /api/ssh/cas/{id}/revoke |
issue capability (step-up capable) | revoke by serial or key ID |
GET /api/ssh/cas/{id}/certificates |
read role + tenant membership | inventory |
GET /api/ssh/cas/{id}/revocations |
read role + tenant membership | revocation records |
GET /api/ssh/cas/{id}/public |
public | CA public key (trust anchor) |
GET /api/ssh/cas/{id}/krl |
public | binary KRL for RevokedKeys |
Signing example:
$ curl -u root:$PASSWORD -X POST https://pki.example.com/api/ssh/cas/$CA/sign \
-H 'Content-Type: application/json' \
-d '{"public_key":"ssh-ed25519 AAAA... alice@laptop",
"cert_type":"user","principals":["alice"],"key_id":"alice@corp",
"critical_options":{"source-address":"10.0.0.0/8"}}'
Authorization mirrors X.509 issuance: signing/revocation require the issue
capability within the CA's tenant (or a per-CA SIGN_CERTIFICATE grant);
cross-tenant access is denied. The full OpenAPI definition lives in the served
spec (/openapi.yaml, tag SSH CA).
The legacy
/api/keys/{id}/signendpoint (restriction-set based, random serials, no revocation) remains for the originalsecsy-sshclient; new integrations should use/api/ssh/*, which adds store-allocated serials, profiles, and KRL revocation.
Key types¶
ed25519 (default), ecdsa-p256, ecdsa-p384, rsa-2048, rsa-4096.
Ed25519 requires an EdDSA-capable token (SoftHSM ≥ 2.4, YubiHSM 2); on HSMs
without it use ecdsa-p256. RSA CAs sign with rsa-sha2-512 (never legacy
ssh-rsa/SHA-1). The SoftHSM suite exercises all three families end to end,
including ssh-keygen interop.
Audit and metrics¶
Audit actions: ssh.ca_init, ssh.sign (serial, type, key ID, principals,
profile in the detail), ssh.revoke (target + idempotence). CLI operations
are recorded with actor cli.
Metrics: secsy_ssh_certificates_total{type,result},
secsy_ssh_revocations_total{result}, secsy_ssh_krl_requests_total{result},
plus the shared HSM operation/duration metrics from the key-provider layer.