Set up SSH certificate-based authentication using a Certificate Authority to eliminate individual key management. Create signed user certificates that provide secure, scalable access control for multiple servers and users.
Prerequisites
- Root or sudo access on SSH servers
- Basic understanding of SSH key authentication
- Network connectivity between CA server and clients
- OpenSSH version 5.4 or later
What this solves
SSH certificate authentication eliminates the need to distribute and manage individual public keys across multiple servers. Instead of copying SSH keys to each server, you create a Certificate Authority (CA) that signs user certificates, and servers trust any certificate signed by your CA. This provides centralized access control, automatic key rotation, and enhanced security logging for enterprise environments.
Step-by-step configuration
Generate SSH Certificate Authority
Create a dedicated CA key pair that will sign user certificates. Store the CA private key securely as it controls access to all servers.
sudo mkdir -p /etc/ssh/ca
sudo chmod 700 /etc/ssh/ca
cd /etc/ssh/ca
sudo ssh-keygen -t ed25519 -f ssh_ca -C "SSH-CA-$(hostname)-$(date +%Y%m%d)"
sudo chmod 600 ssh_ca
sudo chmod 644 ssh_ca.pub
Configure SSH server for certificate authentication
Add the CA public key to SSH server configuration so it trusts certificates signed by your CA.
# Enable certificate authentication
TrustedUserCAKeys /etc/ssh/ca/ssh_ca.pub
Optional: Require certificates (disable key authentication)
PubkeyAuthentication no
AuthenticationMethods publickey
Enable certificate logging
LogLevel INFO
Certificate validation settings
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
Restart SSH service
Apply the SSH configuration changes and verify the service starts correctly.
sudo systemctl reload sshd
sudo systemctl status sshd
Create user SSH key pair
Generate a standard SSH key pair for the user. This key will be signed by your CA to create a certificate.
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_cert -C "username@example.com"
chmod 600 ~/.ssh/id_ed25519_cert
chmod 644 ~/.ssh/id_ed25519_cert.pub
Sign user certificate with CA
Use the CA private key to sign the user's public key, creating a certificate with specific validity period and permissions.
# Copy user public key to CA server
sudo cp ~/.ssh/id_ed25519_cert.pub /etc/ssh/ca/user_key.pub
Sign the certificate (valid for 30 days)
sudo ssh-keygen -s /etc/ssh/ca/ssh_ca \
-I "username@example.com" \
-n "username" \
-V "+30d" \
/etc/ssh/ca/user_key.pub
Copy certificate back to user
sudo cp /etc/ssh/ca/user_key-cert.pub ~/.ssh/id_ed25519_cert-cert.pub
sudo chown $(whoami):$(whoami) ~/.ssh/id_ed25519_cert-cert.pub
chmod 644 ~/.ssh/id_ed25519_cert-cert.pub
Configure SSH client for certificate authentication
Set up the SSH client to use the certificate for authentication.
Host *
CertificateFile ~/.ssh/id_ed25519_cert-cert.pub
IdentityFile ~/.ssh/id_ed25519_cert
IdentitiesOnly yes
Host production-server
HostName 203.0.113.10
User username
Port 22
Create certificate management script
Automate certificate signing with a script that includes validation and logging.
#!/bin/bash
set -euo pipefail
SSH Certificate Signing Script
Usage: sign-ssh-cert.sh username public_key_file [validity_days]
if [ $# -lt 2 ]; then
echo "Usage: $0 username public_key_file [validity_days]"
echo "Example: $0 john /tmp/john.pub 30"
exit 1
fi
USERNAME="$1"
PUBKEY_FILE="$2"
VALIDITY_DAYS="${3:-30}"
CA_KEY="/etc/ssh/ca/ssh_ca"
LOG_FILE="/var/log/ssh-cert-signing.log"
Validation
if [ ! -f "$PUBKEY_FILE" ]; then
echo "Error: Public key file $PUBKEY_FILE not found"
exit 1
fi
if [ ! -f "$CA_KEY" ]; then
echo "Error: CA private key not found at $CA_KEY"
exit 1
fi
Validate public key format
if ! ssh-keygen -l -f "$PUBKEY_FILE" > /dev/null 2>&1; then
echo "Error: Invalid public key format"
exit 1
fi
Generate certificate
CERT_FILE="${PUBKEY_FILE}-cert.pub"
echo "$(date): Signing certificate for $USERNAME, validity: $VALIDITY_DAYS days" >> "$LOG_FILE"
ssh-keygen -s "$CA_KEY" \
-I "$USERNAME@$(hostname)-$(date +%Y%m%d_%H%M%S)" \
-n "$USERNAME" \
-V "+${VALIDITY_DAYS}d" \
"$PUBKEY_FILE"
echo "Certificate created: $CERT_FILE"
echo "Valid for $VALIDITY_DAYS days"
Show certificate details
echo "\nCertificate details:"
ssh-keygen -L -f "$CERT_FILE"
Make script executable and secure
Set correct permissions on the certificate signing script.
sudo chmod 750 /usr/local/bin/sign-ssh-cert.sh
sudo chown root:root /usr/local/bin/sign-ssh-cert.sh
Create log file with proper permissions
sudo touch /var/log/ssh-cert-signing.log
sudo chmod 640 /var/log/ssh-cert-signing.log
sudo chown root:adm /var/log/ssh-cert-signing.log
Create certificate renewal script
Set up automated certificate renewal before expiration.
#!/bin/bash
set -euo pipefail
SSH Certificate Renewal Script
Checks certificate expiration and renews if needed
CERT_FILE="$HOME/.ssh/id_ed25519_cert-cert.pub"
KEY_FILE="$HOME/.ssh/id_ed25519_cert.pub"
CA_SERVER="ca-server.example.com"
USERNAME="$(whoami)"
RENEW_THRESHOLD_DAYS=7
if [ ! -f "$CERT_FILE" ]; then
echo "Certificate not found at $CERT_FILE"
exit 1
fi
Check certificate expiration
EXPIRY_DATE=$(ssh-keygen -L -f "$CERT_FILE" | grep "Valid:" | awk '{print $4}')
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
CURRENT_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - CURRENT_EPOCH) / 86400 ))
echo "Certificate expires in $DAYS_LEFT days ($EXPIRY_DATE)"
if [ "$DAYS_LEFT" -le "$RENEW_THRESHOLD_DAYS" ]; then
echo "Certificate expires within $RENEW_THRESHOLD_DAYS days. Renewing..."
# Copy public key to CA server for signing
scp "$KEY_FILE" "$CA_SERVER:/tmp/${USERNAME}_renewal.pub"
# Request certificate renewal
ssh "$CA_SERVER" "sudo /usr/local/bin/sign-ssh-cert.sh $USERNAME /tmp/${USERNAME}_renewal.pub 30"
# Download new certificate
scp "$CA_SERVER:/tmp/${USERNAME}_renewal.pub-cert.pub" "$CERT_FILE"
# Clean up temporary files
ssh "$CA_SERVER" "sudo rm -f /tmp/${USERNAME}_renewal.pub /tmp/${USERNAME}_renewal.pub-cert.pub"
echo "Certificate renewed successfully"
else
echo "Certificate is still valid"
fi
Set up automated certificate renewal
Create a cron job to automatically check and renew certificates.
chmod +x /usr/local/bin/renew-ssh-cert.sh
Add to user's crontab (runs daily at 9 AM)
echo "0 9 * /usr/local/bin/renew-ssh-cert.sh >> ~/.ssh/cert-renewal.log 2>&1" | crontab -
Verify crontab
crontab -l
Configure host certificate (optional)
Sign host certificates to prevent SSH host key verification warnings.
# Generate host certificate CA (separate from user CA for security)
sudo ssh-keygen -t ed25519 -f /etc/ssh/ca/ssh_host_ca -C "SSH-Host-CA-$(hostname)"
Sign the server's host key
sudo ssh-keygen -s /etc/ssh/ca/ssh_host_ca \
-I "$(hostname)-host-cert" \
-h \
-n "$(hostname),203.0.113.10,localhost" \
-V "+365d" \
/etc/ssh/ssh_host_ed25519_key.pub
Configure SSH to use host certificate
echo "HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub" | sudo tee -a /etc/ssh/sshd_config
sudo systemctl reload sshd
Verify your setup
Test certificate authentication and verify certificate details.
# Check certificate details
ssh-keygen -L -f ~/.ssh/id_ed25519_cert-cert.pub
Test SSH connection with certificate
ssh -v username@203.0.113.10
Check SSH logs for certificate authentication
sudo grep "Accepted publickey" /var/log/auth.log | grep -i cert
Verify CA configuration on server
sudo sshd -T | grep -i trustedusercakeys
Test certificate signing script
sudo /usr/local/bin/sign-ssh-cert.sh testuser ~/.ssh/id_ed25519_cert.pub 7
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Permission denied with certificate | Certificate expired or invalid principals | Check certificate details with ssh-keygen -L -f cert.pub and verify principals match SSH username |
| SSH ignores certificate | TrustedUserCAKeys not configured | Verify /etc/ssh/sshd_config contains correct CA public key path |
| Certificate validation failed | Clock skew between systems | Synchronize time with NTP: sudo chrony sources -v |
| SSH falls back to key authentication | Certificate file not found or wrong name | Ensure certificate file ends with -cert.pub and is in SSH config |
| CA private key permission denied | Incorrect file ownership or permissions | Set ownership: sudo chown root:root ca_key and permissions: sudo chmod 600 ca_key |
Next steps
- Configure SSH two-factor authentication with Google Authenticator TOTP for additional security layers
- Configure SSH key authentication and security hardening for baseline SSH security
- Set up centralized logging with rsyslog and logrotate for SSH audit trails
- Integrate SSH certificate authentication with LDAP for enterprise user management
- Automate SSH certificate rotation with Ansible for large-scale deployments
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# SSH Certificate Authority Setup Script
# Usage: ./setup-ssh-ca.sh [ca_name]
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
CA_NAME="${1:-SSH-CA-$(hostname)-$(date +%Y%m%d)}"
CA_DIR="/etc/ssh/ca"
SSH_CONFIG="/etc/ssh/sshd_config"
LOG_FILE="/var/log/ssh-cert-signing.log"
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
if [ -d "$CA_DIR" ]; then
rm -rf "$CA_DIR"
fi
if [ -f "${SSH_CONFIG}.backup" ]; then
mv "${SSH_CONFIG}.backup" "$SSH_CONFIG"
fi
}
trap cleanup ERR
usage() {
echo "Usage: $0 [ca_name]"
echo "Example: $0 MyCompany-SSH-CA"
exit 1
}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Detect distribution
echo "[1/8] Detecting distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
SERVICE_CMD="systemctl"
SSH_SERVICE="sshd"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
SERVICE_CMD="systemctl"
SSH_SERVICE="sshd"
;;
fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
SERVICE_CMD="systemctl"
SSH_SERVICE="sshd"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
SERVICE_CMD="systemctl"
SSH_SERVICE="sshd"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
# Update package manager
echo "[2/8] Updating package manager..."
case "$PKG_MGR" in
apt) apt update ;;
dnf|yum) $PKG_MGR makecache ;;
esac
# Install required packages
echo "[3/8] Installing required packages..."
$PKG_INSTALL openssh-server
# Backup SSH config
echo "[4/8] Backing up SSH configuration..."
cp "$SSH_CONFIG" "${SSH_CONFIG}.backup"
# Create CA directory structure
echo "[5/8] Setting up Certificate Authority..."
mkdir -p "$CA_DIR"
chmod 700 "$CA_DIR"
# Generate CA key pair
cd "$CA_DIR"
ssh-keygen -t ed25519 -f ssh_ca -C "$CA_NAME" -N ""
chmod 600 ssh_ca
chmod 644 ssh_ca.pub
# Configure SSH server for certificate authentication
echo "[6/8] Configuring SSH server..."
cat >> "$SSH_CONFIG" << EOF
# SSH Certificate Authentication Configuration
TrustedUserCAKeys $CA_DIR/ssh_ca.pub
LogLevel INFO
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
# Create certificate signing script
echo "[7/8] Creating certificate management script..."
cat > /usr/local/bin/sign-ssh-cert << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
# SSH Certificate Signing Script
CA_KEY="/etc/ssh/ca/ssh_ca"
LOG_FILE="/var/log/ssh-cert-signing.log"
if [ $# -lt 2 ]; then
echo "Usage: $0 username public_key_file [validity_days]"
echo "Example: $0 john /tmp/john.pub 30"
exit 1
fi
USERNAME="$1"
PUBKEY_FILE="$2"
VALIDITY_DAYS="${3:-30}"
# Validation
if [ ! -f "$PUBKEY_FILE" ]; then
echo "Error: Public key file $PUBKEY_FILE not found"
exit 1
fi
if [ ! -f "$CA_KEY" ]; then
echo "Error: CA private key not found at $CA_KEY"
exit 1
fi
# Validate public key format
if ! ssh-keygen -l -f "$PUBKEY_FILE" > /dev/null 2>&1; then
echo "Error: Invalid public key format"
exit 1
fi
# Generate certificate
CERT_FILE="${PUBKEY_FILE}-cert.pub"
echo "$(date): Signing certificate for $USERNAME, validity: $VALIDITY_DAYS days" >> "$LOG_FILE"
ssh-keygen -s "$CA_KEY" \
-I "$USERNAME@$(hostname)-$(date +%Y%m%d_%H%M%S)" \
-n "$USERNAME" \
-V "+${VALIDITY_DAYS}d" \
"$PUBKEY_FILE"
echo "Certificate created: $CERT_FILE"
echo "$(date): Certificate signed successfully for $USERNAME" >> "$LOG_FILE"
EOF
chmod 755 /usr/local/bin/sign-ssh-cert
# Create log file with proper permissions
touch "$LOG_FILE"
chmod 640 "$LOG_FILE"
chown root:root "$LOG_FILE"
# Configure SELinux if present
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then
echo -e "${YELLOW}Configuring SELinux contexts...${NC}"
semanage fcontext -a -t ssh_home_t "$CA_DIR(/.*)?" 2>/dev/null || true
restorecon -Rv "$CA_DIR" 2>/dev/null || true
fi
# Test SSH configuration
echo "[8/8] Validating SSH configuration..."
if ! sshd -t; then
echo -e "${RED}SSH configuration test failed${NC}"
exit 1
fi
# Restart SSH service
echo "Restarting SSH service..."
$SERVICE_CMD reload $SSH_SERVICE
$SERVICE_CMD status $SSH_SERVICE --no-pager
# Verification
echo -e "\n${GREEN}Installation completed successfully!${NC}"
echo -e "\n${YELLOW}CA Information:${NC}"
echo "CA Directory: $CA_DIR"
echo "CA Public Key: $CA_DIR/ssh_ca.pub"
echo "Certificate Signing Script: /usr/local/bin/sign-ssh-cert"
echo -e "\n${YELLOW}CA Public Key Content:${NC}"
cat "$CA_DIR/ssh_ca.pub"
echo -e "\n${YELLOW}Next Steps:${NC}"
echo "1. Distribute the CA public key to client machines"
echo "2. Generate user certificates with: sign-ssh-cert username /path/to/user.pub [days]"
echo "3. Configure SSH clients to use certificates"
echo -e "\n${YELLOW}Example SSH client config:${NC}"
cat << EOF
Host *
CertificateFile ~/.ssh/id_ed25519_cert-cert.pub
IdentityFile ~/.ssh/id_ed25519_cert
IdentitiesOnly yes
EOF
echo -e "\n${GREEN}SSH Certificate Authority setup complete!${NC}"
Review the script before running. Execute with: bash install.sh