Securely controlling SSH access to your servers is critical. HashiCorp Vault can act as a central SSH Certificate Authority (CA), allowing you to issue signed user keys, enforce TTLs, and avoid distributing static SSH keys.
Prerequisites / Requirements
- HashiCorp Vault
- Linux Virtual Machine
Step 1: Enable the SSH Secrets Engine
Enable the SSH engine at a chosen mount point:
vault secrets enable -path=ssh ssh
- The
ssh
mount point manages Vault’s SSH CA and key signing. - Vault will generate its own internal SSH CA key for signing user keys.
Step 2: Generate the Vault SSH CA Key
vault write ssh/config/ca generate_signing_key=true
- Vault now holds an internal SSH CA key.
- The private key stays in Vault, ensuring secure signing.
Step 3: Extract the SSH CA Public Key
vault read -field=public_key ssh/config/ca > vault-ca.pub
- This single
vault-ca.pub
file is distributed to your VMs. - It allows your servers to verify certificates signed by Vault.
The last command outputs a public key that should go in the /etc/ssh directory of your Linux virtual machine. However, it wouldn’t be very convenient to load it manually on every device. I handle this through a snippet on the Proxmox hypervisor.
You can create a YAML config file and place it in the /var/lib/vz/snippets directory of your Proxmox host. If you already have a cloud image template, you can attach a snippet, and the public key will be loaded into each virtual machine at boot time.
Example Snippet
# user-data.yaml
users:
- name: user
passwd: mkpasswd --method=SHA-256 password
lock_passwd: false
ssh_authorized_keys:
- Paste SSH Public Key
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
- name: ansible
ssh_authorized_keys:
- Paste SSH Public Key
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
package_update: true
package_upgrade: true
write_files:
- path: /etc/ssh/vault-ca.pub
content: |
Paste Public Key here
permissions: '0644'
bootcmd:
- echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config
runcmd:
- systemctl restart ssh
Assuming your cloud image template has an ID of 1000:
qm set 1000 --cicustom "user=local:snippets/user-data.yaml"
Note: This method doesn’t mesh well with Cloud-Init in the GUI. Either use snippets to configure your cloud images, or use the GUI and leave additional configurations to Ansible and other configuration tools.
Step 4: Create a Role, Generate a Key Pair, and Sign the Key
Define an SSH role in Vault. This role controls which users can request signed certificates and sets the TTL (time-to-live) for the certificates:
Example:
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”
Generate an SSH Key Pair and Sign the Key
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_demo
Sign the key for the ansible
user:
vault write -field=signed_key ssh/sign/dev-role \
public_key=@/home/darnell/.ssh/id_ed25519_demo.pub \
username=ansible \
> /home/darnell/.ssh/id_ed25519_demo-cert.pub
Set the permissions:
chmod 600 ~/.ssh/id_ed25519_demo
chmod 644 ~/.ssh/id_ed25519_demo-cert.pub
Test SSH access using the signed certificate(s):
ssh -i ~/.ssh/id_ed25519_demo -i ~/.ssh/id_ed25519_demo-cer