Kushal Das

FOSS and life. Kushal Das talks here.


Using your OpenPGP key on Yubikey for ssh

Last week I wrote about how you can generate ssh keys on your Yubikeys and use them. There is another way of keeping your ssh keys secure, that is using your already existing OpenPGP key (along with authentication subkey) on a Yubikey and use it for ssh.

In this post I am not going to explain the steps on how to move your key to a Yubikey, but only the steps required to start using it for ssh access. Feel free to have a look at Tumpa if you want an easy way to upload keys to your card.

Enabling gpg-agent for ssh

First we have to add gpg-agent.conf file with correct configuration. Remember to use a different pinentry program if you are on Mac or KDE.

❯ echo "enable-ssh-support" >> ~/.gnupg/gpg-agent.conf
❯ echo "pinentry-program $(which pinentry-gnome)" >> ~/.gnupg/gpg-agent.conf
❯ echo "export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)" >> ~/.bash_profile
❯ source ~/.bash_profile 
❯ gpg --export-ssh-key <KEYID> > ~/.ssh/id_rsa_yubikey.pub

At this moment your public key (for ssh usage) is at ~/.ssh/id_rsa_yubikey.pub file. You can use it in the ~/.ssh/authorized_keys file on the servers as required.

We can then restart the gpg-agent using the following command and then also verify that the card is attached and gpg-agent can find it.

❯ gpgconf --kill gpg-agent
❯ gpg --card-status

Enabling touch policy on the card

We should also enable touch policy on the card for authentication operation. This means every time you will try to ssh using the Yubikey, you will have to touch the interface (it will be flashing the light till you touch it).

❯ ykman openpgp keys set-touch aut On
Enter Admin PIN: 
Set touch policy of authentication key to on? [y/N]: y

If you still have servers where you have only the old key, ssh client will be smart enough to ask you the passphrase for those keys.

ssh authentication using FIDO/U2F hardware authenticators

From OpenSSH 8.2 release it supports authentication using FIDO/U2F. These tokens are required to implement the ECDSA-P256 "ecdsa-sk" key type, but some (say Yubikey) also supports Ed25519 (ed25519-sk) keys. In this example I am using a Yubikey 5.

I am going to generate a non-discoverable key on the card itself. Means along with the card, we will also have a key on disk, and one will need both to authenticate. If someone steals you Yubikey, they will not be able to login just via that.

✦ ❯ ssh-keygen -t ed25519-sk -f .ssh/id_ed25519_sk
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in .ssh/id_ed25519_sk
Your public key has been saved in .ssh/id_ed25519_sk.pub
The key fingerprint is:
SHA256:CoQKA0blJ8A1xOwri167mIDb7rHxr59TYwI25ChOZ4Y kdas@localhost.localdomain
The key's randomart image is:
+[ED25519-SK 256]-+
|++*=             |
|o.o+o            |
|o +*..           |
|oE.*B            |
|+.+.oo  S        |
|.o . ...+        |
|+ =.  .+ .       |
|o++=. ..         |
|o*=o+++.         |

Here we passed the type of the key using -t flag and saving the private key using -f. I pasted the public key in the server's ~/.ssh/authorized_keys file, and then also configured the ssh client on my laptop to use that specified key via the ~/.ssh/config file.

Host kushaldas.in
  HostName kushaldas.in
  User kushal
  IdentityFile ~/.ssh/id_ed25519_sk

Finally we can login via ssh.

✦ ❯ ssh kushaldas.in
Enter passphrase for key '/home/kdas/.ssh/id_ed25519_sk': 
Confirm user presence for key ED25519-SK SHA256:CoQKA0blJ8A1xOwri167mIDb7rHxr59TYwI25ChOZ4Y
User presence confirmed

You will notice that after asking for the passphrase of the key, ssh is asking to touch the Yubikey to confirm the user presence. You can read more in the tutorial from Yubico.

If you miss to touch the Yubikey on time, you will get an error like:

sign_and_send_pubkey: signing failed for ED25519-SK "/home/kdas/.ssh/id_ed25519_sk": invalid format

Working over ssh in Python

Working with the remote servers is a common scenario for most of us. Sometimes, we do our actual work over those remote computers, sometimes our code does something for us in the remote systems. Even Vagrant instances on your laptop are also remote systems, you still have to ssh into those systems to get things done.

Setting up of the remote systems

Ansible is the tool we use in Fedora Infrastructure. All of our servers are configured using Ansible, and all of those playbooks/roles are in a public git repository. This means you can also setup your remote systems or servers in the exact same way Fedora Project does.

I also have many friends who manage their laptops or personal servers using Ansible. Setting up new development systems means just a run of a playbook for them. If you want to start doing the same, I suggest you have a look at the lightsaber built by Ralph Bean.

Working on the remote systems using Python

There will be always special cases where you will have to do something on a remote system from your application directly than calling an external tool (read my previous blog post on the same topic). Python has an excellent module called Paramiko to help us out. This is a Python implementation of SSHv2 protocol.

def run(host='', port=22, user='root',
                  command='/bin/true', bufsize=-1, key_filename='',
                  timeout=120, pkey=None):
    Excecutes a command using paramiko and returns the result.
    :param host: Host to connect
    :param port: The port number
    :param user: The username of the system
    :param command: The command to run
    :param key_filename: SSH private key file.
    :param pkey: RSAKey if we want to login with a in-memory key
    client = paramiko.SSHClient()

    client.connect(hostname=host, port=port,
            username=user, key_filename=key_filename, banner_timeout=10)
    chan = client.get_transport().open_session()
    stdout = chan.makefile('r', bufsize)
    stdout_text = stdout.read()
    status = int(chan.recv_exit_status())
    return stdout_text, status

The above function is a modified version of the run function from Tunir codebase. We are creating a new client, and then connecting to the remote system. If you have an in-memory implementation of the RSA Key, then you can use the pkey parameter the connect method, otherwise, you can provide the full path to the private key file as shown in the above example. I also don’t want to verify or store the host key, the second line of the function adds a policy to make sure of that.

Working on the remote systems using Golang

Now, if you want to do the same in golang, it will not be much different. We will use golang’s crypto/ssh package. The following is taken from gotun project. Remember to fill in the proper error handling as required by your code. I am just copy-pasting the important parts from my code as an example.

func (t TunirVM) FromKeyFile() ssh.AuthMethod {
	file := t.KeyFile
	buffer, err := ioutil.ReadFile(file)
	if err != nil {
		return nil

	key, err := ssh.ParsePrivateKey(buffer)
	if err != nil {
		return nil
	return ssh.PublicKeys(key)

sshConfig := &ssh.ClientConfig{
	User: viper.GetString("USER"),
	Auth: []ssh.AuthMethod{

connection, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", ip, port), sshConfig)
session, err = connection.NewSession()
defer session.Close()
output, err = session.CombinedOutput(actualcommand)

Creating an ssh connection to a remote system using either Python or Golang is not that difficult. Based on the use case choose to either have that power in your code or reuse an existing powerful tool like Ansible.