SSH certificates

SSH certificates setup

Last updated
PKI SSH setups for complex clusters or virtual guests should be a norm, one which improves security, but also manageability. With a scripted setup, automated key rotations come as a bonus.

Following an explanatory post on how to use SSH within Public-key Infrastructure (PKI), here is an example how to deploy it within almost any environment. Primary candidates are virtual guests, but of course also hosts, including e.g. Proxmox VE cluster nodes as those appear as if completely regular hosts from SSH perspective out-of-the-box (without obscure command-line options added) even when clustered - ever since the SSH host key bugfix.

Roles and Parties

There will be 3 roles mentioned going forward, the terms as universally understood:

  • Certification Authority (CA) which will distribute its public key (for verification of its signatures) and sign other public keys (of connecting users and/or hosts being connected to);
  • Control host from which connections are meant to be initiated by the SSH client or the respective user - which will have their public key signed by a CA;
  • Target host on which incoming connections are handled by the SSH server and presenting itself with public host key equally signed by a CA.

Combined roles and parties

Combining roles (of a party) is possible, but generally always decreases the security level of such system.

Important

It is entirely administrator-dependent where which party will reside, e.g. a CA can be performing its role on a Control host. Albeit less than ideal - complete separation would be much better - any of these setups are already better than a non-PKI setup.

One such controversial is combining a Control and Target into one - an architecture under which Proxmox VE falls under with its very philosophy of being able to control any host of the cluster (and guests therein), i.e. a Target, from any other node, i.e. an architecture without a designated Control host.

Tip

More complex setup would go the opposite direction and e.g. split CAs, at least one for signing Control user keys and another for Target host keys. That said, absolutely do AVOID combining the role of CA and a Target. If you have to combine Control and a Target, attempt to do so with a select one only - a master, if you will.

Example scenario

For the sake of simplicity, we assume one external Control party which doubles as a sole CA and multitude of Targets. This means performing signing of all the keys in the same environment as from which the control connections are made. A separate setup would only be more practical in an automated environment, which is beyond scope here.

Ramp-up

Further, we assume a non-PKI starting environment, as that is the situation most readers will begin with. We will intentionally - more on that below - make use of the previously described setup of strict SSH approach,   but with a lenient alias. In fact, let’s make two, one for secure shell ssh   and another for secure copy scp   (which uses ssh):

cat >> ~/.ssh/config <<< "StrictHostKeyChecking yes"

alias blind-ssh='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
alias blind-scp='scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'

Blind connections

Ideally, blind connections should NOT be used, not even for the initial setup. It is explicitly mentioned here as an instrumental approach to cover two concepts:

  • blind-ssh as a pre-PKI setup way of executing a command on a target, i.e. could be instead done securely by performing the command on the host’s console, either physical or with an out-of-band access, or should be part of installation and/or deployment of such host to begin with;

  • blind-scp as an independent mechanism of distributing files across, i.e. shared storage or manual transfer could be utilised instead.

If you already have a secure environment, regular ssh and scp should be simply used instead. For virtual hosts, execution of commands or distribution of files should be considered upon image creation already.

Root connections

We abstract from privilege considerations by assuming any connection to a Target is under the root user. This may appear (and actually is) ill-advised, but is unfortunately a standard Proxmox VE setup and CANNOT be disabled without loss of feature set. Should one be considering connecting with non-privileged users, further e.g. sudo setup needs to be in place, which is out of scope here.

Setup

Certification Authority key

We will first generate CA’s key pair in a new staging directory. This directory can later be completely dismantled, but of course the CA key should be retained elsewhere then.

(umask 077; mkdir ~/stage)
cd ~/stage

ssh-keygen -t ed25519 -f ssh_ca_key -C "SSH CA Key"

Warning

From this point on, the ssh_ca_key is the CA’s private (signing) key and ssh_ca_key.pub the corresponding public key. It is imperative to keep the private key as secure as possible.

Control key

As our CA resides on the Control host, we will right away create a user key and sign it:

Tip

We are marking the certificate with validity of 14 days (-V option), you are free to adjust or omit it.

ssh-keygen -f ssh_control_key -t ed25519 -C "Control User Key"
ssh-keygen -s ssh_ca_key -I control -n root -V +14d ssh_control_key.pub

We have just created user’s private key ssh_control_key, respective public key ssh_control_key.pub and in turn signed it by the CA creating a user certificate ssh_control_key-cert.pub.

Tip

At any point, a certificate can be checked for details, like so:

ssh-keygen -L -f ssh_control_key-cert.pub

Target keys

We will demonstrate setting up a single Target host for connections from our Control host/user. This has to be repeated (automated) for as many targets as we wish to deploy. For the sake of convenience, consider the following script (interleaved with explanations), which assumes setting Target’s hostname or IP address into the TARGET variable:

TARGET=<host or address>

Sign host key for target

First, we will generate identity and principals (concepts explained previously) for our certificate that we will be issuing for the Target host, we can also do this manually, but running e.g. hostname   command remotely and concatenating its comma-delimited outputs for -s, -f and -I switches allow us to list the hostname, the FQDN and the IP address all as principals without any risk of typos.

IDENT=`blind-ssh root@$TARGET "hostname"`
PRINC=`blind-ssh root@$TARGET "(hostname -s; hostname -f; hostname -I) | xargs -n1 | paste -sd,"`

We will now let the remote Target itself generate its new host key (in addition to whichever it already had prior, so as not to disrupt any other parties) and copy over its public key to the control for signing by the CA.

Important

This demonstrates a concept which we will NOT abandon: Never transfer private keys. Not even over secure connections, not even off-band. Have the parties generate them locally and only transfer out the public key from the pair for signing, as in our case, by the CA.

Obviously, if you are generating new keys at the point of host image inception - as would be preferred, this issue is non-existent.

Note that we are NOT setting any validity period on the host key, but we are free to do so as well - if we are ready to consider rotations further down the road.

blind-ssh root@$TARGET "ssh-keygen -t ed25519 -f /etc/ssh/ssh_managed_host_key"
blind-scp root@$TARGET:/etc/ssh/ssh_managed_host_key.pub .

Now with the Target’s public host key on the Control/CA host, we sign it with the affixed identity and principals as previously populated and simply copy it back over to the Target host.

ssh-keygen -s ssh_ca_key -h -I $IDENT -n $PRINC ssh_managed_host_key.pub
blind-scp ssh_managed_host_key-cert.pub root@$TARGET:/etc/ssh/

Configure target

The only thing left is to configure Target host to trust users that had their keys signed by our CA.

We will append our CA’s public key to the remote Target host’s list of (supposedly all pre-existing) trusted CAs that can sign user keys.

blind-ssh root@$TARGET "cat >> /etc/ssh/ssh_trusted_user_ca" < ssh_ca_key.pub

Still on the Target host, we create a new (single) partial configuration file which will simply point to the new host key, the corresponding certificate and the trusted user CA’s key record:


blind-ssh root@$TARGET "cat > /etc/ssh/sshd_config.d/pki.conf" << EOF
HostKey /etc/ssh/ssh_managed_host_key
HostCertificate /etc/ssh/ssh_managed_host_key-cert.pub
TrustedUserCAKeys /etc/ssh/ssh_trusted_user_ca
EOF

All that is left to do is to apply the new setup by reloading the SSH daemon:

blind-ssh root@$TARGET "systemctl reload-or-restart sshd"

First connection

There is a one-off setup of Control configuration needed first (and only once) - we set our Control user to recognise Target host keys when signed by our CA:

cat >> ~/.ssh/known_hosts <<< "@cert-authority * `cat ssh_ca_key.pub`"

We could now test our first connection with the previously signed user key, without being in the blind:

ssh -i ssh_control_key -v root@$TARGET

Tip

Note we have referred directly to our identity (key) we are presenting with via the -i client option, but also added in -v for verbose output this one time.

