Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs stored in a new barrier_keys table, rather than encrypting all barrier entries directly. A v2 ciphertext format (0x02) embeds the key ID so the barrier can resolve which DEK to use on decryption. v1 ciphertext remains supported for backward compatibility. Key changes: - crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs - barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys) - seal: RotateMEK re-wraps DEKs without re-encrypting data - engine: Mount auto-creates per-engine DEK - REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate - proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate - db: migration v2 adds barrier_keys table Also includes: security audit report, CSRF protection, engine design specs (sshca, transit, user), path-bound AAD migration tool, policy engine enhancements, and ARCHITECTURE.md updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
SSH CA Engine Implementation Plan
Overview
The SSH CA engine signs SSH host and user certificates using Go's
golang.org/x/crypto/ssh package. It follows the same architecture as the CA
engine: a single CA key pair signs certificates directly (no intermediate
hierarchy, since SSH certificates are flat).
Engine Type
sshca — registered constant already exists in internal/engine/engine.go.
Mount Configuration
Passed as config at mount time:
| Field | Default | Description |
|---|---|---|
key_algorithm |
"ed25519" |
CA key type: ed25519, ecdsa, rsa |
key_size |
0 |
Key size (ignored for ed25519; 256/384/521 for ECDSA, 2048/4096 for RSA) |
max_ttl |
"87600h" |
Maximum certificate validity |
default_ttl |
"24h" |
Default certificate validity |
Barrier Storage Layout
engine/sshca/{mount}/config.json Engine configuration
engine/sshca/{mount}/ca/key.pem CA private key (PEM, PKCS8)
engine/sshca/{mount}/ca/pubkey.pub CA public key (SSH authorized_keys format)
engine/sshca/{mount}/profiles/{name}.json Signing profiles
engine/sshca/{mount}/certs/{serial}.json Signed cert records
engine/sshca/{mount}/krl.bin Current KRL (OpenSSH format)
In-Memory State
type SSHCAEngine struct {
barrier barrier.Barrier
config *SSHCAConfig
caKey crypto.PrivateKey // CA signing key
caSigner ssh.Signer // ssh.Signer wrapping caKey
mountPath string
mu sync.RWMutex
}
Key material (caKey, caSigner) is zeroized on Seal().
Lifecycle
Initialize
- Parse and store config in barrier as
config.json. - Generate CA key pair using the configured algorithm.
- Store private key PEM and SSH public key in barrier.
- Load key into memory as
ssh.Signer.
Unseal
- Load config from barrier.
- Load CA private key from barrier, parse into
crypto.PrivateKey. - Wrap as
ssh.Signer.
Seal
- Zeroize
caKey(samezeroizeKeyhelper used by CA engine). - Nil out
caSigner,config.
Operations
| Operation | Auth Required | Description |
|---|---|---|
get-ca-pubkey |
None | Return CA public key in SSH authorized_keys format |
sign-host |
User+Policy | Sign an SSH host certificate |
sign-user |
User+Policy | Sign an SSH user certificate |
create-profile |
Admin | Create a signing profile |
update-profile |
Admin | Update a signing profile |
get-profile |
User/Admin | Get signing profile details |
list-profiles |
User/Admin | List signing profiles |
delete-profile |
Admin | Delete a signing profile |
get-cert |
User/Admin | Get cert record by serial |
list-certs |
User/Admin | List issued cert summaries |
revoke-cert |
Admin | Revoke a certificate (soft flag) |
delete-cert |
Admin | Delete a certificate record |
sign-host
Request data:
| Field | Required | Description |
|---|---|---|
public_key |
Yes | SSH public key to sign (authorized_keys format) |
hostnames |
Yes | Valid principals (hostnames) |
ttl |
No | Validity duration (default: default_ttl) |
extensions |
No | Map of extensions to include |
Flow:
- Authenticate caller (
IsUser()); admins bypass policy/ownership checks. - Parse the supplied SSH public key.
- Generate a 64-bit serial using
crypto/rand. - Build
ssh.CertificatewithCertType: ssh.HostCert, principals, validity, serial. - Policy check:
sshca/{mount}/id/{hostname}for each principal, with ownership rules (same as CA engine — hostname not held by another user's active cert). - Sign with
caSigner. - Store
CertRecordin barrier (certificate bytes, metadata; no private key). - Return signed certificate in OpenSSH format.
sign-user
Request data:
| Field | Required | Description |
|---|---|---|
public_key |
Yes | SSH public key to sign (authorized_keys format) |
principals |
Yes | Valid usernames/principals |
ttl |
No | Validity duration (default: default_ttl) |
profile |
No | Signing profile name (see below) |
extensions |
No | Map of extensions (e.g. permit-pty) |
Critical options are not accepted directly in the sign request. They can
only be applied via a signing profile. This prevents unprivileged users from
setting security-sensitive options like force-command or source-address.
Flow:
- Authenticate caller (
IsUser()); admins bypass. - Parse the supplied SSH public key.
- If
profileis specified, load the signing profile and check policy (sshca/{mount}/profile/{profile_name}, actionread). Merge the profile's critical options and extensions into the certificate. Any extensions in the request are merged with profile extensions; conflicts are resolved in favor of the profile. - Generate a 64-bit serial using
crypto/rand. - Build
ssh.CertificatewithCertType: ssh.UserCert, principals, validity, serial. - If the profile specifies
max_ttl, enforce it (cap the requested TTL). - Policy check:
sshca/{mount}/id/{principal}for each principal. Default rule: a user can only sign certs for their own username as principal, unless a policy grants access to other principals. - Sign with
caSigner. - Store
CertRecordin barrier (includes profile name if used). - Return signed certificate in OpenSSH format.
Signing Profiles
A signing profile is a named, admin-defined template that controls what goes into a signed user certificate. Profiles are the only way to set critical options, and access to each profile is policy-gated.
Profile Configuration
type SigningProfile struct {
Name string `json:"name"`
CriticalOptions map[string]string `json:"critical_options"` // e.g. {"force-command": "/usr/bin/rsync", "source-address": "10.0.0.0/8"}
Extensions map[string]string `json:"extensions"` // merged with request extensions
MaxTTL string `json:"max_ttl,omitempty"` // overrides engine max_ttl if shorter
AllowedPrincipals []string `json:"allowed_principals,omitempty"` // if set, restricts principals
}
Storage
engine/sshca/{mount}/profiles/{name}.json
Operations
| Operation | Auth Required | Description |
|---|---|---|
create-profile |
Admin | Create a signing profile |
update-profile |
Admin | Update a signing profile |
get-profile |
User/Admin | Get profile details |
list-profiles |
User/Admin | List available profiles |
delete-profile |
Admin | Delete a signing profile |
Policy Gating
Access to a profile is controlled via policy on resource
sshca/{mount}/profile/{profile_name}, action read. A user must have
policy access to both the profile and the requested principals to sign
a certificate using that profile.
Example use cases:
restricted-sftp:force-command: "internal-sftp",source-address: "10.0.0.0/8"— grants users SFTP-only access from internal networks.deploy:force-command: "/usr/local/bin/deploy",source-address: "10.0.1.0/24"— CI/CD deploy key with restricted command.unrestricted: empty critical options — for trusted users who need full shell access (admin-only policy).
CertRecord
type CertRecord struct {
Serial uint64 `json:"serial"`
CertType string `json:"cert_type"` // "host" or "user"
Principals []string `json:"principals"`
CertData string `json:"cert_data"` // OpenSSH format
IssuedBy string `json:"issued_by"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked,omitempty"`
RevokedAt time.Time `json:"revoked_at,omitempty"`
RevokedBy string `json:"revoked_by,omitempty"`
}
Key Revocation List (KRL)
SSH servers cannot query Metacrypt in real time to check whether a certificate
has been revoked. Instead, the SSH CA engine generates an OpenSSH-format KRL
(Key Revocation List) that SSH servers fetch periodically and reference via
RevokedKeys in sshd_config.
KRL Generation
The engine maintains a KRL in memory, rebuilt whenever a certificate is revoked
or deleted. The KRL is a binary blob in OpenSSH KRL format
(golang.org/x/crypto/ssh provides marshalling helpers), containing:
- Serial revocations: Revoked certificate serial numbers, keyed to the CA public key. This is the most compact representation.
- KRL version: Monotonically increasing counter, incremented on each rebuild. SSH servers can use this to detect stale KRLs.
- Generated-at timestamp: Included in the KRL for freshness checking.
The KRL is stored in the barrier at engine/sshca/{mount}/krl.bin and cached
in memory. It is rebuilt on:
revoke-cert— adds the serial to the KRL.delete-cert— if the cert was revoked, the KRL is regenerated from all remaining revoked certs.- Engine unseal — loaded from barrier into memory.
Distribution
KRL distribution is a pull model. SSH servers fetch the current KRL via an unauthenticated endpoint (analogous to the public CA key endpoint):
| Method | Path | Description |
|---|---|---|
| GET | /v1/sshca/{mount}/krl |
Current KRL (binary, OpenSSH format) |
The response includes:
Content-Type: application/octet-streamETagheader derived from the KRL version, enabling conditional fetches.Cache-Control: max-age=60to encourage periodic refresh without overwhelming the server.
SSH servers should be configured to fetch the KRL on a cron schedule (e.g.
every 1–5 minutes) and write it to a local file referenced by sshd_config:
RevokedKeys /etc/ssh/metacrypt_krl
A helper script or systemd timer can fetch the KRL:
curl -s -o /etc/ssh/metacrypt_krl \
https://metacrypt.example.com:8443/v1/sshca/ssh/krl
Operations
| Operation | Auth Required | Description |
|---|---|---|
get-krl |
None | Return the current KRL in OpenSSH format |
gRPC Service (proto/metacrypt/v2/sshca.proto)
service SSHCAService {
rpc GetCAPublicKey(GetCAPublicKeyRequest) returns (GetCAPublicKeyResponse);
rpc SignHost(SignHostRequest) returns (SignHostResponse);
rpc SignUser(SignUserRequest) returns (SignUserResponse);
rpc CreateProfile(CreateProfileRequest) returns (CreateProfileResponse);
rpc UpdateProfile(UpdateProfileRequest) returns (UpdateProfileResponse);
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse);
rpc ListProfiles(ListProfilesRequest) returns (ListProfilesResponse);
rpc DeleteProfile(DeleteProfileRequest) returns (DeleteProfileResponse);
rpc GetCert(SSHGetCertRequest) returns (SSHGetCertResponse);
rpc ListCerts(SSHListCertsRequest) returns (SSHListCertsResponse);
rpc RevokeCert(SSHRevokeCertRequest) returns (SSHRevokeCertResponse);
rpc DeleteCert(SSHDeleteCertRequest) returns (SSHDeleteCertResponse);
rpc GetKRL(GetKRLRequest) returns (GetKRLResponse);
}
REST Endpoints
Public (unseal required, no auth):
| Method | Path | Description |
|---|---|---|
| GET | /v1/sshca/{mount}/ca |
CA public key (SSH format) |
| GET | /v1/sshca/{mount}/krl |
Current KRL (OpenSSH format) |
Typed endpoints (auth required):
| Method | Path | Description |
|---|---|---|
| POST | /v1/sshca/{mount}/sign-host |
Sign host cert |
| POST | /v1/sshca/{mount}/sign-user |
Sign user cert |
| POST | /v1/sshca/{mount}/profiles |
Create profile |
| GET | /v1/sshca/{mount}/profiles |
List profiles |
| GET | /v1/sshca/{mount}/profiles/{name} |
Get profile |
| PUT | /v1/sshca/{mount}/profiles/{name} |
Update profile |
| DELETE | /v1/sshca/{mount}/profiles/{name} |
Delete profile |
| GET | /v1/sshca/{mount}/cert/{serial} |
Get cert record |
| POST | /v1/sshca/{mount}/cert/{serial}/revoke |
Revoke cert |
| DELETE | /v1/sshca/{mount}/cert/{serial} |
Delete cert record |
All operations are also accessible via the generic POST /v1/engine/request.
Web UI
Add to /dashboard the ability to mount an SSH CA engine.
Add an /sshca page (or section on the existing PKI page) displaying:
- CA public key (for
TrustedUserCAKeys/@cert-authoritylines) - Sign host/user certificate form
- Certificate list with detail view
Implementation Steps
internal/engine/sshca/— ImplementSSHCAEngine(types, lifecycle, operations). ReusezeroizeKeyfrominternal/engine/ca/(move to shared helper or duplicate).- Register factory in
cmd/metacrypt/main.go:registry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine). - Proto definitions —
proto/metacrypt/v2/sshca.proto, runmake proto. - gRPC handlers —
internal/grpcserver/sshca.go. - REST routes — Add to
internal/server/routes.go. - Web UI — Add template + webserver routes.
- Tests — Unit tests with in-memory barrier following the CA test pattern.
Dependencies
golang.org/x/crypto/ssh(already ingo.modvia transitive deps)
Security Considerations
- CA private key encrypted at rest in barrier, zeroized on seal.
- Signed certificates do not contain private keys.
- Serial numbers are always generated server-side using
crypto/rand(64-bit); user-provided serials are not accepted. max_ttlis enforced server-side; the engine rejects TTL values exceeding it.- User cert signing defaults to allowing only the caller's own username as principal, preventing privilege escalation.
- Critical options (
force-command,source-address, etc.) are only settable via admin-defined signing profiles, never directly in the sign request. This prevents unprivileged users from bypassingsshd_configrestrictions. - Profile access is policy-gated: a user must have policy access to
sshca/{mount}/profile/{name}to use a profile.