SSH Certificate Authority with Vault

← All guides

SSH Certificate Authority with Vault

Static SSH keys are a recurring audit finding. They live in authorized_keys files scattered across servers, get copied between teammates, and rarely get cleaned up when someone leaves. There’s no expiration, no central record of who has access to what, and no way to revoke a key you didn’t know existed.

Vault’s SSH secrets engine acts as a certificate authority. It signs keys on demand, enforces TTLs, and produces an audit trail of every issuance request. When a certificate expires, access is gone automatically.

Prerequisites

Vault installed, running, and authenticated. Linux VMs or cloud instances you control. Administrative access to Vault.

How It Works

Vault signs the key — A user presents their public key. Vault signs it with the CA private key and returns a certificate with an embedded TTL.

Server trusts the CA — Each server is configured with Vault’s CA public key via TrustedUserCAKeys. It accepts any certificate signed by that CA.

Certificate expires — When the TTL runs out, the certificate is rejected automatically. No revocation process needed — the clock does the work.

Step 1 — Enable the SSH Secrets Engine

vault secrets enable -path=ssh ssh

Step 2 — Generate the CA Key

vault write ssh/config/ca generate_signing_key=true

Vault generates an internal SSH CA key pair. The private key stays inside Vault and never leaves — only the public key is distributed to servers.

Step 3 — Distribute the CA Public Key

# Export the CA public key
vault read -field=public_key ssh/config/ca > vault-ca.pub

Copy vault-ca.pub to each server and add one line to /etc/ssh/sshd_config:

TrustedUserCAKeys /etc/ssh/vault-ca.pub

Then restart sshd. The server will now accept any certificate signed by Vault’s CA.

Automating distribution at instance launch

For new VMs, embed the CA key in your cloud-init user-data so every instance is configured at boot without manual steps. This means the control is enforced from the moment the instance exists rather than relying on someone to configure it afterward.

# user-data.yaml
write_files:
  - path: /etc/ssh/vault-ca.pub
    content: |
      ssh-rsa AAAAB3NzaC1... (your vault CA public key)
    permissions: '0644'

bootcmd:
  - echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config

runcmd:
  - systemctl restart ssh

Step 4 — Create a Role

Roles define what the signed certificate is allowed to do — which usernames are valid, what the TTL is, and whether host or user certificates are issued. Keeping TTLs short and usernames explicit is how you enforce least privilege at the certificate layer.

vault write ssh/roles/dev-role \
    key_type=ca \
    allow_user_certificates=true \
    allowed_users="ansible,darnell" \
    default_user="ansible" \
    ttl="1h" \
    max_ttl="24h"

Step 5 — Sign a Key and Connect

# Generate a new key pair
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_demo

# Sign the public key — Vault returns the certificate
vault write -field=signed_key ssh/sign/dev-role \
    public_key=@$HOME/.ssh/id_ed25519_demo.pub \
    username=ansible \
    > $HOME/.ssh/id_ed25519_demo-cert.pub

chmod 600 ~/.ssh/id_ed25519_demo
chmod 644 ~/.ssh/id_ed25519_demo-cert.pub

# Connect using both the key and the certificate
ssh -i ~/.ssh/id_ed25519_demo \
    -i ~/.ssh/id_ed25519_demo-cert.pub \
    ansible@your-server

Inspect the Certificate

# Full certificate details
ssh-keygen -L -f ~/.ssh/id_ed25519_demo-cert.pub

# Check validity window
ssh-keygen -L -f ~/.ssh/id_ed25519_demo-cert.pub | grep "Valid:"

Control Mapping

Replacing static SSH keys with Vault-issued certificates isn’t just a security improvement — it directly addresses access control requirements that show up consistently in NIST-based assessments and SOC 2 audits.

NIST AC-2 NIST AC-17 NIST IA-5 NIST AU-2 NIST AU-12 NIST CM-6

AC-2 (Account Management) requires that access is revoked when no longer needed. Static SSH keys fail this requirement because revocation depends on someone manually removing the key from every authorized_keys file on every server that user could access. TTL-bound certificates fix this at the architecture level — the certificate expires automatically, and no server-side cleanup is required.

AC-17 (Remote Access) requires that remote access sessions are authorized, monitored, and controlled. Vault’s SSH role configuration is the authorization mechanism — access to a role requires a Vault policy, and the role itself defines which usernames and TTLs are permitted. Every certificate request goes through that gate.

IA-5 (Authenticator Management) covers the lifecycle of authentication credentials. Static SSH keys are long-lived credentials with no built-in expiration and no rotation mechanism. Short-TTL certificates replace them with credentials that are time-limited by design, eliminating an entire class of stale credential findings.

AU-2 and AU-12 (Audit Events and Audit Record Generation) require that authentication and access events are logged. Vault’s audit log captures every certificate signing request — who requested it, which role was used, what username was embedded, and when it was issued. That’s a complete access trail that static authorized_keys files cannot provide.

CM-6 (Configuration Settings) requires establishing and maintaining secure configuration settings. Authorized_keys files scattered across servers are a configuration drift problem — keys accumulate, ownership becomes unclear, and the actual state of access diverges from what any documentation says. Centralizing issuance through Vault eliminates that entire vector.

Key Points

  • The CA private key never leaves Vault — only the public key is distributed to servers
  • Deploying the CA public key via cloud-init means every new instance is enrolled in the certificate model automatically, with no manual configuration step that could be skipped
  • Set ttl based on your access model — 1h for interactive sessions, longer for automation accounts — and treat the TTL as a control parameter, not just a convenience setting
  • Roles control which usernames are embeddable in certificates — a certificate for ansible won’t work as root unless root is in allowed_users, which is how you enforce least privilege at the certificate layer
  • Pair certificate issuance with Vault’s LDAP or OIDC auth methods so every certificate request is tied to an authenticated identity — this is what gives you a complete, attributable access trail and turns the Vault audit log into genuine evidence for access reviews

Tags:

Darnell Keith

Not sure where you stand on this?

A Gap Assessment or IAM Governance Assessment tells you exactly where the gaps are — and what to do about them, in order of what matters most.

Get in Touch
NAXS Labs
Logo