And we should be right in, no prompts about unknown hosts, no passwords. But for some more convenience, we should really make use of client configuration.

First, let’s move the user key and certificate into the usual directory - as we are still in the staging one:

mv ssh_control_key* ~/.ssh/

Now the full configuration for host which we will simply alias as h1:

cat >> ~/.ssh/config << EOF
Host t1
    HostName $TARGET
    User root
    Port 22
    IdentityFile ~/.ssh/ssh_control_key
    CertificateFile ~/.ssh/ssh_control_key-cert.pub
EOF

Tip

The client configuration   really allows for a lot of convenience, e.g. with its staggered setup it is possible to only define some of the options and then others shared by multiple hosts further down with wildcards, such as Host *.node.internal. Feel free to explore and experiment.

From now on, our connections are as simple as:

ssh t1

Rotation

If you paid attention, we used an example of generating user key signed only for a specified period, after which it would be failing. It is very straightforward to simply generate a new one any time and sign it without having to change anything further on the targets anymore - especially on our model setup where CA is on the Control host.

If you wish to also rotate Target host key, while more elaborate, this is now trivial - the above steps for the Target setup specifically (combined into a single script) will serve just that purpose.

Tip

There’s one major benefit to the above approach. Once the setup has been with PKI in mind, rotating even host keys within the desired period, i.e. before they expire, must then just work WITHOUT use of the blind- aliases using regular ssh and scp invocations. And if they do not, that’s a cause for investigation - of such rotation script failing.

Troubleshooting

If troubleshooting, the client ssh from the Control host can be invoked with multiple -v, e.g. -vvv for more detailed output which will produce additional debug lines prepended with debug and numberical designation of the level. On a successful certificate based connection, both user and host, we would want to see some of the following:

debug3: record_hostkey: found ca key type ED25519 in file /root/.ssh/known_hosts:1
debug3: load_hostkeys_file: loaded 1 keys from 10.10.10.10
debug1: Server host certificate: ssh-ed25519-cert-v01@openssh.com SHA256:JfMaLJE0AziLPRGnfC75EiL4pxwFNmDWpWT6KiDikQw, serial 0 ID "pve" CA ssh-ed25519 SHA256:sJvDprmv3JQ2n+9OeqnvIdQayrFFlxX8/RtzKhBKXe0 valid forever
debug2: Server host certificate hostname: pve
debug2: Server host certificate hostname: pve.lab.internal
debug2: Server host certificate hostname: 10.10.10.10
debug1: Host '10.10.10.10' is known and matches the ED25519-CERT host certificate.

debug1: Will attempt key: ssh_control_key ED25519-CERT SHA256:mDucgr+IrmNYIT/4eEIVjVNnN0lApBVdDgYrVDqyrKY explicit
debug1: Offering public key: ssh_control_key ED25519-CERT SHA256:mDucgr+IrmNYIT/4eEIVjVNnN0lApBVdDgYrVDqyrKY explicit
debug1: Server accepts key: ssh_control_key ED25519-CERT SHA256:mDucgr+IrmNYIT/4eEIVjVNnN0lApBVdDgYrVDqyrKY explicit

In case of need, the Target (server-side) log can be checked with journalctl -u ssh, or alternatively journalctl -t sshd.

Final touch

One of the last pieces of advice for any well set up system would be to eventually prevent root SSH connections altogether, even with key, even with a signed one - there is the PermitRootLogin   that can be set to no. This would, however cause Proxmox VE to fail. The second best option is to prevent root connections with a password, i.e. only allowing a key. This is covered by the value prohibit-password that comes with stock Debian (but NOT Proxmox VE) install, however - be aware of the remaining bug that could cause you getting cut off with passwordless root before doing so.


Manual pages:  hostnamepastesshssh-keygensshdumaskxargs
Also available as an irregularly updated GitHub Gist.
Excuse limited formatting, absent referencing and missing rich content.
Your feedback is welcome in comments therein.