Managing SSH keys for a home network

How to generate certificate authorities so your clients and hosts can all talk to each other easily.
ssh
linux
bash
Published

May 3, 2020

Introduction

This guide covers how I set up and manage ssh keys on my home network. It’s not meant as an explainer on what ssh is or why you should use it, more a recipe for how I do things.

  • Edit October 16 2020 - Added scripts to automate this setup

Who this is for

Mostly me so I remember how to do this if I have to rebuild my network, or want to add new clients. I’m not an expert in security, so definitely do your own research before implementing any of this, and I would certainly say that this is a hobbyist implementation in general and not suitable for an organization.

I got the inspiration for this from How to SSH properly. It’s a nice and thorough guide, but it’s a bit enterprise oriented for my purposes. The idea here will be to make something that’s more maintainable for a local setup.

The actual process

The idea is to have my certificate authority keys live on a USB drive. That way when I set up a new machine I can plug in the drive and sign the keys, and then the rest of the time the keys are completely removed from network access. As long as I’m smart and don’t lose/destroy the USB drive it seems like a great idea. The main advantage from just using regular keys rather than signing them is that I don’t have to add a line to authorized_keys on every host every time I add a client, and I can keep a nice clean known_hosts on each of my clients. For this example my host machine will be named mars and the client machine will be named luna. I’ll do the preliminary setup from luna, but any machine should be fine.

Get the drive ready

I have a 64GB USB key that I’m going to store the CAs on. That’s way too big for what I need, and I wouldn’t mind having it handy to shuttle other files around (I’m not going to be taking it anywhere, on account of it has CAs for my network on it, but still). So I created a big exfat partition that took up most of the disk for files and such, along with a 200MB ntfs partition right at the end for storing keys. I went with exfat for the storage partition because it’s readable in both Windows and Linux (once you install exfat-utils and exfat-fuse), is designed for flash media, and can handle large files. I went with NTFS for the secrets partition because I have to have file permissions on the private keys or it won’t work, and windows machines won’t read EXT4. It does mean I’ll have to install NTFS support on any Linux boxes I want to run it from, which is a hassle, but I can live with that.

Generate CA keys

From the usb drive:

ssh-keygen -t ed25519 -f user_ca -C user_ca
ssh-keygen -t ed25519 -f host_ca -C host_ca

Generate a host and user signing key using ed25519 encryption. Generates public private key pairs named host/user_ca and host/user_ca.pub for the public keys. RSA is the default, but all of my systems support ed25519 and I understand it’s better in terms of security and performance so I might as well take this opportunity to update.

Generate known_hosts file

This will go in the ~/.ssh folder of clients in order to validate access.

echo -n "@cert-authority * " > known_hosts
cat host_ca.pub >> known_hosts

Generate the host key and sign it

For any machines that I want to be able to ssh into I need to generate and sign a host key. From the same folder as the CA keys were generated:

ssh-keygen -t ed25519 -f mars_host_ed25519_key
ssh-keygen -s host_ca -I mars -h mars_host_ed25519_key.pub

WARNING: DO NOT PUT A PASSPHRASE ON HOST KEYS

The first line operates the same as before. The second line signs the public key. -I mars is the certificate’s identity, apparently it can be used to revoke a certificate in the future although I’m not totally clear on how at this point. You can use the -n flag to specify which hostname in particular this key is valid for but I don’t really see the need in as small a setup as I’m doing. -h identifies this as a host key.

At the end of this, three new files are generated: mars_host_ed25519_key, mars_host_ed25519_key-cert.pub, and mars_host_ed25519_key.pub.

Configure SSH to use host certificates

Move the three generated files from the last section and copy user_ca.pub to /etc/ssh on your host and set the permissions to match the other files there. In this example I’ve physically moved the key over to the host machine and mounted it. If your host currently has ssh enabled with password based authentication you could scp it over instead:

sudo mv mars_host_ed25519_key* /etc/ssh/
sudo cp user_ca.pub /etc/ssh/
cd /etc/ssh/
sudo chown root:root mars_host*
sudo chown root:root user_ca.pub
sudo chmod 644 mars_host*
sudo chmod 600 mars_host_ed25519_key # stricter private key permissions
sudo chmod 644 user_ca.pub

