guest@6admin.io:~$

SSH in 2021: what thirteen years changed since the 2008 series

The SSH series around here ended in 2013: key authentication in 2008 , multiple authentication in 2012 , connection restriction and PKCS#8 in 2013 as the final act. Eight years later, everything those articles did with craftsmanship exists as primitives. Here is the 2021 memo, complete.

The keys: Ed25519 and the FIDO2 couple

ssh-keygen -t ed25519 -a 100 -C "logout@2021"
ssh-keygen -t ed25519-sk -a 100                  # hardware key, touch required
ssh-add -K                                       # downloads resident keys from the YubiKey

Ed25519 for everything software: shorter, faster, more resilient and -a 100 hardens the KDF against passphrase brute-forcing (the direct heir of the 2013 PKCS#8 article , which was right ahead of its time). The -sk keys (OpenSSH 8.2, 2020) plug in a YubiKey: the private key never leaves the hardware, the protocol requires a physical touch. And RSA? The sun is setting: OpenSSH 8.2 disabled ssh-rsa (SHA-1) in CASignatureAlgorithms by default and the full retirement announcement is already in the release notes. In 2008, RSA 2048 was the luxury; in 2021, it is technical debt.

Certificates: the structured answer to “multiple authentication”

The 2012 article hacked with several keys and force-command. The tool that makes it clean is called an SSH certificate:

# on the CA (one machine, one secret)
ssh-keygen -s ca_key -I "logout-access" -n prod,deploy -V +1d logout.pub
# on the server
echo "@cert-authority * <contents of ca_key.pub>" > /etc/ssh/trusted-user-ca-keys.pem
# in sshd
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
AuthorizedPrincipalsFile /etc/ssh/principals/%u

A CA key signs temporary identities (-V +1d) with principals (the groups: prod, deploy) and the server no longer carries a 40-line authorized_keys: it trusts an authority. Revocation = do not renew. Rotation = re-sign. The long-lived password and key disappear from the equation and 2012’s “multiple authentication” becomes a principals=.

The sshd hardening, the 2021 version

# /etc/ssh/sshd_config.d/hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
ClientAliveInterval 120
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com

Nothing exotic: every line removes an option 2008 left open for comfort. The sshd_config.d/ (Include, OpenSSH 8.2+) lets you version it per machine without forking the main file.

The client, the part that changed most

# ~/.ssh/config
Include config.d/*

Host prod-jump
  HostName bastion.prod.example.tld
  User logout
  IdentitiesOnly yes

Host prod-*
  ProxyJump prod-jump              # the 2008 tunnel becomes one line
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m
  ServerAliveInterval 30
  ServerAliveCountMax 4
  HashKnownHosts yes
  StrictHostKeyChecking accept-new
  UpdateHostKeys yes

Three direct inheritances from the series: ProxyJump (-J) kills administrative tunneling in one option (no more cascading -L); ControlMaster/ControlPersist multiplexes sessions over one TCP connection (the TCP-over-TCP price disappears); and Match exec + Include allow context-based config:

# ~/.ssh/config.d/work
Match host "github.com-work" exec "test -d ~/projets/company"
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

That is the heir of 2012’s connection restriction : the restrictions now live in the config itself. And at scale, it becomes the myflec split : a ~/.ssh/config holding nothing but an Include config.d/* and three files per context (work, private, lab), every Host block staying in the file of the context it belongs to. Two rules make the setup sustainable: IdentitiesOnly yes everywhere (otherwise ssh tries every key in order and burns attempts) and first match wins (specific blocks before wildcards, the include expanding at its point of inclusion). The naming convention follows: keys named <forge>_<label>_<type> (github.com_perso_ed25519, company.work_companyuser_ed25519), host aliases <forge>-<label> (git@github.com-work:owner/repo). Nine years after the 2012 article, the config became versioned, per-context data instead of a plate of spaghetti.

The moral, thirteen years later

In 2008, the public key was a conquest. In 2013, PKCS#8 protected the key. In 2021, the key never leaves the hardware, the identity gets signed for a day and the server only knows certificates. The series continues; it just moved from craftsmanship to primitives. Next article in the series in eight years, or whenever FIDO2 keys become as mundane as Ed25519 keys have become.

[edit: 2026] The Ansible implementation of all this: the ssh role .