Secure SSH access with cheap HSM

If you need to manage multiple Linux servers, secure use of ssh on a large scale is a challenge. The most commonly used authentication method (except passwords, of course) is using SSH key pairs. It’s difficult to manage many keys for multiple administrators, and in particular to ensure good private keys security. Using ssh keys for authentication requires copying the public key to every server you need login to. Therefore, there’s a challenge when you need to quickly remove a given public key from all authorized_keys files on all servers in case of suspicion of compromising the private key or even terminating cooperation with a given admin.

One of the solution is to use OpenSSH Certificate Authentication. OpenSSH supports login using special certificates signed by Certificate Authority (CA). Such a certificate contains the public key, identity and validity period. This solution allows installation on any server to which we will log in only one public CA key instead of multiple public keys - for each administrator. The entire user management process can now take place in one place instead of being distributed, as is the case with classic SSH key pairs. This solution is used by major organizations, e.g. Facebook.

Here I will show how to configure a bastion host containing CA, through which you’ll connect to your servers. We’ll solve the problem of secure private key storage with a cheap USB dongle - Yubikey 4 or Yubikey 5. Yubikey provides the Smart Card capabilities based on the PIV (Personal Identity Verification) interface. Supports e.g. RSA 2048, ECC p-256, or ECC p-384.

If our bastion host isn’t a physical machine containing USB ports, then we need to use a solution that allows connecting USB keys via the IP network. We can use USB Dongle Server myUTN series from SEH. They offer solutions that allow connecting 2 to 20 USB ports over an IP network. Connections can be encrypted. Drivers for all major operating systems are available.

Yubikey 5 NFC costs $45 and myUTN-50a is $186, so we can have our “Hardware Security Module” (HSM) for about $231. If you want a cheaper alternative, then you can place a bastion host on a Raspberry PI, to which you will directly connect Yubikey, without any USB dongle server.

It’s an alternative for those who for various reasons cannot or do not want to use HSM cloud solutions, such as AWS Cloud HSH or Google Cloud HSM. Especially if you want to have your private keys under your sole control.

The basic idea

The user logs on to the bastion host. There, it generates a temporary CA certificate using Yubikey. With it, he can log in to the remote host. Such a certificate has a limited validity period (e.g. 10 minutes). This time determines when he can establish a new connection to the target server(s), which is of course not terminated after it has expired. The private key for generating the certificate is created and stored only on Yubikey. It cannot be recovered from it. You can only use it with access to Yubikey. Additional security may be the requirement to provide a PIN code each time the temporary certificate is generated. Incorrect PIN entry three times blocks Yubikey until you use the PUK code.

Bastion host configuration

All the following examples are to be performed on CentOS/RHEL 8 and Ubuntu 19.10. We couldn’t use Ubuntu 18.04 LTS because in version 7.6p1 OpenSSH PKCS#11 interface does not support ECC.

IP addressing in the examples below:

Hostname IP Description
sshca1 10.10.0.10 Bastion Host
utn1 10.10.1.100 USB access through myUTN
server1 10.10.2.20 server1.example.com (host to which we want access)

Installing the SEH Dongle server drivers

If you have Yubikey connected directly to a physical machine (e.g. Raspberry PI) then you don’t need a dongle server.

Drivers can be found here. An SEH Dongle server requires configuration (access settings, IP addresses, etc.). This can be done via the web interface. Instructions are available on the drivers download page above.

There are four installation packages: driver, service, clitool (command line interface tool ‘utnm’) and manager (graphical user interface). We need to install the first three: driver, service and clitool. At the time of writing, the latest SEH driver version was 3.2.2.

Installation on CentOS/RHEL 8:

sudo dnf install epel-release
sudo dnf install kernel-devel make dkms unzip usbutils

wget https://www.seh-technology.com/fileadmin/user/downloads/deviceserver/tools/sehutnmanager-rpm_linux_64bit-3.2.2.zip
unzip ./sehutnmanager-rpm_linux_64bit-3.2.2.zip
cd sehutnmanager-rpm_linux_64bit-3.2.2
sudo dnf install ./seh-utn-driver-3.2.2-1dkms.noarch.rpm
sudo dnf install ./seh-utn-service-3.2.2-1.x86_64.rpm
sudo dnf install ./seh-utn-clitool-3.2.2-1.x86_64.rpm

Installation on Ubuntu 19.10:

sudo apt-get install linux-headers-`uname -r`
sudo apt-get install dkms unzip