Next, edit /etc/ssh/sshd_config to have the lines

HostKey /etc/ssh/mars_host_ed25519_key
HostCertificate /etc/ssh/mars_host_ed25519_key-cert.pub
TrustedUserCAKeys /etc/ssh/user_ca.pub

There was a commented out line in the file for HostKey so I put it below there. Placement shouldn’t really matter but it will hopefully be easier to find if I have to track this file down later.

sudo systemctl restart sshd

Once you’re sure everything is working you’ll want to disable password authentication. Be aware that if you screw up keys and have the second line set you’ll have to physically connect to the machine to resolve it. For my home network this is no big deal, but just be aware. Go into /etc/ssh/sshd_config and set the following lines and restart ssh again:

PubkeyAuthentication yes
PasswordAuthentication no

Create user key

Still in the folder with the ca key:

ssh-keygen -f luna_ed25519 -t ed25519
ssh-keygen -s user_ca -I ian@luna -n admin,ansible,ipreston,pi luna_ed25519.pub
mv luna* ~/.ssh/
cp known_hosts ~/.ssh/

The only new flag in this is -n ipreston,admin,pi which is the comma separated list of users I want this to be valid for. In addition to the username I set up on my server I want this key to be able to connect to my pfsense admin user and my raspberry pi, which I generally just leave with the default pi user.

Give it a test drive

At this point if everything is configured correctly you should be able to ssh from your client to your host and only be prompted for the passkey you set (or without any prompting if you didn’t set a passkey)

ssh -i ~/.ssh/luna_ed25519 ipreston@mars

Set up an ssh config file

If that all worked the next step will be to set a nice alias to save some typing in the future.

touch ~/.ssh/config
chmod 600 ~/.ssh/config

In config:

Host mars
    User ipreston
    IdentityFile ~/.ssh/luna_ed25519

There are lots of other options you can set in that config but that’s all I need for my setup. After that all you should need to type is ssh mars to be connected to your host with the right user and using the correct authentication.

External servers

There’s no real sense in signing keys for external services. For example, I use GitHub with SSH, but they only allow CA authentication for enterprise (fair enough). There’s also no particular reason to re-use my LAN keys for GitHub, so I created a new key and set ~/.ssh/config to use the correct identity automatically:

Use the same commands as the user key setup stage, except don’t bother signing the key. Add the public key to GitHub as you normally would (GitHub docs) and then edit ~/.ssh/config to add something like the following:

Host github.com
    IdentityFile ~/.ssh/luna_github_ed25519
    HostName github.com
    User git

Android

I don’t ssh a ton from Android, but every so often it’s handy to be able to do. My former go-to app was Connectbot, but I never really liked how it managed keys, and I couldn’t get it to work with the CA. I ended up going with termux. To set it up:

  • Generate and sign keys as normal.
  • copy the key pairs, config and known_hosts onto your android device
  • Install termux (it’s in the play store)
  • Open termux
  • Install openssh with pkg install openssh
  • Allow access to internal storage with termux-setup-storage and accepting the request
  • Navigate to where you copied the files (somewhere in ~/storage/shared) and copy them to ~/.ssh using cp, it’s just regular bash in termux.
  • Everything should work, try connecting to a host.

Auto config scripts

These have been tested to set up the host and user authentication on Arch Linux. Presumably they’ll work on other distros as well. Android will still need to be done manually probably.

To set them up save them both in a directory, and then in a CA subfolder create user and host cert keys as well as a known_hosts file and config file as described above. In the config where you’d normally have the hostname associated with the key file just put HOST and the script will replace it with whatever your hostname is, which is also how it creates keys.

Set up host authentication

This script when run as root will generate a signed host key, move it to the correct directory, modify SSH configuration to authenticate using it and then restart ssh.

#!/usr/bin/env bash

# Bash "strict" mode, -o pipefail removed
SOURCED=false && [ "${0}" = "${BASH_SOURCE[0]}" ] || SOURCED=true
if ! $SOURCED; then
  set -eEu
  shopt -s extdebug
  trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
  IFS=$'\n\t'
fi

# Text modifiers
Bold="\033[1m"
Reset="\033[0m"

