Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsTraining
StudyProjectsNewsletterHire MeAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Training
Study
Projects
Newsletter
Hire Me
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

2368+ Articles
158+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Checklists
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. OpenSSH Hardening with Certificate-Based Authentication
OpenSSH Hardening with Certificate-Based Authentication
HOWTOIntermediate

OpenSSH Hardening with Certificate-Based Authentication

Replace static SSH keys with a short-lived certificate authority. Harden sshd_config, eliminate lateral-movement risk, and enforce zero-trust access across your Linux fleet.

Dylan H.

Tutorials

August 10, 2026
11 min read

Prerequisites

  • Linux server running Ubuntu 22.04 / Debian 12 or RHEL 9-compatible distro
  • OpenSSH 8.0+ on both client and server (check with `ssh -V`)
  • Root or sudo access on the servers you want to harden
  • Basic familiarity with the command line and SSH key concepts

Introduction

Static SSH keys are a ticking time bomb. Once a private key is copied to a developer's laptop, a CI runner, or a backup archive, you lose all visibility into who is using it and when it expires — because it never does. When that laptop is lost or that contractor leaves, you're hunting through ~/.ssh/authorized_keys files across a dozen servers hoping you caught every copy.

SSH certificate authentication solves this at the root. You run a lightweight Certificate Authority (CA), sign short-lived user certificates (e.g. 8-hour TTL), and every server trusts the CA — not individual public keys. Revocation is instant: stop signing, and stale keys can't authenticate. Combine this with a locked-down sshd_config and you have an SSH posture that holds up under a real audit.

This guide covers:

  1. Standing up an SSH CA (user CA + host CA)
  2. Signing host certificates so clients can verify servers (eliminating TOFU risk)
  3. Signing user certificates with a short TTL
  4. Hardening sshd_config to close common attack vectors
  5. Verifying and troubleshooting the setup

No external tooling required — everything here uses stock OpenSSH and standard Linux commands.


Prerequisites

Before you begin, confirm your OpenSSH version supports the features used here:

ssh -V
# OpenSSH_8.9p1 Ubuntu-3ubuntu0.10, OpenSSL 3.0.2 ...

OpenSSH 8.0 or later is required for the -I identity comment flag and principal enforcement used in this guide. Most modern distros ship a compatible version.

You'll also want at least two machines handy:

  • CA host — a dedicated, hardened machine that will hold the CA private keys. It should be offline or firewalled when not actively signing. In small environments, a separate VM or even a YubiKey-backed setup works.
  • SSH server — the Linux host you want to harden.
  • Client machine — where your admin will initiate SSH sessions.

Step 1 — Create the Certificate Authority Keys

On your CA host, generate two separate CA keypairs: one for signing user certificates, one for signing host certificates. Keeping them separate limits blast radius if one is compromised.

# Create a dedicated directory for CA keys
sudo mkdir -p /etc/ssh/ca
sudo chmod 700 /etc/ssh/ca
cd /etc/ssh/ca
 
# User CA — signs user (client) certificates
sudo ssh-keygen -t ed25519 -f user_ca -C "CosmicBytez User CA" -N ""
 
# Host CA — signs host (server) certificates
sudo ssh-keygen -t ed25519 -f host_ca -C "CosmicBytez Host CA" -N ""

Security note: In production, protect these private keys with strong passphrases (-N "" is used here for automation clarity). Store user_ca and host_ca private keys offline or in a hardware security module (HSM/YubiKey). The .pub files are safe to distribute widely.

Verify both keypairs were created:

ls -la /etc/ssh/ca/
# -rw------- user_ca        (PRIVATE — guard this)
# -rw-r--r-- user_ca.pub    (distribute to servers)
# -rw------- host_ca        (PRIVATE — guard this)
# -rw-r--r-- host_ca.pub    (distribute to clients)

Step 2 — Sign the Host Certificate

Clients will use the host CA's public key to verify they're talking to a legitimate server — eliminating the "Are you sure you want to continue connecting?" TOFU prompt.

On the CA host, sign the SSH server's host public key:

# Copy the server's host public key to the CA host first
scp admin@ssh-server:/etc/ssh/ssh_host_ed25519_key.pub /tmp/ssh_host_ed25519_key.pub
 
# Sign it — valid for 52 weeks, hostname as principal
sudo ssh-keygen -s /etc/ssh/ca/host_ca \
  -I "ssh-server.cosmicbytez.ca" \
  -h \
  -n "ssh-server.cosmicbytez.ca,ssh-server,10.10.1.50" \
  -V +52w \
  /tmp/ssh_host_ed25519_key.pub
 
