Recommendation for Secure Shell Public Key Management

Paul Sander, January 2, 2018

Secure Shell is used to initiate secure authenticated login sessions on remote hosts.  It can use one of several methods to authenticate user.  Password authentication is one supported method, but challenge strings combined with public key encryption can also be used to authenticate a user without using a password.

This document describes a method to manage Secure Shell public keys on a server.  The method is useful when multiple public keys are used to authenticate users to access a single shared account.  Examples of accounts requiring such access include:

An account is assigned to a shared non-user resource, where multiple users require access to administer or use that resource.  A shared Git server is an example that might require this type of access.

An account is owned by a single user, but a security policy requires unique public keys be assigned to each of the devices from which the user may log in to that account.  A user’s corporate home directory is an example if it may be accessed from the user’s desktop workstation, company issued laptop, home computer, tablet, and smart phone.

Conventional wisdom is to edit the account’s public key database (the “authorized_keys” file) directly, and in some cases to manage the database by way of a system configuration management tool such as Puppet or Chef.  In practice, this is cumbersome because specific keys can be difficult to identify, thus making them difficult to remove when authorization is revoked.  As a result, users may never lose their ability to access the account, which is a potential security risk.  This document presents a method to derive the authorized_keys file from other managed sources in a way that simplifies managing its contents.

Generating Secure Shell Encryption Keys

Secure Shell expects to find the user’s configuration file and encryption keys in a directory named “.ssh” located in the user’s home directory.  In that directory, the configuration file is named “config” and contains records (a collection of name/value pairs) that describe how to connect to remote hosts as well as various features of the connection.  (For example, TCP port forwarding or an X11 proxy server can be configured.  Encryption keys can also be specified in the configuration file.)

Public key encryption involves the use of two encryption keys:  A private key, and a public key, also called a “key pair”.  These keys have the property that data encrypted by one can be decrypted by the other.  Data encrypted with the public key can be read by someone possessing the private key, so that anyone can send data privately to the holder of the private key.  Conversely, data encrypted with the private key can be decrypted by anyone possessing the public key, which gives the public confidence that the data really did originate from the owner of the private key.  The private key must be kept a closely guarded secret, while the public key can be shared with anyone.

The “ssh-keygen” program creates key pairs.  It is used as follows:

cd $HOME/.ssh && ssh-keygen -f id_rsa

The result is two files named “id_rsa” and “id_rsa.pub” that are located in the “.ssh” directory.  The id_rsa file contains the private key, and the id_rsa.pub file contains the public key.  This file is used by default when initiating a Secure Shell connection.

Additional keys can be created by replacing “id_rsa” in the above command with the name of the new key pair.  These keys can be used by supplying the fullpath of the private key file to the Secure Shell command (ssh) using its “-i” command line option, or by using the “IdentityFile” setting in the user’s Secure Shell configuration file.

A user need not create a new key pair for every service.  Because only public information is shared, keys can be reused.  However, users should create new key pairs on each device from which they might initiate a Secure Shell session.  This is especially important for portable devices, so that credentials for specific devices can be easily revoked if the device is lost or stolen, without generating new key pairs for every service and every other device.

After a key pair has been created, the public key file can be shared with the owner of a shared resource or service.  The file contains plain text, but it is typically encoded in a very long line of text.  As a consequence, it is unwise to try to cut and paste the key unless there is no other way to share it.  Normally, the public key file would be uploaded via the service’s user interface, or it would be e-mailed as an attachment to the owner of the service.

The Basic Method

The traditional method to manage the shared resource’s public key database (the $HOME/.ssh/authorized_keys file) is to simply append each new public key to this file as it is received.  This is easy to do, and keeps a permanent record of every public key ever used to access the service.  However, revoking access for a key managed in this way is cumbersome because the owners of individual keys can be hard to identify.  Although each key has an identifier associated with it, identifiers such as “root@localhost” are not helpful but are surprisingly common.

The recommended method stores each public key in a unique file.  The authorized_keys file is generated by concatenating the contents of each of the public key files that identify authorized users.

The mechanism is very simple:  In the shared account’s $HOME/.ssh directory, create child directories named “users” and “revoked”.  Place the valid keys in the “users” directory, and invalid keys in the “revoked” directory.  After the keys have been categorized and stored as needed, generate the authorized_keys file using the following command:

cd $HOME/.ssh && cat users/* > authorized_keys

To grant access to the service, create a new public key file in the “users” directory.  To revoke access, move a file from the “users” directory to the “revoked” directory.  To reinstate access, move a file from the “revoked” directory back to the “users” directory.  Then regenerate the authorized_keys file using the command above.

Leading Commentary

It may be useful to add commentary to the top of the authorized_keys file to remind users to not edit the file directly, and to either provide instructions on how to update the keys or to provide a link to the proper procedure.  This can be done by creating a file in the “users” directory whose name appears earlier in the system’s sort order than any of the key files.  Naming the file “!!README!!” often fulfills this need.

The following is some sample text that can be used as leading commentary:

# DO NOT EDIT THE authorized_keys FILE!

# This file derives from the contents of the
# “users” directory.  When adding new keys,
# please store them in a new file in the “users”
# directory.  When removing keys, please
# move them from the “users” directory to the
# “revoked” directory for possible future
# reinstatement.  Then use the following command to
# regenerate this file:
#
#         cat users/* > authorized_keys
#

When expanding wildcards, the shell sorts the list of matching files.  The contents of the “!!README!!” file therefore appear at the top of the authorized_keys file.  In the unlikely situation that the system’s collating order is not as expected, then it may be necessary to set one or more of the following environment variables to the value “C”:  LC_COLLATE, LC_ALL, LANG.

Naming Conventions

At the very least, the names of any public key file should contain the email address of the user that the file authorizes.  This makes it easy to match keys to their owners should it be necessary to revoke or reinstate their access.  Additional conventions may be desirable:

Best practice is for each user to supply unique public keys for each of their devices so that the keys can be revoked if the device is lost or stolen.  It becomes easy to revoke keys for lost devices if the device is identified through the naming conventions of the key files.

Embedding date stamps in the names can be useful if a security policy limits the lifetimes of key files.  This removes a dependency on file modification times stored in the filesystem, which can change under various circumstances (such as relocating a service to a new host).

Serial numbers may be used to disambiguate otherwise identical names.  They should be fixed length and be padded with leading zeros, so that they can be sorted as text strings.  When combined with date stamps, serial numbers should be at least two characters long.  (It is possible that a confused user can produce more than ten keys in a day, but 100 keys is much less likely.)  If serial numbers are not combined with date stamps, then the length should be at least four or five digits.  (This is to account for the possibility that the lifetime of a service is very long, and that some users’ tenure could be equally long, while the lifetime of a public key is comparatively short.)

A better naming convention would append the user’s device and a serial number, and possibly a date stamp, to the user’s email address.  Here are examples:

user@domain+device-yyyymmddnn

user@domain+device-nnnnn

Note that the “+” sign is not valid for an Internet domain name, so this is a reasonable delimiter between the user’s identity and the other parts of the naming convention.

Eliminating a Race Condition

Generating the authorized_keys file as described above creates a potential race condition in which the file is incomplete while a user attempts to log in to the service.  The following procedure minimizes the race condition by creating a temporary file and renaming it into place:

cd $HOME/.ssh && cat users/* > authkeys.new && mv -f authkeys.new authorized_keys

Deploying the Method to an Existing Service

If a service already exists and manages public keys by editing the authorized_keys file directly, this new method can be easily deployed in one of two ways.  To begin, create the “!!README!!” file in the “users” directory as described above.

One way to deploy the new method is to simply rename the authorized_keys file as follows.  This is quick and easy, but it means that the contents of the original file must be managed specially:

cd $HOME/.ssh && mv authorized_keys users/original

Another way is to use the identities already stored in the authorized_keys file as the name of new key files.  This is inexact because it uses values produced by the users’ systems’ ssh-keygen tools, but it is better than simply moving the original file.  The following procedure splits up the authorized_keys file:

cd $HOME/.ssh/users && awk ‘{ print $0 >> $3 }’ ../authorized_keys

After performing either of these two procedures, generate the new authorized_keys file in the usual way.

Closing Comments

This recommended method for managing Secure Shell public keys has proven useful for managing access to resources that are shared by way of Secure Shell.  Although it was invented independently, other software packages use similar techniques to manage public keys.  (An example of such a package is Gitolite, which performs simple access control for Git repositories on a shared server.)

In addition to protecting shared resources, individual users can protect their home accounts using this method when they use multiple devices for remote access.  They create unique public keys for each device, and store them in the “users” directory and generate the authorized_keys file.  If a device is lost or replaced, the unique key for that device can be easily revoked, and a new device (with a new key) can be easily added.

Please send feedback to the author, Paul Sander, at paul@wakawaka.com.