Static SSH keys are a liability. They live in authorized_keys files scattered across servers, get copied between teammates, and rarely get cleaned up when someone leaves. Vault’s SSH secrets engine solves this by acting as a certificate authority — it signs keys on demand, enforces TTLs, and gives you an audit trail. When a certificate expires, access is gone automatically.
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. No revocation 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 with cloud-init
For new VMs, embed the CA key directly in your cloud-init user-data so every instance is configured at boot without manual steps:
# 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
If you’re using Proxmox snippets (qm set 1000 --cicustom "user=local:snippets/user-data.yaml"), the snippet approach and the cloud-init GUI are mutually exclusive — pick one. For anything beyond basic settings, snippets give you full control.
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.
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:"
Key Points
- The CA private key never leaves Vault — only the public key is distributed to servers
- One CA public key deployed via cloud-init covers all future VMs without further changes
- Set
ttlbased on your access model — 1h for interactive sessions, longer for automation accounts - Roles control which usernames are embeddable in certificates — a certificate for
ansiblewon’t work asrootunlessrootis inallowed_users - To integrate with your existing identity system, pair this with Vault’s LDAP or OIDC auth methods so only authenticated users can request signed certificates