# This creates: /tmp/ssh_host_ed25519_key-cert.pub

Flag breakdown:

  • -s — path to the signing (CA) private key
  • -I — certificate identity (appears in logs — use a meaningful hostname)
  • -h — this is a host certificate (not a user certificate)
  • -n — comma-separated list of valid principals (DNS names and IPs the cert is valid for)
  • -V +52w — validity period (52 weeks; use a shorter TTL for higher-security environments)

Copy the signed certificate back to the SSH server:

scp /tmp/ssh_host_ed25519_key-cert.pub admin@ssh-server:/etc/ssh/

On the SSH server, tell sshd to present this certificate:

# Add to /etc/ssh/sshd_config
echo "HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub" | sudo tee -a /etc/ssh/sshd_config

Step 3 — Configure Clients to Trust the Host CA

On each client machine, add the host CA to the global known_hosts so SSH automatically trusts signed servers without prompting:

# Download or copy the host CA public key
scp admin@ca-host:/etc/ssh/ca/host_ca.pub /tmp/host_ca.pub
 
# Add to system-wide known_hosts with @cert-authority marker
echo "@cert-authority *.cosmicbytez.ca,10.10.1.0/24 $(cat /tmp/host_ca.pub)" | \
  sudo tee -a /etc/ssh/ssh_known_hosts

Adjust the hostname pattern to match your domain. Now when a client connects to any server that presents a valid host certificate, SSH trusts it silently — no TOFU, no manual host key acceptance.


Step 4 — Sign User Certificates

User certificates replace (or complement) individual authorized_keys entries. With a short TTL, even a stolen private key becomes useless quickly.

On the CA host, sign a user's public key:

# The user sends you their public key — never their private key
# Example: ~/.ssh/id_ed25519.pub from admin user "dylan"
 
sudo ssh-keygen -s /etc/ssh/ca/user_ca \
  -I "dylan@cosmicbytez.ca" \
  -n "admin,ubuntu,ec2-user" \
  -V -5m:+8h \
  /path/to/dylan_id_ed25519.pub

Flag breakdown:

  • -n "admin,ubuntu,ec2-user" — the Unix usernames this certificate can log in as (principals). The server enforces this.
  • -V -5m:+8h — valid from 5 minutes ago (clock skew buffer) to 8 hours from now. Adjust TTL based on your risk tolerance.

The output is dylan_id_ed25519-cert.pub. Send it back to the user:

scp dylan_id_ed25519-cert.pub dylan@workstation:~/.ssh/id_ed25519-cert.pub

The user doesn't need to do anything special — if the certificate file is named <keyname>-cert.pub alongside the private key, SSH picks it up automatically:

ls ~/.ssh/
# id_ed25519          (private key — never leaves the workstation)
# id_ed25519.pub      (public key — sent to CA for signing)
# id_ed25519-cert.pub (signed certificate — received from CA, expires in 8h)

Step 5 — Configure Servers to Trust the User CA

On each SSH server, tell sshd to accept certificates signed by your user CA instead of (or in addition to) authorized_keys:

# Copy the user CA public key to the server
scp admin@ca-host:/etc/ssh/ca/user_ca.pub /tmp/user_ca.pub
sudo cp /tmp/user_ca.pub /etc/ssh/user_ca.pub
sudo chmod 644 /etc/ssh/user_ca.pub

Add the TrustedUserCAKeys directive to sshd_config:

echo "TrustedUserCAKeys /etc/ssh/user_ca.pub" | sudo tee -a /etc/ssh/sshd_config

For strict environments, you can also require that the certificate principal matches the target Unix account by adding an AuthorizedPrincipalsFile. Create /etc/ssh/auth_principals/%u per username:

sudo mkdir -p /etc/ssh/auth_principals
 
# For the "admin" Unix user, allow certificates with the "admin" principal
echo "admin" | sudo tee /etc/ssh/auth_principals/admin
echo "ubuntu" | sudo tee /etc/ssh/auth_principals/ubuntu

Then enable it in sshd_config:

echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" | sudo tee -a /etc/ssh/sshd_config

Step 6 — Harden sshd_config

With certificates in place, apply a comprehensive sshd_config hardening pass. Replace or append to /etc/ssh/sshd_config:

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
# /etc/ssh/sshd_config — hardened configuration
 