# Colors
Red="\033[31m"
Green="\033[32m"
Yellow="\033[33m"

error_msg() {
  T_COLS=$(tput cols)
  echo -e "${Red}$1${Reset}\n" | fold -sw $((T_COLS - 1))
  exit 1
}

check_root() {
  echo "Checking root permissions..."

  if [[ "$(id -u)" != "0" ]]; then
    error_msg "ERROR! You must execute the script as the 'root' user."
  fi
}


generate_and_sign_host_key() {
  private_suffix="_host_ed25519_key"
  private_key="$HOSTNAME$private_suffix"
  public_key="$private_key.pub"
  ssh-keygen -t ed25519 -f $private_key -N ""
  chown root:root $private_key*
  # Have to lock it down before signing
  chmod 600 $private_key
  ssh-keygen -s CA/host_ca -I $HOSTNAME -h $public_key
  cp CA/user_ca.pub /etc/ssh/
  chown root:root /etc/ssh/user_ca.pub
  chmod 644 /etc/ssh/user_ca.pub
  chmod 644 $private_key*
  # Set back to stricter private key access
  chmod 600 $private_key
  mv $private_key* /etc/ssh/
}

update_sshd_config() {
  private_suffix="_host_ed25519_key"
  private_key="$HOSTNAME$private_suffix"
  hostkey="HostKey /etc/ssh/$private_key"
  cert="HostCertificate /etc/ssh/$private_key-cert.pub"
  ca="TrustedUserCAKeys /etc/ssh/user_ca.pub"

  sshd="/etc/ssh/sshd_config"
  sed -i "/^#HostKey\s\/etc\/ssh\/ssh_host_ed25519_key/a $ca" $sshd
  sed -i "/^#HostKey\s\/etc\/ssh\/ssh_host_ed25519_key/a $cert" $sshd
  sed -i "/^#HostKey\s\/etc\/ssh\/ssh_host_ed25519_key/a $hostkey" $sshd
  sed -i 's/^#PasswordAuthentication\syes/PasswordAuthentication no/g' $sshd
}

check_root
generate_and_sign_host_key
update_sshd_config
systemctl restart sshd

Setup user authentication

Whatever user your run this as should end up with a configured SSH setup that will allow access into any similarly configured hosts.

#!/usr/bin/env bash

# Bash "strict" mode, -o pipefail removed
SOURCED=false && [ "${0}" = "${BASH_SOURCE[0]}" ] || SOURCED=true
if ! $SOURCED; then
  set -eEu
  shopt -s extdebug
  trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
  IFS=$'\n\t'
fi

# Text modifiers
Bold="\033[1m"
Reset="\033[0m"

# Colors
Red="\033[31m"
Green="\033[32m"
Yellow="\033[33m"

error_msg() {
  T_COLS=$(tput cols)
  echo -e "${Red}$1${Reset}\n" | fold -sw $((T_COLS - 1))
  exit 1
}

check_root() {
  echo "Checking root permissions..."

  if [[ "$(id -u)" == "0" ]]; then
    error_msg "ERROR! Don't run this as root."
  fi
}

update_known_hosts() {
  cp CA/known_hosts $HOME/.ssh/known_hosts
}

gen_keys() {
  cp CA/user_ca .
  chown $USER user_ca
  chmod 600 user_ca
  private_suffix="_ed25519"
  private_key=$HOME/.ssh/$HOSTNAME$private_suffix
  public_key="$private_key.pub"
  ssh-keygen -f $private_key -t ed25519 -N ""
  ssh-keygen -s user_ca -I $USER@$HOSTNAME -n admin,ipreston,cornucrapia,pi,ansible $public_key
  rm user_ca
}

update_config() {
  user_config="$HOME/.ssh/config"
  cp CA/config $user_config
  chown $USER $user_config
  chmod 600 $user_config
  sed -i -e "s/HOST/$HOSTNAME/g" $user_config
}


check_root
mkdir -p $HOME/.ssh
update_known_hosts
gen_keys
update_config

Conclusion

That’s basically it, whenever I add a new client or host I can connect the USB key, run setup_host.sh as root and then setup_user.sh as any users. All previously configured clients or hosts will automatically have the correct permissions.