wget https://www.seh-technology.com/fileadmin/user/downloads/deviceserver/tools/sehutnmanager-deb_linux_64bit-3.2.2.zip
unzip ./sehutnmanager-deb_linux_64bit-3.2.2.zip
cd sehutnmanager-deb_linux_64bit-3.2.2/
sudo dpkg -i ./seh-utn-driver_3.2.2-1dkms_all.deb
sudo dpkg -i ./seh-utn-service_3.2.2-1_amd64.deb
sudo dpkg -i ./seh-utn-clitool_3.2.2-1_amd64.deb

We assume that you have given the IP address 10.10.1.100 for USB access in myUTN and put Yubikey on port 1 myUTN. To check connectivity (you can wait a few moments for the answer), type:

sudo utnm -c "getlist 10.10.1.100"
Port VID    PID    Manufacturer                  Product
---- ------ ------ ----------------------------- -----------------------------
1    0x1050 0x0407 Yubico                        Yubikey 4 OTP+U2F+CCID


Port State           Owner                     Class Port name
---- --------------- ------------------------- ----- -------------------------
1    Not activated                             0x00

If you see Yubikey on the list, you can activate it on port 1, which will connect it to our Linux system:

sudo utnm -c "activate 10.10.1.100 1"

After waiting a few seconds, let’s check if Yubikey is visible on our Linux. Yubico USB Vendor ID is 1050:

[pawel@sshca1 ~]$ lsusb | grep 1050:
Bus 001 Device 003: ID 1050:0407 Yubico.com Yubikey 4 OTP+U2F+CCID

If you have Yubikey visible in the system, you can activate USB port permanently and restart server. After reboot you need to check (lsusb), whether Yubikey is visible.

sudo utnm -c "set autoconnect=true 10.10.1.100 1"
sudo reboot

Generation of private key and certificates on Yubikey

The Yubikey Series 4 and 5 have the PIV subsystem (FIPS 201 US government standard) provided RSA or ECC sign/encrypt operations using a private key stored on a built-in smartcard.

  1. Install the necessary packages

CentOS/RHEL 8:

At the time of writing, the yubico-piv-tool package for the EL8 family was in preparation. In case of unavailability in the official EPEL repository, you can download it from: https://copr.fedorainfracloud.org/coprs/jjelen/yubico-piv-tool/

sudo dnf install epel-release
sudo dnf install opensc yubico-piv-tool

CentOS/RHEL 8 requires you to define which non-root user or group will have access to the smartcard. So create the ssh_admins group and allow its members access to the smartcard via the pcsc interface:

sudo groupadd ssh_admins
sudo usermod -a -G ssh_admins pawel


sudo cat > /usr/share/polkit-1/rules.d/sssd-pcsc.rules << 'ENDFILE'
// access for smart card for ssh_admins group

polkit.addRule(function(action, subject) {
    if (action.id == "org.debian.pcsc-lite.access_card" &&
        subject.isInGroup ("ssh_admins")) {
            return polkit.Result.YES;
    }
});

polkit.addRule(function(action, subject) {
    if (action.id == "org.debian.pcsc-lite.access_pcsc" &&
        subject.isInGroup ("ssh_admins")) {
            return polkit.Result.YES;
    }
});

ENDFILE

Restart server (or only polkit and pcscd services)

Ubuntu 19.10:

sudo apt-get install opensc yubico-piv-tool
  1. Check the connectivity through the pcsc interface:
[pawel@sshca1 ~]$ opensc-tool --list-readers
# Detected readers (pcsc)
Nr.  Card  Features  Name
0    Yes             Yubico Yubikey 4 OTP+U2F+CCID 00 00
  1. Check connectivity in yubico-piv-tool. If you don’t have any Yubikey certificates, you’ll see:
[pawel@sshca1 ~]$ yubico-piv-tool -a status
Version:        4.3.7
Serial Number:  5001234
CHUID:  No data available
CCC:    No data available
PIN tries left: 3
  1. To prevent C_Sign error during the generation of certificates using the pcsc libraries> = 0.18, as a temporary solution, add or uncomment in opensc.conf: pin_cache_ignore_user_consent = true; This should be placed in the ‘framework pkcs15’ section. The problem was only observed on CentOS/RHEL 8. On Ubuntu 19.10 you can skip this step.
[pawel@sshca1 ~]$ cat /etc/opensc.conf
app default {
        # debug = 3;
        # debug_file = opensc-debug.txt;
        framework pkcs15 {
                # use_file_caching = true;
                pin_cache_ignore_user_consent = true;
        }
        reader_driver pcsc {
                # The pinpad is disabled by default,
                # because of many broken readers out there
                enable_pinpad = false;
        }
}

Reset Yubikey PIV subsystem

If there is a need to reset the PIV system on Yubikey (e.g. there are some certificates, or there is a custom PIN/PUK), then we follow the instructions below. The standard PIN is 123456, PUK is 12345678 and the MGMT_KEY is 010203040506070801020304050607080102030405060708. PIV reset requires entering incorrect PIN and PUK values (i.e. only 0 digits) three times. Do not pay attention to messages like “Pin verification failed” when following the instructions below.

yubico-piv-tool -averify-pin -P000000
yubico-piv-tool -averify-pin -P000000
yubico-piv-tool -averify-pin -P000000
yubico-piv-tool -achange-puk -P000000 -N0000000
yubico-piv-tool -achange-puk -P000000 -N0000000
yubico-piv-tool -achange-puk -P000000 -N0000000
yubico-piv-tool -areset

Setting new PIN, PUK and MGMT_KEY

Generate and insert new PIN, PUK and MGMT_KEY codes into the ykeys.txt file in the temporary yubico directory. It’s best to use random values, but you can also set your own. MGMT_KEY will be needed once to authenticate during CA certificate generation.

mkdir -p ~/yubico
echo mgmt_key=`dd if=/dev/urandom bs=1 count=24 2>/dev/null | hexdump -v -e '/1 "%02X"'` > ~/yubico/ykeys.txt
echo pin=`dd if=/dev/urandom bs=1 count=6 2>/dev/null | hexdump -v -e '/1 "%u"' | cut -c1-6` >> ~/yubico/ykeys.txt
echo puk=`dd if=/dev/urandom bs=1 count=6 2>/dev/null | hexdump -v -e '/1 "%u"' | cut -c1-8` >> ~/yubico/ykeys.txt

After generation, the sample codes look like this:

[pawel@sshca1 ~]$ cat ~/yubico/ykeys.txt
mgmt_key=471DAF7AD19C9CF61D6979457357D8D3B5449DB37DABADE4
pin=219205
puk=64802189

Now set Yubikey with generated codes. We assume that PIN and PUK have default values:

source ~/yubico/ykeys.txt
yubico-piv-tool -a set-mgm-key -n $mgmt_key
yubico-piv-tool -k $key -a change-pin -P 123456 -N $pin
yubico-piv-tool -k $key -a change-puk -P 12345678 -N $puk

Generation of private keys and CA certificate on Yubico

Yubico PIV interface provides RSA or ECC sign/decrypt operations using a private key stored on the smartcard. Supports key sizes of RSA 2048 or ECC p-256, or ECC p-384. We’ll generate a ECC p-384 keypair. The private key stays on Yubikey, and the public key is saved to the file ~/yubico/ca-pubkey.pem:

source ~/yubico/ykeys.txt
yubico-piv-tool -s 9c -A ECCP384 -a generate -k$mgmt_key -o ~/yubico/ca-pubkey.pem

You’ll now generate a new x509 ECC p-384 certificate for our CA valid for 10 years based on a previously generated key pair and save to file ~/yubico/ssh-x509-cert.pem. You have to provide the PIN code (e.g. 219205) that you generated earlier:

ca_subj="/C=PL/ST=LD/L=Lodz/O=Secure Access Inc./OU=IT/CN=ssh_ca/"
yubico-piv-tool -a verify-pin -a selfsign-certificate -s 9c -S "$ca_subj" --valid-days 3650 -i ~/yubico/ca-pubkey.pem -o ~/yubico/ssh-x509-cert.pem

Import the generated CA certificate into Yubikey:

yubico-piv-tool -k$mgmt_key -a import-certificate -s 9c < ~/yubico/ssh-x509-cert.pem

Check the PIV content of our Yubikey. The CA certificate is visible now in slot 9c:

[pawel@sshca1 ~]$ yubico-piv-tool -a status
Version:        4.3.7
Serial Number:  5001234
CHUID:  No data available
CCC:    No data available
Slot 9c:
        Algorithm:      ECCP384
        Subject DN:     C=PL, ST=LD, L=Lodz, O=Secure Access Inc., OU=IT, CN=ssh_ca
        Issuer DN:      C=PL, ST=LD, L=Lodz, O=Secure Access Inc., OU=IT, CN=ssh_ca
        Fingerprint:    b812ee13f6acf78e5a91fd2c36bc230a40fbc87f0f1ba1d4a9f2827929d19979
        Not Before:     Feb  6 12:24:00 2020 GMT
        Not After:      Feb  3 12:24:00 2030 GMT
PIN tries left: 3

Now generate the ssh public key in RFC4716 format (required by OpenSSH) for the previously generated CA certificate and save it to the file ~/yubico/ssh-ca.pub. Use the PKCS11 interface to access Yubikey. Check the location of the opensc-pkcs11.so libraries beforehand.

CentOS/RHEL 8:

ssh-keygen -D /usr/lib64/opensc-pkcs11.so -e >> ~/yubico/ssh-ca.pub

Ubuntu 19.10:

ssh-keygen -D /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so -e >> ~/yubico/ssh-ca.pub

You now have the ssh ca public key:

[pawel@sshca1 ~]$ cat ~/yubico/ssh-ca.pub
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBP/tkJYihhSactl9dmhBwY6xz3bEohmGBr40RL8ZZiHyFJZKsdoqPdvNfVLx/Uz4wbHAlzwE1EyGN63V7l2OP9jZrRL+nKZriE4zkq3a/kiyjkfKnb6juws/tEn68wXf1g== pkcs11:id=%02;object=SIGN%20pubkey;token=ssh_ca;manufacturer=piv_II?module-path=/usr/lib64/opensc-pkcs11.so

For identification purposes, you can add or change the public key label generated by ssh-keygen to one containing the Yubikey serial number (e.g. Y5001234). CentOS/RHEL ssh-keygen adds label “pkcs11:id=%02;object=SIGN%20pubkey;token=ssh_ca;manufacturer=piv_II?module-path=/usr/lib64/opensc-pkcs11.so”, and Ubuntu doesn’t add any.
Adding a label can be useful if you have more than one bastion server, each with its own dedicated Yubikey.

You will copy ssh-ca.pub file to every host you want to access.

[pawel@sshca1 ~]$ cat ~/yubico/ssh-ca.pub
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBP/tkJYihhSactl9dmhBwY6xz3bEohmGBr40RL8ZZiHyFJZKsdoqPdvNfVLx/Uz4wbHAlzwE1EyGN63V7l2OP9jZrRL+nKZriE4zkq3a/kiyjkfKnb6juws/tEn68wXf1g== Y5001234

Save the contents of the ~/yubico/ykeys.txt file to a safe place and delete the file itself for safety. The PIN code is best to remember - it will always be used when accessing remote hosts via ssh. The PUK code will be used if the PIN code is incorrectly used three times.

Secure file deletion:

shred -u ~/yubico/*.txt
shred -u ~/yubico/*.pem

Preparing a remote server for access from the bastion host

  1. Copy the file ~/yubico/ssh-ca.pub from the bastion host to the target server (server1.example.com):
#commands executed on the bastion host
scp ~/yubico/ssh-ca.pub server1.example.com:~
  1. Log in to the remote server (eg server1.example.com). Then copy the ssh-ca.pub file to the /etc/ssh/ directory, add the line with TrustedUserCAKeys to the /etc/ssh/sshd_config file and restart openssh:
#commands executed on the remote server
sudo mv ~/ssh-ca.pub /etc/ssh/
sudo chown 0:0 /etc/ssh/ssh-ca.pub
sudo echo TrustedUserCAKeys /etc/ssh/ssh-ca.pub >> /etc/ssh/sshd_config
sudo systemctl restart sshd

Establishing an ssh connection to a target server from the bastion host

You will use the ed25519 key signed with the CA ECC p384 certificate to log in.

  1. Generate ed25519 key pair (once)
[pawel@sshca1 ~]$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/pawel/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/pawel/.ssh/id_ed25519.
Your public key has been saved in /home/pawel/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:RgDRmUO4J6DOHuUfzmIBYJxn/Vwmkz1fV1TDwswucGo pawel@sshca1
The key's randomart image is:
+--[ED25519 256]--+
|. . +*ooo   + .o*|
|.+.o..== * . * o.|
|o.o. .o.* * o o  |
|.. .o .+ E o .   |
|o +  o  S   .    |
| + o . .         |
|. . = .          |
| . o +           |
|  . .            |
+----[SHA256]-----+
  1. Read the CA public key from Yubikey to the file ~/.ssh/ssh-ca-local.pub. It has the same content as ssh-ca.pub, but can contain only one entry for a connected Yubikey. (once)

CentOS/RHEL 8:

ssh-keygen -D /usr/lib64/opensc-pkcs11.so -e > ~/.ssh/ssh-ca-local.pub

Ubuntu 19.10:

ssh-keygen -D /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so -e > ~/.ssh/ssh-ca-local.pub
  1. Generate a temporary certificate for remote ssh access

Before each remote login you will generate a temporary certificate, valid for a short period of time (e.g. 10 minutes), which will use your private key ed25519 signed by CA ECC p384 certificate. During the certificate validity period you will be able to log in to all hosts where you have added the CA public key (in the TrustedUserCAKeys parameter). In the following example:

  • -I (certificate_identity) parameter contains the login name of the current user (e.g. pawel)
  • -n (principals) containst one or more principals to be included in a certificate (in this case - root)
  • -V (validity_interval) 10 minutes
  • -z (serial_number) generation time in the yymmddhhmmss format

You must provide the PIN when generating the certificate. The following command generates a temporary certificate to the ~/.ssh/id_ed25519-cert.pub file.

CentOS/RHEL 8:

[pawel@sshca1 ~]$ ssh-keygen -D /usr/lib64/opensc-pkcs11.so -s ~/.ssh/ssh-ca-local.pub -I $USER -n root -V +10m -z $(date +%y%m%d%H%M%S) ~/.ssh/id_ed25519.pub
Enter PIN for 'ssh_ca':
Signed user key /home/pawel/.ssh/id_ed25519-cert.pub: id "pawel" serial 200206144748 for root valid from 2020-02-06T14:46:00 to 2020-02-06T14:57:48

Ubuntu 19.10:

pawel@sshca1:~$ ssh-keygen -D /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so -s ~/.ssh/ssh-ca-local.pub -I $USER -n root -V +10m -z $(date +%y%m%d%H%M%S) ~/.ssh/id_ed25519.pub
Enter PIN for 'ssh_ca': 
Signed user key /home/pawel/.ssh/id_ed25519-cert.pub: id "pawel" serial 200206144748 for root valid from 2020-02-06T14:46:00 to 2020-02-06T14:57:48

Let’s watch the content of the generated certificate:

[pawel@sshca1 ~]$ ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
/home/pawel/.ssh/id_ed25519-cert.pub:
        Type: [email protected] user certificate
        Public key: ED25519-CERT SHA256:RgDRmUO4J6DOHuUfzmIBYJxn/Vwmkz1fV1TDwswucGo
        Signing CA: ECDSA SHA256:7ZYK5ggHjyrD/cxOcp4orbKfZCh7xgQhcga6oE0/QXk (using ecdsa-sha2-nistp384)
        Key ID: "pawel"
        Serial: 200206144748
        Valid: from 2020-02-06T14:46:00 to 2020-02-06T14:57:48
        Principals:
                root
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc
  1. Finally, log in to the target host as root (principal included in the certificate):
[pawel@sshca1 .ssh]$ ssh server1.example.com -l root
Last login: Thu Feb  6 14:49:25 2020 from 10.10.0.10
[root@server1 ~]#

The sshd log on the target server shows ID (pawel), serial (200206144748), bastion host’s IP address (10.10.0.10):

Feb  6 14:51:05 lab8a sshd[1716]: Accepted publickey for root from 10.10.0.10 port 39858 ssh2: ED25519-CERT SHA256:RgDRmUO4J6DOHuUfzmIBYJxn/Vwmkz1fV1TDwswucGo ID pawel (serial 200206144748) CA ECDSA SHA256:7ZYK5ggHjyrD/cxOcp4orbKfZCh7xgQhcga6oE0/QXk

If the validity period of the id_ed25519-cert.pub has expired, using the ssh -l root server1.example.com command will take you to the next authentication method - e.g. password prompt. Then we need to generate a new temporary certificate.

Final thoughts

It is critical to ensure the bastion host’s security. It’s definitely worth using multi-factor authentication and good event logging system. This is obvious. Next, allow to use ssh connections on each remote server only from the bastion host(s).
You can run more bastion hosts for high availability. Each connected to its own Yubikey. The distributed ssh-ca.pub file can contain more entries - one line for each CA.