# ── Protocol & Network ────────────────────────────────────────────────────────
Port 22
AddressFamily inet
ListenAddress 0.0.0.0
 
# ── Host Keys — prefer modern algorithms ─────────────────────────────────────
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
 
# ── Cryptography — disable weak ciphers/MACs/KexAlgorithms ──────────────────
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
 
# ── Certificate Authority ─────────────────────────────────────────────────────
TrustedUserCAKeys /etc/ssh/user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
 
# ── Authentication ────────────────────────────────────────────────────────────
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys   # keep for break-glass access
 
# Disable auth methods you're not using
GSSAPIAuthentication no
HostbasedAuthentication no
IgnoreRhosts yes
PermitEmptyPasswords no
 
# Root login — never allow direct root SSH
PermitRootLogin no
 
# Limit login attempts and grace time
MaxAuthTries 3
LoginGraceTime 30
 
# ── Session ───────────────────────────────────────────────────────────────────
# Disconnect idle sessions after 15 min
ClientAliveInterval 300
ClientAliveCountMax 3
 
# Limit concurrently unauthenticated connections (anti-scanner)
MaxStartups 10:30:60
 
# ── Access Control ────────────────────────────────────────────────────────────
# Restrict to specific group — add your admins to this group
AllowGroups sshusers
 
# ── Features — disable what you don't need ────────────────────────────────────
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
PrintMotd no
Banner /etc/ssh/banner
 
# ── Logging ───────────────────────────────────────────────────────────────────
LogLevel VERBOSE
SyslogFacility AUTH

Create the sshusers group and add your admin accounts:

sudo groupadd sshusers
sudo usermod -aG sshusers admin
sudo usermod -aG sshusers ubuntu

Create a login banner (optional but recommended for compliance):

sudo tee /etc/ssh/banner << 'EOF'
*******************************************************************************
  WARNING: Authorized access only. All sessions are logged and monitored.
  Unauthorized access is prohibited and may be subject to criminal prosecution.
*******************************************************************************
EOF

Validate the configuration before reloading:

sudo sshd -t
# If no output, config is valid

Reload sshd:

sudo systemctl reload sshd

Important: Keep your current session open until you've verified a new session connects successfully. Never reload sshd in a way that closes your only access path.


Step 7 — Automate Certificate Issuance (Optional)

For teams, manually signing certificates is friction. A simple wrapper script on the CA host makes issuance self-service with an approval step. Here's a minimal signing script you can wrap behind a bastion API or call over a VPN:

#!/usr/bin/env bash
# /usr/local/bin/sign-ssh-cert.sh
# Usage: sign-ssh-cert.sh <pubkey-file> <identity> <principals> <ttl>
 
set -euo pipefail
 
PUBKEY="${1:?pubkey required}"
IDENTITY="${2:?identity required}"
PRINCIPALS="${3:-admin}"
TTL="${4:-+8h}"
CA_KEY="/etc/ssh/ca/user_ca"
OUT_DIR="/tmp/signed-certs"
 
mkdir -p "$OUT_DIR"
CERT_OUT="$OUT_DIR/$(basename "$PUBKEY" .pub)-cert.pub"
 
ssh-keygen -s "$CA_KEY" \
  -I "$IDENTITY" \
  -n "$PRINCIPALS" \
  -V "-5m:${TTL}" \
  -z "$(date +%s)" \
  "$PUBKEY"
 
echo "Certificate written to: ${PUBKEY%%.pub}-cert.pub"
echo "Valid principals: $PRINCIPALS"
echo "Expires: $TTL from now"

The -z $(date +%s) flag sets a unique serial number on each certificate, making certificate revocation via RevokedKeys possible if needed.


Verification & Testing

Inspect a signed certificate

ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub

Expected output:

/home/dylan/.ssh/id_ed25519-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:...
        Signing CA: ED25519 SHA256:... (using ssh-ed25519)
        Key ID: "dylan@cosmicbytez.ca"
        Serial: 1723294812
        Valid: from 2026-08-10T07:55:00 to 2026-08-10T16:00:00
        Principals:
                admin
                ubuntu
        Extensions:
                permit-pty
                permit-user-rc

Test the connection with verbose output

ssh -vvv -i ~/.ssh/id_ed25519 admin@ssh-server.cosmicbytez.ca 2>&1 | grep -E "(certificate|principal|Offering|Accepted)"

Look for lines like:

debug1: Server host certificate: ssh-ed25519-cert-v01@openssh.com ...
debug1: Server host key: ssh-ed25519-cert-v01@openssh.com SHA256:...
debug1: Offering public key: ... ED25519-CERT ...
debug1: Server accepts key: ... ED25519-CERT ...

Verify sshd logs certificate auth events

sudo journalctl -u ssh --since "5 minutes ago" | grep -i cert
# Aug 10 08:00:01 ssh-server sshd[12345]: Accepted publickey for admin from 10.10.1.5 port 54321 ssh2: ED25519-CERT ID dylan@cosmicbytez.ca ...

Test that expired certificates are rejected

Create a certificate with a 1-second TTL for testing:

ssh-keygen -s /etc/ssh/ca/user_ca \
  -I "test-expiry" \
  -n "admin" \
  -V +1s \
  ~/.ssh/id_ed25519.pub
 
sleep 5
 
ssh -i ~/.ssh/id_ed25519 admin@ssh-server
# Received disconnect from ... : Certificate invalid: expired

Confirm weak cipher rejection

ssh -c aes128-cbc admin@ssh-server
# Unable to negotiate ... no matching cipher found

Troubleshooting

Permission denied (publickey) even with a valid certificate

  • Check the certificate principals match the Unix username you're logging in as: ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
  • Verify TrustedUserCAKeys points to the correct file and the file is readable by sshd: sudo -u sshd cat /etc/ssh/user_ca.pub
  • If using AuthorizedPrincipalsFile, confirm the file exists and contains the correct principal: cat /etc/ssh/auth_principals/admin

Host certificate not being presented

  • Check HostCertificate is in sshd_config and points to the .pub file (not the private key): grep HostCertificate /etc/ssh/sshd_config
  • Verify the certificate file is readable: ls -la /etc/ssh/ssh_host_ed25519_key-cert.pub
  • Confirm the cert was signed with -h (host flag): ssh-keygen -L -f /etc/ssh/ssh_host_ed25519_key-cert.pub | grep Type

SSH still prompts "Are you sure you want to continue connecting?"

The client isn't trusting the host CA. Double-check /etc/ssh/ssh_known_hosts on the client has the correct @cert-authority line and the hostname pattern matches the server you're connecting to.

sshd: no hostkeys available after config change

You removed a HostKey directive for a key that exists on disk. Add back the HostKey directive or generate the missing key: sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

Locked out after PermitRootLogin no

Ensure at least one non-root user in the sshusers group has a valid certificate or authorized_key. Always maintain a break-glass authorized_keys entry until certificate auth is confirmed working.

MaxStartups causing connection drops under load

Increase MaxStartups 10:30:100 or MaxStartups 50:30:100. The format is start:rate:full — 10:30:60 means: start refusing 30% of connections at 10 unauthenticated sessions, refuse all at 60.


Summary

You've hardened SSH across three layers:

LayerWhat you didSecurity gain
Host certsCA-signed server identityEliminates TOFU; clients verify servers cryptographically
User certsShort-TTL signed user certsStolen keys expire fast; revocation is instant (stop signing)
sshd_configDisabled weak crypto, root login, idle sessionsDramatically reduces attack surface

The CA model scales naturally: add a server to the fleet by dropping one TrustedUserCAKeys directive in its sshd_config — no per-user authorized_keys management required. Rotate the CA by generating a new keypair, distributing the new public key, and phasing out the old one as existing certificates expire.

For production environments, consider pairing this setup with HashiCorp Vault's SSH secrets engine, which acts as a managed CA and enforces TTLs and RBAC on who can request certificates for which principals — closing the loop on the signing workflow itself.

#ssh#openssh#linux#hardening#pki#certificates#zero-trust#authentication

Related Articles

AIDE File Integrity Monitoring: Detect Unauthorized Changes on Linux

Deploy AIDE (Advanced Intrusion Detection Environment) to build a cryptographic baseline of your Linux filesystem and automatically alert on unauthorized modifications — a core control for compliance and breach detection.

9 min read

WireGuard VPN: Secure Remote Access for IT Professionals

Deploy a modern, high-performance WireGuard VPN server on Linux for secure remote access. Covers server setup, client configuration, multi-peer management, and firewall rules.

9 min read

HashiCorp Vault: Centralized Secrets Management for Modern

Deploy and configure HashiCorp Vault to securely store, rotate, and audit secrets across your infrastructure — covering installation, auth methods,...

8 min read
Back to all HOWTOs