SSH Certificates
Table of contents
SSH authentication methods
There are two popular ways to SSH into a server.
- Use a system password
- Use SSH key
Using an SSH key adds more security layers compared to using a system password.
However, even with an SSH key, there still exists a problem that once an SSH key is placed on the authorized_keys
of the server, it is permanent unless someone actively removes it.
This may not matter if you’re the one and only person trying to SSH into a server, but if the number of people that need to be given or revoked access increases, this can become a hassle.
Using SSH certificates can alleviate these issues.
SSH certificates
SSH certificate authentication goes in both directions. The server/host presents its certificate to the user/client, and vice versa.
The whole idea of a certificate is simple:
- Both parties agree on some authority and trust what it signs.
Such authority is called a Certificate Authority (CA).
Technically, our CA is just a pair of cryptographic keys.
So instead of trusting each user keys, parties will decide trust the CA that will sign those keys. This eliminates having to accumulate public keys in authorized_keys
.
In addition, CAs can set a expiration date on their signatures.
So if you decide to give someone a server access for only a single day, but don’t want to bother remembering to come back after a day to remove his/her key from your authorized_keys
, you can have the CA sign the key to only be valid for a day.
In the following example, I will be using Vault to manage these CAs.
Host certificate
Usually people dismiss the need for host certificates. However it is a good security layer to prevent SSHing into a bad machine.
When you’re SSHing without a certificate, you’ve probably seen something like this.
$ ssh server
The authenticity of host 'badsiteindisguise.com' can't be established.
...cryptographic key fingerprint...
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Usually you just end up typing yes, which adds the fingerprint to ~/.ssh/known_hosts
and you never see the prompt again.
However, just like the prompt says, are you sure
that this site can be trusted? Are you sure that there wasn’t a man in the middle that redirected you to one of his bad machines?
By placing a trusted host CA’s public key on the client before the first SSH, this security risk can be avoided.
Configure a host CA
Create a key pair for host CA:
ssh-keygen -t ed25519 -C "hostca" -f hostca
Which produces hostca
and hostca.pub
.
vault write ssh-host-signer/config/ca generate_signing_key=true
currently only generates rsa
keys, which is deprecated in newer OpenSSH. To use different crypto algorithms such as ed25519
, you have to generate one and upload it to Vault.
Mount the SSH secrets engine on Vault:
vault secrets enable -path=ssh-host-signer ssh
The path
can be anything you like, just make sure you are logged in to Vault and has the policy to read/write to that path.
Upload the CA key pair:
vault write ssh-host-signer/config/ca \
private_key=@hostca \
public_key=@hostca.pub
The @
points to the file. Use "..."
to copy and paste the values.
Extend the host key certificate TTL (time-to-live):
vault secrets tune -max-lease-ttl=87600h ssh-host-signer
87600h
is 10 years.
Create a role to sign host keys:
vault write ssh-host-signer/roles/hostrole \
key_type=ca \
ttl=87600h \
allow_host_certificates=true \
allowed_domains="example.com,something.com" \
allow_bare_domains=true \
allow_subdomains=true
Vault uses these roles to sign keys.
The above configuration basically says it can sign host certificates for domains of example.com
, *.example.com
, something.com
, *.something.com
, and the certificate will be valid for 10 years.
Sign the host key with CA
Sign and save the resulting certificate on the server:
vault write -field=signed_key ssh-host-signer/sign/hostrole \
cert_type=host \
public_key=@/etc/ssh/ssh_host_ed25519_key.pub > /etc/ssh/ssh_host_ed25519_key-cert.pub
Optionally set permission on the certificate:
sudo chmod 0640 /etc/ssh/ssh_host_ed25519_key-cert.pub
If the host key doesn’t exist, create one using ssh-keygen
.
Update /etc/ssh/sshd_config
:
HostKey /etc/ssh/ssh_ed25519_key
HostCertificate /etc/ssh/ssh_ed25519_key-cert.pub
Save host CA public key on client
From the client, get the public key of ssh-host-signer
CA:
# If using API endpoint
curl <vault-api-url>/v1/ssh-host-signer/public-key
# If client has direct access to the Vault server
vault read -field=public_key ssh-host-signer/config/ca
Save the result to client’s ~/.ssh/known_hosts
:
@cert-authority *.example.com,*.something.com ssh-ed25519 ...
If you have already logged in to the server before the host certificate was set up, remove the corresponding fingerprint in known_hosts
.
Try SSHing to the server with a password or a regular public key.
Assuming you have never SSHed to the server before or have removed the previous fingerprint, SSH should not show you the Are you sure you want to continue
prompt.
If it does, the host certificate is not set up correctly.
Client certificate
Instead of having the client’s public key saved to host’s authorized_keys
, we will have the client use a certificate to authenticate.
Some of the process is actually very similar to above. Mostly the only difference is to use client
or user
instead of host
.
Configure a client CA
Create a key pair for client CA:
ssh-keygen -t ed25519 -C "clientca" -f clientca
Which produces clientca
and clientca.pub
.
You can actually use the same pair of keys you used for host CA.
Mount the SSH secrets engine on Vault:
vault secrets enable -path=ssh-client-signer ssh
Upload the CA key pair:
vault write ssh-client-signer/config/ca \
private_key=@clientca \
public_key=@clientca.pub
Create a role to sign client keys:
vault write ssh-client-signer/roles/clientrole -<<"EOH"
{
"allow_user_certificates": true,
"allowed_users": "my-user",
"allowed_extensions": "permit-pty,permit-port-forwarding,permit-x11-forwarding,permit-agent-forwarding,permit-user-rc",
"default_extensions": [
{
"permit-pty": ""
}
],
"key_type": "ca",
"default_user": "my-user",
"ttl": "30m0s"
}
EOH
allowed_users
: Comma separated list of allowed usernameallowed_extensions
: Comma separated list of extensions that client can request in their certificatedefault_extensions
: Default extensions given when this role signs a certificatedefault_user
: Username to use when one isn’t specifiedttl
: Client certificate expires afterttl
.
Sign the client key with CA
Create an SSH key if one doesn’t already exist:
ssh-keygen -t ed25519 -C "user@example.com" -f client_key
Sign and save the resulting certificate on the client:
- To accept default
vault write -field=signed_key ssh-client-signer/sign/clientrole \
public_key=@client_key.pub > client_key-cert.pub
- To customize
vault write ssh-client-signer/sign/my-role -<<"EOH"
{
"public_key": "ssh-ed25519 ...",
"valid_principals": "my-user",
"extensions": {
"permit-pty": "",
"permit-port-forwarding": ""
}
}
EOH
Then copy and paste the certificate to client_key-cert.pub
.
If your certificate ends in the <same_base>-cert.pub
suffix, OpenSSH will automatically detect it so you won’t have to pass in your certificate as an identity file in addition to the private key.
Save client CA public key on host
From the host, get the public key of ssh-client-signer
CA and save it to /etc/ssh/trusted-user-ca-keys.pem
:
# If using API endpoint
curl -o /etc/ssh/trusted-user-ca-keys.pem http://127.0.0.1:8200/v1/ssh-client-signer/public_key
# If host has direct access to the Vault server
vault read -field=public_key ssh-client-signer/config/ca > /etc/ssh/trusted-user-ca-keys.pem
Now modify /etc/ssh/sshd_config
on host:
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
Host sshd_config
settings
To disable SSH password authentication,
# /etc/ssh/sshd_config
PasswordAuthentication no
ChallengeResponseAuthentication no
Recap:
HostKey /etc/ssh/ssh_ed25519_key
HostCertificate /etc/ssh/ssh_ed25519_key-cert.pub
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
Test connection
Now the client should be able to authenticate to the server with certificates:
# If the certificate ends in '-cert.pub' with the same base name
ssh -i ~/.ssh/client_key my-user@example.com
# If the certificate has a different naming scheme
ssh -i ~/.ssh/client-certificate.pub -i ~/.ssh/client_key my-user@example.com
Connection is a success if you don’t see any fingerprint validation prompt and was able to connect without adding client_key.pub
to host’s authorized_keys
.
Debugging
From the client side
Add -vvv
to get a verbose log output:
ssh -vvv -i ~/.ssh/client_key my-user@example.com
From the host side
Linux
Set the LogLevel
in /etc/ssh/sshd_config
to VERBOSE
.
Then inspect /var/log/auth.log
.
macOS
log show --process sshd --last <num> --debug --info
See log show help
for details.
To check certificate metadata
ssh-keygen -Lf ssh_key-cert.pub
SSH certificate extensions
Some of the basic extensions (names are self explanatory):
permit-pty
: Allow interactive shellpermit-port-forwarding
: Allow SSH tunnelspermit-x11-forwarding
permit-agent-forwarding
permit-user-rc
References: