SSH Certificate Authority -- Sign SSH Keys on Demand
Signs SSH public keys with a CA certificate, enabling certificate-based SSH authentication. No passwords, no authorized_keys distribution -- clients present a signed certificate and the server trusts the CA.
Capabilities
| Capability | Description |
|---|---|
secret_generation | Signs SSH public keys, returning a certificate |
secret_validation | Verifies a certificate was signed by this CA and is not expired |
Configuration
| Parameter | Required | Description | Example |
|---|---|---|---|
ca_private_key | Optional | PEM-encoded CA private key. If omitted, a new Ed25519 CA key pair is generated at bootstrap | -----BEGIN OPENSSH PRIVATE KEY-----... |
ca_public_key | Optional | Corresponding CA public key. Required if ca_private_key is provided | ssh-ed25519 AAAA... |
default_ttl | Optional | Default certificate TTL when not specified in request. Defaults to 8h | 24h |
max_ttl | Optional | Maximum allowed TTL for any certificate. Defaults to 720h (30 days) | 720h |
When both ca_private_key and ca_public_key are omitted, the engine generates a new Ed25519 CA key pair at bootstrap and stores it in the engine's config. The CA public key must be added to target servers in /etc/ssh/sshd_config:
TrustedUserCAKeys /etc/ssh/ca.pub
Roles
| Role | Principals | Max TTL | Use Case |
|---|---|---|---|
user | ["username"] (from request) | 24h | Developer SSH access |
deploy | ["deploy", "ubuntu"] | 8h | CI/CD deployment pipelines |
admin | ["root"] | 1h | Emergency root access |
Operations
generate
Signs an SSH public key with the CA, producing a certificate.
Request:
{
"method": "generate",
"params": {
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG...",
"principals": ["ubuntu", "deploy"],
"ttl": "8h",
"key_id": "deploy-pipeline-42"
}
}
What happens internally:
- Parses the user's SSH public key
- Generates a random serial number
- Builds an
ssh.Certificatewith the requested principals and TTL - Signs it with the CA private key
- Returns the marshalled certificate
Response:
{
"data": {
"signed_certificate": "[email protected] AAAAIHNzaC1lZDI...",
"serial": 8837462519,
"key_id": "deploy-pipeline-42",
"principals": ["ubuntu", "deploy"],
"valid_after": "2026-04-02T10:00:00Z",
"valid_until": "2026-04-02T18:00:00Z",
"ca_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
}
}
validate
Verifies a certificate was signed by this CA and is still valid.
Request:
{
"method": "validate",
"params": {
"certificate": "[email protected] AAAAIHNzaC1lZDI..."
}
}
Response (valid):
{
"data": {
"valid": true,
"serial": 8837462519,
"key_id": "deploy-pipeline-42",
"principals": ["ubuntu", "deploy"],
"valid_after": "2026-04-02T10:00:00Z",
"valid_until": "2026-04-02T18:00:00Z"
}
}
Response (invalid):
{
"data": {
"valid": false,
"message": "certificate has expired"
}
}
Complete Go Plugin Source
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"time"
"getarcan.dev/arcan/sdk"
"golang.org/x/crypto/ssh"
)
type SSHEngine struct {
caSigner ssh.Signer
caPubKey ssh.PublicKey
}
type generateParams struct {
PublicKey string `json:"public_key"`
Principals []string `json:"principals"`
TTL string `json:"ttl"`
KeyID string `json:"key_id"`
}
type validateParams struct {
Certificate string `json:"certificate"`
}
func (e *SSHEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "ssh",
Version: "0.1.0",
DisplayName: "SSH Certificate Authority",
Description: "Signs SSH public keys with a CA certificate for certificate-based auth",
Capabilities: []string{"secret_generation", "secret_validation"},
}
}
func (e *SSHEngine) Ping() error {
if e.caSigner == nil {
return fmt.Errorf("CA key not loaded")
}
return nil
}
func (e *SSHEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
var p generateParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}
if p.PublicKey == "" {
return nil, fmt.Errorf("public_key is required")
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(p.PublicKey))
if err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
}
ttl := 8 * time.Hour
if p.TTL != "" {
ttl, err = time.ParseDuration(p.TTL)
if err != nil {
return nil, fmt.Errorf("invalid ttl: %w", err)
}
}
var serialBytes [8]byte
if _, err := rand.Read(serialBytes[:]); err != nil {
return nil, fmt.Errorf("generating serial: %w", err)
}
serial := binary.BigEndian.Uint64(serialBytes[:])
now := time.Now().UTC()
cert := &ssh.Certificate{
CertType: ssh.UserCert,
Key: pubKey,
Serial: serial,
KeyId: p.KeyID,
ValidPrincipals: p.Principals,
ValidAfter: uint64(now.Unix()),
ValidBefore: uint64(now.Add(ttl).Unix()),
Permissions: ssh.Permissions{
Extensions: map[string]string{
"permit-pty": "",
"permit-user-rc": "",
"permit-agent-forwarding": "",
},
},
}
if err := cert.SignCert(rand.Reader, e.caSigner); err != nil {
return nil, fmt.Errorf("signing certificate: %w", err)
}
return &sdk.SecretResult{
Data: map[string]any{
"signed_certificate": string(ssh.MarshalAuthorizedKey(cert)),
"serial": serial,
"key_id": p.KeyID,
"principals": p.Principals,
"valid_after": now.Format(time.RFC3339),
"valid_until": now.Add(ttl).Format(time.RFC3339),
"ca_public_key": string(ssh.MarshalAuthorizedKey(e.caPubKey)),
},
}, nil
}
func (e *SSHEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
var p validateParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(p.Certificate))
if err != nil {
return &sdk.ValidationResult{Valid: false, Message: fmt.Sprintf("parse error: %v", err)}, nil
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return &sdk.ValidationResult{Valid: false, Message: "not an SSH certificate"}, nil
}
checker := &ssh.CertChecker{
IsUserAuthority: func(auth ssh.PublicKey) bool {
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(e.caPubKey)
},
}
if err := checker.CheckCert("", cert); err != nil {
return &sdk.ValidationResult{Valid: false, Message: err.Error()}, nil
}
return &sdk.ValidationResult{Valid: true, Message: fmt.Sprintf(
"serial=%d principals=%v expires=%s",
cert.Serial, cert.ValidPrincipals,
time.Unix(int64(cert.ValidBefore), 0).UTC().Format(time.RFC3339),
)}, nil
}
func main() {
var signer ssh.Signer
if keyData := os.Getenv("ARCAN_SSH_CA_PRIVATE_KEY"); keyData != "" {
var err error
signer, err = ssh.ParsePrivateKey([]byte(keyData))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse CA key: %v\n", err)
os.Exit(1)
}
} else {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to generate CA key: %v\n", err)
os.Exit(1)
}
signer, err = ssh.NewSignerFromKey(priv)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create signer: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "WARNING: using ephemeral CA key -- set ARCAN_SSH_CA_PRIVATE_KEY for persistence\n")
}
eng := &SSHEngine{
caSigner: signer,
caPubKey: signer.PublicKey(),
}
sdk.Serve(eng)
}
Build & Install
mkdir -p engines/ssh && cd engines/ssh
go mod init getarcan.dev/engines/ssh
go get getarcan.dev/arcan/sdk
go get golang.org/x/crypto/ssh
# Build
go build -o arcan-engine-ssh .
# Install (copy to Arcan plugin directory)
cp arcan-engine-ssh ~/.arcan/plugins/
Usage
# Start the engine (Arcan core does this automatically)
echo '{"method":"ping","params":{}}' | ./arcan-engine-ssh
# => {"data":{"status":"healthy"}}
# Sign a public key
echo '{"method":"generate","params":{"public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...","principals":["ubuntu","deploy"],"ttl":"8h","key_id":"ci-job-42"}}' | ./arcan-engine-ssh
# => {"data":{"signed_certificate":"[email protected] AAAA...","serial":...}}
# Validate a certificate
echo '{"method":"validate","params":{"certificate":"[email protected] AAAA..."}}' | ./arcan-engine-ssh
# => {"data":{"valid":true,"message":"serial=... principals=[ubuntu deploy] expires=..."}}
Using the signed certificate
# Save the signed certificate
echo "$SIGNED_CERT" > ~/.ssh/id_ed25519-cert.pub
# SSH using the certificate (server must trust the CA)
ssh -o CertificateFile=~/.ssh/id_ed25519-cert.pub [email protected]
Server-side setup
# Add the CA public key to the server
echo "$CA_PUBLIC_KEY" > /etc/ssh/ca.pub
# Add to sshd_config
echo "TrustedUserCAKeys /etc/ssh/ca.pub" >> /etc/ssh/sshd_config
systemctl restart sshd
Security Notes
- The CA private key is the root of trust. Protect it with the same rigor as a TLS root CA key.
- Certificates include a serial number for revocation tracking. Implement a revocation list (KRL) for production use.
- Short TTLs (1-8h) are preferred. Certificates expire automatically -- no revocation infrastructure needed for most cases.
- The engine uses
crypto/randfor all random number generation (serial numbers). - All timestamps use UTC (
time.Now().UTC()). - The
permit-ptyextension allows interactive sessions. Remove it for non-interactive deploy keys. - Host certificates (CertType
ssh.HostCert) are not covered here but follow the same signing pattern.