📑 Table of Contents

This lab is structured as six interconnected modules, each building upon the previous to create a comprehensive SSH skill set. You can complete modules in order for the full experience, or jump to specific modules based on your current needs. Each module includes theoretical foundations before hands-on exercises, ensuring you understand not just how to do something, but why it works and when to apply it. Plan for 5-7 hours total, or tackle individual modules in 45-90 minute sessions.

🎯 Lab Overview & What You Will Build

This intermediate lab transforms you from someone who can use SSH keys into someone who can architect SSH-based access systems. Building on the foundations from LAB 3, you will implement a complete home lab environment featuring multiple SSH identities for work/personal separation, GitHub integration for passwordless Git operations, ssh-agent mastery for secure key caching, server hardening to eliminate password authentication, SSH tunneling for secure access to remote services, and SSH certificates following enterprise patterns used by companies like Facebook and Netflix. By the end, you'll have skills that directly translate to DevOps, Site Reliability Engineering, and Security Engineering roles.

✅ Prerequisites: Complete LAB 3 First

This lab assumes you have completed LAB 3: SSH Key-Based Authentication and understand the fundamentals of public/private key pairs, basic SSH connections, and file permissions. If you're starting fresh, complete LAB 3 first—it takes 2-3 hours and provides essential foundations.

What You Will Build

By completing all six modules, you will have:

Learning Objectives

🏢 Enterprise Scenario: Growing Into a Senior Role

You've been promoted to Senior Systems Administrator at CloudScale Inc. With the promotion comes new responsibilities: you now manage SSH access for the entire engineering team (50+ developers), ensure compliance with security audits, and architect solutions that scale. Your manager says:

"We need to eliminate password authentication across all systems, implement proper key management for the team, set up SSH certificates so we're not managing thousands of authorized_keys entries, and document SSH tunneling procedures for developers who need database access from home."

This lab teaches exactly those skills. After completion, you'll be able to:

  • Advise developers on proper SSH key management and multi-identity setups
  • Audit and harden SSH configurations to pass security reviews
  • Design certificate-based authentication for teams of any size
  • Create secure access patterns using SSH tunnels for sensitive services

🎯 Skills You Will Gain & How They Apply

Identity Architecture

Design multi-key systems that separate personal, work, and service identities. Foundation for IAM roles and credential management.

Developer Tools Integration

SSH + Git integration used daily by millions of developers. Essential for DevOps and platform engineering.

Secure Credential Caching

ssh-agent patterns apply to all credential caching: AWS STS, Kubernetes tokens, Vault leases.

Security Hardening

Server hardening skills transfer to all Linux services, containers, and cloud instances.

Network Security

SSH tunneling is the basis for secure access patterns, VPN alternatives, and service mesh concepts.

PKI & Certificate Management

SSH certificates teach PKI fundamentals that apply to TLS, code signing, and enterprise CA systems.

📋 Prerequisites & Lab Environment

This lab is designed for a home lab environment using your local machine and a Linux virtual machine. No cloud accounts are required (though Module 2 uses a free GitHub account). The entire lab can be completed offline except for GitHub integration. Budget 5-7 hours for all modules, or complete individual modules in separate sessions.

Required Components

Component Requirement Purpose
Local Machine Windows 10+, macOS 10.14+, or Linux SSH client, key generation, agent
Ubuntu VM Ubuntu 22.04 LTS, 2GB RAM, 20GB disk SSH server for hardening and tunneling
GitHub Account Free account at github.com Module 2 SSH integration
LAB 3 Completion Basic SSH key authentication working Foundation for all modules

Hypervisor Options

Set up your Ubuntu VM using either:

Device Badges Legend

Code blocks are tagged with badges indicating where to run commands:

Local Machine Your personal computer
Ubuntu VM Linux virtual machine (SSH server)
SSH Server Server being hardened/configured
GitHub GitHub web interface
CA Server Certificate Authority host

🏗️ Lab Architecture Overview

This diagram shows the complete environment you'll build across all six modules. Each colored box represents a component you'll configure, with connections showing how they interact. By the end of this lab, you'll have a fully functional SSH ecosystem demonstrating enterprise-grade practices.

▼ ADVANCED SSH LAB ARCHITECTURE ▼

🔑 MULTI-KEY SETUP

Module 1
Personal + Work + GitHub
~/.ssh/config routing

🐙 GITHUB SSH

Module 2
Passwordless Git
SSH URL format

🔐 SSH AGENT

Module 3
Key caching
Timeouts & confirmation

🛡️ HARDENED SERVER

Module 4
No passwords
Rate limiting & logging

🚇 SSH TUNNELS

Module 5
Local/Remote/Dynamic
SOCKS proxy

📜 SSH CERTIFICATES

Module 6
User CA
No authorized_keys

🔑 Module 1: Multiple SSH Keys & Identity Management

Module 1: SSH Identity Architecture

Learn to create and manage multiple SSH identities for different purposes—personal projects, work systems, and service accounts.

⏱️ 45-60 minutes 🎯 4 steps 📍 Local Machine

Using a single SSH key for everything is like using one password for all your accounts—convenient but risky. If that key is compromised, every system trusting it is vulnerable. In enterprise environments, security policies often require separate keys for different security domains. This module teaches you to create purpose-specific keys and configure SSH to automatically select the correct key based on the destination host.

📚 Understanding SSH Identity Management

When you run ssh user@host, SSH tries keys in a specific order: keys offered by ssh-agent, then default key files (~/.ssh/id_ed25519, ~/.ssh/id_rsa, etc.), then keys specified in the config file. By creating separate keys and using SSH config to map them to hosts, you gain:

  • Security Isolation: Compromising your personal key doesn't affect work systems
  • Audit Clarity: Different keys create distinct audit trails
  • Access Revocation: Removing one key doesn't require updating all systems
  • Compliance: Many security frameworks require key separation

Common Identity Patterns:

  1. Personal vs. Work: Separate keys for personal projects and employer systems
  2. Environment-based: Different keys for dev, staging, and production
  3. Service-specific: Dedicated keys for GitHub, GitLab, AWS, etc.
  4. Device-based: Unique keys per laptop/workstation for revocation
1

Create a Personal Key

First, let's create a dedicated key for personal projects. We'll use a custom filename and a comment that clearly identifies its purpose. This key will be used for personal servers, side projects, and non-work GitHub repositories.

Local Machine
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_personal -C "personal@$(hostname)-$(date +%Y)"

Explanation of flags:

  • -t ed25519 — Use the Ed25519 algorithm (modern, secure, fast)
  • -f ~/.ssh/id_ed25519_personal — Custom filename instead of default
  • -C "..." — Comment including hostname and year for identification

When prompted for a passphrase, enter a strong passphrase. For personal keys, a memorable sentence works well: "My first car was a 1997 Honda!"

2

Create a Work Key

Now create a separate key for work-related systems. This key should have a different (ideally stronger) passphrase than your personal key. In a real work environment, you might store this passphrase in your company's password manager.

Local Machine
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "work@$(hostname)-$(date +%Y)"
3

Create a GitHub-Specific Key

GitHub recommends using a dedicated SSH key for their service. This allows you to easily revoke GitHub access without affecting other systems, and provides a clear audit trail of Git operations.

Local Machine
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github -C "github@$(hostname)-$(date +%Y)"
4

Configure SSH to Use Correct Keys Automatically

Now we'll configure SSH to automatically select the appropriate key based on the destination. Create or edit your SSH config file to map hosts to keys. This is the magic that makes multi-key setups seamless—you never have to specify -i manually.

Local Machine
cat >> ~/.ssh/config << 'EOF' # ============================================ # IDENTITY MANAGEMENT CONFIGURATION # ============================================ # Default settings for all connections Host * AddKeysToAgent yes IdentitiesOnly yes ServerAliveInterval 60 ServerAliveCountMax 3 # GitHub (uses dedicated GitHub key) Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_github IdentitiesOnly yes # Work servers (example patterns) Host *.work.example.com work-* IdentityFile ~/.ssh/id_ed25519_work User workuser # Personal servers (example patterns) Host personal-* *.home.lan IdentityFile ~/.ssh/id_ed25519_personal User personaluser # Lab VM (for this lab) Host labvm HostName localhost User labuser IdentityFile ~/.ssh/id_ed25519_personal Port 22 EOF chmod 600 ~/.ssh/config

Key Configuration Options Explained:

  • IdentitiesOnly yes — Only use keys explicitly specified, don't try all keys
  • AddKeysToAgent yes — Automatically add keys to ssh-agent after first use
  • Host *.work.example.com work-* — Pattern matching for multiple hosts

✅ Module 1 Complete!

You now have three separate SSH identities with automatic key selection. Run ls -la ~/.ssh/ to verify your new keys. In Module 2, we'll register the GitHub key with your GitHub account.

🐙 Module 2: GitHub SSH Integration

Module 2: Passwordless Git with SSH

Configure GitHub to authenticate via SSH keys, eliminating password prompts and personal access tokens for Git operations.

⏱️ 30-45 minutes 🎯 5 steps 📍 Local Machine + GitHub

Every professional developer uses SSH for GitHub authentication. It's more secure than HTTPS with passwords (no credentials transmitted), more convenient than personal access tokens (no token management), and provides clear audit trails. After this module, you'll push and pull code with zero password prompts—the way millions of developers work daily.

📚 How GitHub SSH Authentication Works

When you clone a repository using SSH (git@github.com:user/repo.git), Git initiates an SSH connection to github.com on port 22 as user git. GitHub doesn't have individual Unix users—instead, it identifies you by which SSH public key you present. Each key you upload to your GitHub account is associated with your profile. When you connect:

  1. Your SSH client offers your private key to prove identity
  2. GitHub checks if the corresponding public key exists in any user's account
  3. If found, GitHub knows which user you are and grants appropriate access
  4. Git operations proceed with that user's permissions

SSH vs. HTTPS for Git:

FactorSSHHTTPS
AuthenticationKey-based (no password)Token or password
SetupOne-time key uploadCredential helper or prompts
SecurityKey never transmittedToken transmitted (encrypted)
FirewallsPort 22 (sometimes blocked)Port 443 (rarely blocked)
5

Copy Your GitHub Public Key

First, display your GitHub-specific public key so you can copy it to your clipboard. Remember, this is the public key (.pub file)—safe to share.

Local Machine
cat ~/.ssh/id_ed25519_github.pub

Select and copy the entire output. It should start with ssh-ed25519 and end with your comment.

6

Add Public Key to GitHub

Navigate to GitHub's SSH key settings and add your public key. This registers your key with your account so GitHub can identify you.

GitHub Web Interface
1. Go to: https://github.com/settings/keys 2. Click "New SSH key" button 3. Title: "Home Lab - [Your Computer Name] - [Year]" 4. Key type: "Authentication Key" 5. Key: [Paste your public key from step 5] 6. Click "Add SSH key" 7. Confirm with your GitHub password if prompted
7

Test GitHub SSH Connection

Verify your SSH connection to GitHub works. GitHub provides a special endpoint that confirms your identity without accessing any repository. The first connection will ask you to verify GitHub's host key—this is expected and safe to accept.

Local Machine
ssh -T git@github.com

Expected output: Hi [username]! You've successfully authenticated, but GitHub does not provide shell access.

If you see "Permission denied (publickey)", check that your SSH config correctly specifies the GitHub key and that you uploaded the correct public key.

8

Clone a Repository via SSH

Test the full workflow by cloning a repository using SSH. You can use any public repository. Notice the URL format: git@github.com:owner/repo.git instead of https://github.com/owner/repo.git.

Local Machine
mkdir -p ~/lab4-test && cd ~/lab4-test git clone git@github.com:octocat/Hello-World.git cd Hello-World git remote -v

The git remote -v output should show SSH URLs (starting with git@), not HTTPS URLs.

9

Convert Existing Repositories to SSH

If you have existing repositories using HTTPS, convert them to SSH by changing the remote URL. This is a one-time change per repository.

Local Machine
# View current remote (likely HTTPS) git remote -v # Convert to SSH format # Before: https://github.com/username/repo.git # After: git@github.com:username/repo.git git remote set-url origin git@github.com:USERNAME/REPO.git # Verify the change git remote -v

✅ Module 2 Complete!

Your GitHub account now recognizes your SSH key. All future git clone, git push, and git pull operations using SSH URLs will authenticate automatically. No more password prompts!

🔐 Module 3: Mastering SSH Agent

Module 3: Secure Key Caching with ssh-agent

Learn to cache decrypted keys in memory, configure automatic timeouts, and require confirmation for sensitive operations.

⏱️ 45-60 minutes 🎯 6 steps 📍 Local Machine

If you set passphrases on your SSH keys (as you should), typing that passphrase for every SSH connection quickly becomes tedious. The ssh-agent solves this by securely caching your decrypted keys in memory. You type your passphrase once when adding the key, and the agent handles authentication for subsequent connections. This module teaches you to use ssh-agent effectively while maintaining security through timeouts and confirmation prompts.

📚 How SSH Agent Works

The ssh-agent is a background process that holds your private keys in memory after you unlock them with your passphrase. When SSH needs to authenticate:

  1. SSH client contacts the agent via a Unix socket
  2. Client asks agent to sign an authentication challenge
  3. Agent signs using the stored key (key never leaves agent memory)
  4. Signed response proves identity to the server

Key Security Concepts:

  • Memory-only storage: Keys exist only in RAM, never written to disk in decrypted form
  • Socket-based communication: Only processes with access to the socket can use the agent
  • Request-based signing: The private key never leaves the agent; only signatures do
  • Lifetime limits: Keys can auto-expire after a configurable time

Agent Forwarding Warning: The -A flag (agent forwarding) allows remote servers to use your agent. This is convenient but dangerous—a compromised server can use your agent to authenticate elsewhere. We'll cover safer alternatives.

10

Verify SSH Agent is Running

Most modern systems start ssh-agent automatically, but let's verify it's running and understand how to start it manually if needed.

Local Machine
# Check if agent is running echo $SSH_AUTH_SOCK # List currently loaded keys (probably empty) ssh-add -l # If agent is not running, start it: eval "$(ssh-agent -s)"

If $SSH_AUTH_SOCK shows a path (like /tmp/ssh-xxx/agent.xxx), the agent is running. If ssh-add -l shows "The agent has no identities," no keys are loaded yet—that's expected.

11

Add Keys to the Agent

Add your keys to the agent. You'll be prompted for each key's passphrase. After entering it once, you won't need to enter it again until the key expires or is removed.

Local Machine
# Add personal key ssh-add ~/.ssh/id_ed25519_personal # Add GitHub key ssh-add ~/.ssh/id_ed25519_github # Add work key ssh-add ~/.ssh/id_ed25519_work # Verify keys are loaded ssh-add -l
12

Add Keys with Lifetime Limits

For better security, add keys with automatic expiration. This limits the window of vulnerability if your machine is compromised while you're away. The -t flag sets the lifetime in seconds.

Local Machine
# Remove all keys first ssh-add -D # Add keys with 4-hour lifetime (14400 seconds) ssh-add -t 14400 ~/.ssh/id_ed25519_personal ssh-add -t 14400 ~/.ssh/id_ed25519_github # Add work key with 8-hour lifetime (work day) ssh-add -t 28800 ~/.ssh/id_ed25519_work # Verify keys and notice they don't show expiration in list ssh-add -l

Recommended Lifetimes:

  • Personal keys: 4-8 hours (or session length)
  • Work keys: 8-12 hours (work day)
  • Sensitive keys: 1-2 hours or use confirmation
13

Add Keys with Confirmation Requirement

For high-security keys, require confirmation before each use. This prevents automated processes (including malware) from silently using your keys. The -c flag enables confirmation mode.

Local Machine
# Add a key requiring confirmation before each use ssh-add -c ~/.ssh/id_ed25519_work # When this key is used, you'll see a GUI prompt # (or terminal prompt) asking to confirm

🔒 When to Use Confirmation Mode

Confirmation mode is recommended for keys that access:

  • Production systems with customer data
  • Financial systems or payment processing
  • Certificate authorities or signing keys
  • Any system where unauthorized access would be catastrophic
14

Configure Automatic Agent Key Loading

On macOS, you can configure SSH to automatically add keys to the agent and use the system keychain for passphrase storage. On Linux, we'll use the AddKeysToAgent directive to add keys on first use.

Local Machine
# Update SSH config for automatic key loading cat >> ~/.ssh/config << 'EOF' # ============================================ # SSH AGENT CONFIGURATION # ============================================ Host * # Add keys to agent on first use AddKeysToAgent yes # macOS only: Use Keychain for passphrases # UseKeychain yes # Default key lifetime when auto-adding (8 hours) # Note: This requires OpenSSH 8.4+ # AddKeysToAgent 28800 EOF
15

Agent Management Commands Reference

Here's a reference of common ssh-agent and ssh-add commands you'll use regularly:

Local Machine
# List all loaded keys ssh-add -l # List keys with full public key (for matching) ssh-add -L # Remove a specific key ssh-add -d ~/.ssh/id_ed25519_personal # Remove ALL keys (use when leaving workstation) ssh-add -D # Lock the agent with a password ssh-add -x # Unlock the agent ssh-add -X # Add key with both timeout AND confirmation ssh-add -c -t 3600 ~/.ssh/id_ed25519_sensitive

✅ Module 3 Complete!

You now understand ssh-agent and can configure keys with lifetimes and confirmation requirements. Your daily workflow will be: unlock keys once at login, use them seamlessly all day, and have them auto-expire for security.

🛡️ Module 4: SSH Server Hardening

Module 4: Production-Grade SSH Security

Transform a default SSH installation into a hardened, audit-ready configuration following industry best practices.

⏱️ 60-90 minutes 🎯 8 steps 📍 Ubuntu VM

A default SSH server installation prioritizes convenience over security—password authentication enabled, root login allowed, no rate limiting. In production environments, this is unacceptable. This module transforms your Ubuntu VM into a hardened SSH bastion following CIS benchmarks, Mozilla security guidelines, and real-world enterprise standards. You'll disable password authentication, implement rate limiting, configure audit logging, and create a server that would pass a security review.

📚 SSH Hardening Philosophy

SSH hardening follows the principle of defense in depth—multiple security layers so that if one fails, others still protect the system:

  1. Authentication hardening: Disable weak methods (passwords), allow only strong ones (keys)
  2. Authorization restrictions: Limit which users can SSH and from where
  3. Protocol hardening: Disable weak algorithms, use modern ciphers
  4. Rate limiting: Prevent brute force attacks
  5. Monitoring: Log everything for audit and incident response

Industry Standards We'll Follow:

  • CIS Benchmark for Linux: Center for Internet Security's hardening guide
  • Mozilla SSH Guidelines: Modern, regularly updated recommendations
  • NIST SP 800-53: Federal security controls framework

⚠️ Important: Don't Lock Yourself Out!

Before disabling password authentication, verify key-based authentication works. Test in a second terminal before closing your current session. Always maintain a backup access method (VM console, recovery boot, etc.).

16

Deploy Your Public Key to the VM

Before hardening, ensure you can access the VM via SSH key. From your local machine, copy your personal key to the VM. If you completed LAB 3, this may already be done.

Local Machine
# Replace 'labuser' and 'vm-ip' with your values ssh-copy-id -i ~/.ssh/id_ed25519_personal labuser@vm-ip # Test key authentication ssh -i ~/.ssh/id_ed25519_personal labuser@vm-ip "echo 'Key auth works!'"
17

Backup the Current SSH Configuration

Always backup configuration files before making changes. This allows quick recovery if something goes wrong.

SSH Server (Ubuntu VM)
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d) ls -la /etc/ssh/sshd_config*
18

Create Hardened SSH Configuration

Create a new, hardened SSH server configuration file. This configuration follows industry best practices and will be explained line by line.

SSH Server (Ubuntu VM)
sudo tee /etc/ssh/sshd_config.d/hardening.conf << 'EOF' # ============================================ # SSH SERVER HARDENING CONFIGURATION # Identity Bytes Lab 4 - Module 4 # Based on CIS Benchmark & Mozilla Guidelines # ============================================ # -------------------------------------------- # AUTHENTICATION SETTINGS # -------------------------------------------- # Disable password authentication (keys only) PasswordAuthentication no PermitEmptyPasswords no # Disable root login (use sudo instead) PermitRootLogin no # Enable public key authentication PubkeyAuthentication yes # Disable other authentication methods ChallengeResponseAuthentication no KerberosAuthentication no GSSAPIAuthentication no # Only allow specific users (uncomment and customize) # AllowUsers labuser admin # Only allow specific groups (uncomment and customize) # AllowGroups sshusers # -------------------------------------------- # PROTOCOL SETTINGS # -------------------------------------------- # Use only SSH Protocol 2 Protocol 2 # Restrict key exchange algorithms to secure options KexAlgorithms curve25519-sha256@libssh.org,curve25519-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 # Restrict ciphers to modern, secure options Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com # Restrict MACs to secure options MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com # Restrict host key algorithms HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # -------------------------------------------- # SESSION SETTINGS # -------------------------------------------- # Set idle timeout (5 minutes) ClientAliveInterval 300 ClientAliveCountMax 2 # Maximum authentication attempts per connection MaxAuthTries 3 # Maximum number of concurrent sessions MaxSessions 3 # Maximum number of concurrent unauthenticated connections MaxStartups 10:30:60 # -------------------------------------------- # SECURITY RESTRICTIONS # -------------------------------------------- # Disable X11 forwarding (unless needed) X11Forwarding no # Disable agent forwarding by default AllowAgentForwarding no # Disable TCP forwarding by default (enable per-user if needed) AllowTcpForwarding no # Disable stream local forwarding AllowStreamLocalForwarding no # Disable tunneling PermitTunnel no # Strict mode (check file permissions) StrictModes yes # -------------------------------------------- # LOGGING AND AUDITING # -------------------------------------------- # Enhanced logging for audit trails LogLevel VERBOSE # Log SFTP access Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO # -------------------------------------------- # BANNER AND INFORMATION # -------------------------------------------- # Display legal warning banner Banner /etc/ssh/banner.txt # Don't show last login PrintLastLog yes # Show message of the day PrintMotd no EOF echo "Hardening config created"
19

Create Legal Warning Banner

A login banner provides legal notice and may deter casual attackers. Many compliance frameworks require this. The banner is displayed before authentication.

SSH Server (Ubuntu VM)
sudo tee /etc/ssh/banner.txt << 'EOF' ******************************************************************* * AUTHORIZED ACCESS ONLY * ******************************************************************* * This system is for authorized users only. All activity is * * monitored and logged. Unauthorized access is prohibited and * * will be prosecuted to the fullest extent of the law. * * * * By accessing this system, you consent to monitoring and agree * * that you have no expectation of privacy. * ******************************************************************* EOF
20

Validate and Apply Configuration

Before applying, validate the configuration syntax. A syntax error could prevent SSH from starting, locking you out. Then apply the configuration by restarting the SSH service.

SSH Server (Ubuntu VM)
# Validate configuration syntax sudo sshd -t # If no errors, restart SSH service sudo systemctl restart sshd # Check service status sudo systemctl status sshd

⚠️ Test Before Closing Your Session!

Open a NEW terminal and test SSH key authentication before closing your current session:

ssh -i ~/.ssh/id_ed25519_personal labuser@vm-ip
21

Install and Configure Fail2ban

Fail2ban monitors logs for failed authentication attempts and temporarily bans IP addresses that show malicious behavior. This prevents brute force attacks even if password authentication is re-enabled accidentally.

SSH Server (Ubuntu VM)
# Install fail2ban sudo apt update && sudo apt install -y fail2ban # Create SSH jail configuration sudo tee /etc/fail2ban/jail.d/sshd.local << 'EOF' [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 findtime = 600 ignoreip = 127.0.0.1/8 ::1 EOF # Restart fail2ban sudo systemctl restart fail2ban # Check status sudo fail2ban-client status sshd
22

Verify Password Authentication is Disabled

Test that password authentication is truly disabled. This attempt should fail immediately without even prompting for a password.

Local Machine
# Try to authenticate with password (should fail immediately) ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no labuser@vm-ip # Expected output: "Permission denied (publickey)."
23

View SSH Security Logs

Check the SSH logs to see authentication attempts and verify logging is working. These logs are essential for security monitoring and incident investigation.

SSH Server (Ubuntu VM)
# View recent SSH authentication logs sudo journalctl -u sshd --since "10 minutes ago" # View auth.log for SSH entries sudo grep sshd /var/log/auth.log | tail -20 # View fail2ban logs sudo tail -20 /var/log/fail2ban.log

✅ Module 4 Complete!

Your SSH server is now hardened to production standards. Password authentication is disabled, root login is prohibited, secure ciphers are enforced, and fail2ban protects against brute force attacks. This configuration would pass most security audits.

🚇 Module 5: SSH Tunneling & Port Forwarding

Module 5: Secure Access via SSH Tunnels

Master local, remote, and dynamic port forwarding to securely access services through encrypted SSH connections.

⏱️ 60-90 minutes 🎯 7 steps 📍 Local Machine + VM

SSH tunneling (port forwarding) is one of SSH's most powerful features, allowing you to securely access services that aren't directly exposed to the internet. Need to access a database that only accepts connections from localhost? SSH tunnel. Want to browse the web through a remote server? SSH SOCKS proxy. This module teaches all three types of tunneling with practical scenarios you'll encounter in real work.

📚 Understanding SSH Tunneling Types

SSH provides three types of port forwarding, each solving different problems:

1. Local Port Forwarding (-L)

Forwards a port on your local machine to a destination through the SSH server. Traffic flows: Local → SSH Server → Destination

Use case: Access a remote database that only accepts localhost connections

2. Remote Port Forwarding (-R)

Forwards a port on the remote server to a destination through your local machine. Traffic flows: Remote → SSH Tunnel → Local → Destination

Use case: Expose a local development server to the internet temporarily

3. Dynamic Port Forwarding (-D)

Creates a SOCKS proxy on your local machine that routes all traffic through the SSH server. Traffic flows: Local App → SOCKS → SSH Server → Internet

Use case: Browse the web as if you were at the remote server's location

⚠️ Re-enable TCP Forwarding for This Module

In Module 4, we disabled TCP forwarding for security. To practice tunneling, temporarily enable it on your VM:

24

Enable TCP Forwarding on the Server

Temporarily enable TCP forwarding on your hardened server. In production, you would enable this selectively per-user or create a separate SSH configuration for jump hosts.

SSH Server (Ubuntu VM)
# Create tunneling configuration override sudo tee /etc/ssh/sshd_config.d/tunneling.conf << 'EOF' # Enable TCP forwarding for lab exercises # In production, enable per-user with Match blocks AllowTcpForwarding yes GatewayPorts no EOF # Restart SSH sudo systemctl restart sshd
25

Set Up a Test Service on the VM

Create a simple web server on the VM that only listens on localhost. This simulates a service (like a database admin panel) that isn't exposed to the network.

SSH Server (Ubuntu VM)
# Create a simple web page mkdir -p ~/webtest echo "

Success! You accessed this via SSH tunnel.

This server only listens on localhost:8080

" > ~/webtest/index.html # Start a simple HTTP server (Python 3) bound to localhost only cd ~/webtest python3 -m http.server 8080 --bind 127.0.0.1 & # Verify it's running and only on localhost ss -tlnp | grep 8080

The web server is now running on port 8080, but it only accepts connections from 127.0.0.1 (localhost on the VM). You cannot access it directly from your local machine.

26

Local Port Forwarding: Access Remote Localhost Service

Use local port forwarding to access the VM's localhost-only web server from your local machine. This forwards your local port 9080 to the VM's localhost:8080.

Local Machine
# Create local port forward # -L localport:destination:destport ssh -L 9080:127.0.0.1:8080 labuser@vm-ip -N & # The -N flag means "don't execute a command" (tunnel only) # The & runs it in background # Now access the remote service via your local port curl http://localhost:9080 # Or open in browser: http://localhost:9080

▼ LOCAL PORT FORWARDING FLOW ▼

YOUR BROWSER

localhost:9080

SSH TUNNEL

encrypted

SSH SERVER

receives request

WEB SERVER

127.0.0.1:8080

27

Dynamic Port Forwarding: SOCKS Proxy

Create a SOCKS proxy that routes all traffic through the SSH server. This is useful for browsing the web as if you were at the server's location, or for accessing multiple services behind a firewall.

Local Machine
# Create SOCKS proxy on local port 1080 ssh -D 1080 labuser@vm-ip -N & # Configure curl to use the SOCKS proxy curl --socks5 localhost:1080 http://ifconfig.me # This shows the VM's public IP, not your local IP # To use with browser: # Firefox: Settings → Network → Manual proxy → SOCKS Host: localhost, Port: 1080 # Chrome: Requires extension or command-line flag
28

Remote Port Forwarding: Expose Local Service

Remote port forwarding exposes a service on your local machine through the SSH server. This is useful for sharing a local development server with a colleague or webhook testing.

Local Machine
# First, start a local web server mkdir -p ~/local-web && cd ~/local-web echo "

Hello from my local machine!

" > index.html python3 -m http.server 8000 & # Now expose it through the VM # -R remoteport:localhost:localport ssh -R 9000:localhost:8000 labuser@vm-ip -N & # On the VM, curl localhost:9000 would show your local page
29

Persistent Tunnels with SSH Config

For tunnels you use frequently, configure them in ~/.ssh/config so you can establish them with a simple command.

Local Machine
cat >> ~/.ssh/config << 'EOF' # ============================================ # SSH TUNNELING SHORTCUTS # ============================================ # Quick database access tunnel Host db-tunnel HostName vm-ip User labuser IdentityFile ~/.ssh/id_ed25519_personal LocalForward 5432 localhost:5432 LocalForward 3306 localhost:3306 # SOCKS proxy for web browsing Host socks-proxy HostName vm-ip User labuser IdentityFile ~/.ssh/id_ed25519_personal DynamicForward 1080 # Web admin tunnel Host web-tunnel HostName vm-ip User labuser IdentityFile ~/.ssh/id_ed25519_personal LocalForward 9080 127.0.0.1:8080 EOF # Usage: ssh -N web-tunnel &
30

Clean Up Tunnel Processes

When you're done with tunnels, clean up the background SSH processes.

Local Machine
# Find SSH tunnel processes ps aux | grep "ssh -[LDR]" # Kill all SSH tunnel processes pkill -f "ssh -[LDR]" # Or kill specific tunnel by port # Find PID: lsof -i :9080 # Kill: kill PID

✅ Module 5 Complete!

You now understand all three types of SSH tunneling. These techniques are used daily by developers and administrators to securely access databases, internal services, and test webhooks.

📜 Module 6: SSH Certificates (Enterprise Pattern)

Module 6: Certificate-Based Authentication

Implement an SSH Certificate Authority to issue time-limited user certificates—the same pattern used by major tech companies.

⏱️ 60-90 minutes 🎯 8 steps 📍 Local Machine + VM

Managing SSH keys at scale is challenging: authorized_keys files on hundreds of servers, key rotation across thousands of users, no built-in expiration. SSH certificates solve these problems elegantly. Instead of copying public keys to every server, you create a Certificate Authority (CA) that signs user keys. Servers trust the CA, so any user with a valid certificate can authenticate. Facebook, Netflix, and many other large companies use this pattern. This module teaches you to set up your own SSH CA.

📚 SSH Certificates vs. Static Keys

Understanding the difference between traditional keys and certificates:

Traditional SSH Keys (What We've Used)
  • Public key copied to each server's authorized_keys
  • No expiration (keys valid until manually removed)
  • Scaling problem: N users × M servers = N×M authorized_keys entries
  • Revocation requires updating every server
SSH Certificates (Enterprise Pattern)
  • CA public key installed once on each server
  • Users receive signed certificates valid for limited time
  • Scaling: 1 CA key × M servers + N user certificates
  • Automatic expiration (no revocation needed for time-limited certs)
  • Centralized access control (change CA signing policy, not servers)
Certificate Contents

An SSH certificate contains:

  • The user's public key (what they're authenticating with)
  • Valid principals (which usernames can use this certificate)
  • Validity period (from/to timestamps)
  • Extensions and restrictions (e.g., no port forwarding)
  • CA's signature (proves the CA issued this certificate)
31

Create the Certificate Authority Key Pair

First, create a dedicated key pair for your SSH Certificate Authority. This CA key will sign user certificates. Protect this key carefully—anyone with access to it can create certificates trusted by all servers.

Local Machine
# Create directory for CA materials mkdir -p ~/.ssh/ca # Generate CA key pair (use a strong passphrase!) ssh-keygen -t ed25519 -f ~/.ssh/ca/user_ca -C "SSH User CA - Lab 4" # Set restrictive permissions chmod 700 ~/.ssh/ca chmod 600 ~/.ssh/ca/user_ca # View the CA public key cat ~/.ssh/ca/user_ca.pub

🔒 Protecting the CA Key

In production, the CA private key should be stored in a Hardware Security Module (HSM) or air-gapped system. For this lab, we're keeping it on your local machine, but treat it as highly sensitive.

32

Configure the Server to Trust the CA

Install the CA public key on your server and configure SSHD to trust certificates signed by this CA. This is a one-time setup per server.

SSH Server (Ubuntu VM)
# First, copy the CA public key to the server # Run this on your LOCAL machine: scp ~/.ssh/ca/user_ca.pub labuser@vm-ip:/tmp/user_ca.pub # Then on the SERVER, install and configure it: sudo mv /tmp/user_ca.pub /etc/ssh/user_ca.pub sudo chmod 644 /etc/ssh/user_ca.pub # Add CA trust configuration sudo tee /etc/ssh/sshd_config.d/certificates.conf << 'EOF' # Trust certificates signed by our User CA TrustedUserCAKeys /etc/ssh/user_ca.pub # Optional: Specify which principals are allowed for which users # AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u EOF # Restart SSH sudo systemctl restart sshd
33

Sign a User Certificate

Now the exciting part—sign your existing public key to create a certificate. This certificate proves your key was approved by the CA and specifies which username(s) you can authenticate as.

CA Server (Local Machine)
# Sign your personal public key # -s: CA private key (signs the certificate) # -I: Key ID (shows in logs, for identification) # -n: Principals (usernames allowed) # -V: Validity period (+1d = 1 day from now) ssh-keygen -s ~/.ssh/ca/user_ca \ -I "labuser@myworkstation" \ -n labuser \ -V +1d \ ~/.ssh/id_ed25519_personal.pub # This creates: ~/.ssh/id_ed25519_personal-cert.pub ls -la ~/.ssh/id_ed25519_personal-cert.pub

Understanding the signing options:

  • -s ~/.ssh/ca/user_ca — Use this CA key to sign
  • -I "labuser@myworkstation" — Key ID for audit logs
  • -n labuser — This cert can authenticate as "labuser"
  • -V +1d — Valid for 1 day (production might use hours)
34

Examine the Certificate

Before using the certificate, let's examine its contents to understand what we created. The certificate contains metadata about validity, principals, and restrictions.

Local Machine
# View certificate details ssh-keygen -L -f ~/.ssh/id_ed25519_personal-cert.pub

Key fields in the output:

  • Type: ssh-ed25519-cert-v01@openssh.com user certificate
  • Public key: Your original public key (embedded)
  • Signing CA: Fingerprint of the CA that signed
  • Key ID: The identifier you specified
  • Valid: Time window when cert is usable
  • Principals: Usernames you can authenticate as
35

Test Certificate-Based Authentication

Now test authentication using your certificate. SSH will automatically use the certificate file if it exists alongside your private key. You can also explicitly specify it.

Local Machine
# Test with verbose output to see certificate being used ssh -v -i ~/.ssh/id_ed25519_personal labuser@vm-ip "echo 'Certificate auth successful!'" # Look for this line in the output: # "Offering public key: ... ED25519-CERT"
36

Create Certificates with Restrictions

Certificates can include restrictions that limit what the user can do. This is useful for service accounts or limited-access scenarios.

CA Server (Local Machine)
# Create a restricted certificate (no forwarding, no PTY) ssh-keygen -s ~/.ssh/ca/user_ca \ -I "restricted-user" \ -n labuser \ -V +1h \ -O no-port-forwarding \ -O no-x11-forwarding \ -O no-agent-forwarding \ -O no-pty \ -O source-address=192.168.0.0/16 \ ~/.ssh/id_ed25519_work.pub # View the restrictions ssh-keygen -L -f ~/.ssh/id_ed25519_work-cert.pub

Available restrictions:

  • no-port-forwarding — Disable all port forwarding
  • no-pty — Prevent getting a terminal (useful for rsync/scp only access)
  • no-agent-forwarding — Disable agent forwarding
  • source-address=CIDR — Only allow connections from specific IPs
  • force-command=CMD — Force a specific command on connect
37

Certificate Expiration and Renewal

Certificates automatically expire after their validity period. When expired, the user must request a new certificate from the CA. This is a feature, not a bug—it ensures regular re-authorization.

Local Machine
# Check remaining validity ssh-keygen -L -f ~/.ssh/id_ed25519_personal-cert.pub | grep Valid # Create a certificate renewal script cat > ~/renew-cert.sh << 'EOF' #!/bin/bash # Simple certificate renewal script # In production, this would be a secure service CERT_VALIDITY="+8h" CA_KEY="$HOME/.ssh/ca/user_ca" USER_KEY="$HOME/.ssh/id_ed25519_personal.pub" ssh-keygen -s "$CA_KEY" \ -I "$(whoami)@$(hostname)" \ -n labuser \ -V "$CERT_VALIDITY" \ "$USER_KEY" echo "Certificate renewed, valid for 8 hours" ssh-keygen -L -f "${USER_KEY%.pub}-cert.pub" | grep Valid EOF chmod +x ~/renew-cert.sh
38

Remove Traditional authorized_keys (Optional)

With certificate-based authentication working, you can remove traditional authorized_keys entries. The server now authenticates based on the CA signature, not individual public keys.

SSH Server (Ubuntu VM)
# Backup and clear authorized_keys cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.backup echo "" > ~/.ssh/authorized_keys # Test that certificate auth still works # (Run from local machine) # ssh labuser@vm-ip "echo 'Still works without authorized_keys!'" # Note: Keep the backup until you're confident certificates are working

✅ Module 6 Complete!

You've implemented SSH certificate-based authentication! This enterprise pattern eliminates authorized_keys management, provides automatic expiration, and enables centralized access control. You now understand how companies like Facebook and Netflix manage SSH access at scale.

🛡️ Common Vulnerabilities & Best Practices

This lab covers advanced SSH techniques that introduce new attack surfaces. Understanding these vulnerabilities and their mitigations is essential for secure implementation. This section covers risks specific to the technologies in this lab: multi-key management, agent forwarding, server hardening gaps, tunneling abuse, and certificate authority compromise.

CRITICAL

Certificate Authority Key Compromise

If the CA private key is stolen, attackers can issue certificates for any user, bypassing all authentication on every server trusting that CA.

⚔️ Attack Scenario

An attacker gains access to the system storing the CA key. They issue themselves a certificate for "root" on all servers. Every server trusting the CA accepts their certificate without question.

🛡️ Mitigation
  • Store CA keys in Hardware Security Modules (HSMs)
  • Use offline/air-gapped systems for CA operations
  • Implement multi-person authorization for signing
  • Issue short-lived certificates (hours, not days)
  • Monitor certificate issuance with audit logs
HIGH

SSH Agent Hijacking

A malicious process on your local machine could connect to your ssh-agent socket and use your cached keys to authenticate to remote systems.

⚔️ Attack Scenario

You download a compromised npm package that runs malicious code. The malware detects your ssh-agent socket, uses it to authenticate to GitHub, and pushes malicious code to your repositories.

🛡️ Mitigation
  • Use ssh-add -c for confirmation on each use
  • Set key lifetimes with ssh-add -t
  • Lock agent when leaving workstation (ssh-add -x)
  • Use separate agents for different security levels
HIGH

Tunnel Abuse for Lateral Movement

SSH tunnels can be used by attackers for command-and-control channels or to pivot to internal networks.

⚔️ Attack Scenario

An attacker compromises a server with SSH access. They create a reverse tunnel to their C2 server, establishing persistent access that bypasses firewalls and looks like normal SSH traffic.

🛡️ Mitigation
  • Disable AllowTcpForwarding except where needed
  • Use Match blocks to restrict forwarding per-user
  • Monitor for unusual SSH session durations
  • Implement network segmentation
MEDIUM

Multi-Key Confusion Attacks

With multiple keys, users may accidentally use the wrong key, potentially exposing work credentials to personal systems or vice versa.

⚔️ Attack Scenario

A user's SSH config doesn't correctly route keys. They accidentally use their personal key for a work server, creating an unauthorized authentication path. Or worse, they upload a work key to a public GitHub account.

🛡️ Mitigation
  • Use IdentitiesOnly yes in SSH config
  • Test key routing with ssh -v before production use
  • Use distinct key comments for easy identification
  • Implement key separation policies in documentation

✅ Advanced SSH Security Checklist

  • ✅ Separate SSH keys by security domain (personal/work/service)
  • ✅ Use ssh-agent with confirmation (-c) for sensitive keys
  • ✅ Set key lifetimes in agent (-t) to limit exposure window
  • ✅ Disable password authentication on all servers
  • ✅ Disable AllowTcpForwarding unless specifically required
  • ✅ Implement fail2ban or similar rate limiting
  • ✅ Use SSH certificates for environments with >10 users or servers
  • ✅ Store CA keys in HSMs or offline systems
  • ✅ Issue short-lived certificates (8 hours or less)
  • ✅ Monitor SSH logs for anomalies
  • ✅ Regularly audit authorized_keys and CA-signed certificates

⚠️ Lab vs Production Configuration

Setting Lab Value Production Value
CA Key Storage Local filesystem HSM or air-gapped system
Certificate Validity 1 day (learning) 4-8 hours maximum
AllowTcpForwarding Enabled (for Module 5) Disabled or per-user only
SSH Agent Confirmation Optional Required for production keys
Key Lifetime in Agent Unlimited (convenience) 4-8 hours

🔧 Troubleshooting Guide

Advanced SSH configurations have more failure points than basic setups. This section covers troubleshooting techniques for the specific technologies in this lab.

Issue: "Permission denied" after enabling certificates

Root Cause: Certificate not being used, or principals don't match.

Diagnostic Steps:

  • Verify certificate exists: ls ~/.ssh/*-cert.pub
  • Check certificate is valid: ssh-keygen -L -f ~/.ssh/key-cert.pub | grep Valid
  • Verify principals: ssh-keygen -L -f ~/.ssh/key-cert.pub | grep Principals
  • Check server trusts CA: grep TrustedUserCAKeys /etc/ssh/sshd_config*
  • Use verbose mode: ssh -vvv user@server and look for certificate messages

Issue: SSH tunnel not working / "Connection refused"

Root Cause: TCP forwarding disabled or service not listening.

Diagnostic Steps:

  • Check AllowTcpForwarding: sshd -T | grep allowtcpforwarding
  • Verify service is running: ss -tlnp | grep PORT
  • Test locally first: curl localhost:PORT on the server
  • Check local port isn't already in use: lsof -i :LOCALPORT

Issue: Wrong SSH key being used

Root Cause: SSH config not routing correctly or agent offering wrong key first.

Diagnostic Steps:

  • Check which key is offered: ssh -v user@host 2>&1 | grep "Offering"
  • Verify config: ssh -G hostname | grep identityfile
  • Ensure IdentitiesOnly yes is set in config
  • List agent keys: ssh-add -l

🎓 Key Takeaways

Congratulations on completing this comprehensive lab! You've progressed from basic SSH usage to implementing enterprise-grade patterns. Here's a summary of the skills you've mastered.

Skills Mastered

What's Next

📚 Additional Learning Resources

Continue your SSH mastery with these carefully selected resources covering advanced topics, enterprise implementations, and emerging best practices.

📖 Official Documentation
🏢 Enterprise Implementation Guides
🔐 Security Standards
🔧 Tools & Automation
  • Netflix BLESS AWS Lambda-based SSH certificate authority with OIDC integration.
  • Uber pam-ussh PAM module for SSH certificate authentication with Okta/Google integration.
  • SSH Audit Online tool to audit SSH server configurations against security best practices.
📚 Books & Deep Dives
💻 Practice & Challenges

💡 Practice Recommendations

  • Set up Vault SSH: Install HashiCorp Vault and configure the SSH secrets engine for automated certificate issuance
  • Build a bastion host: Create a hardened jump server with ProxyJump configuration for all internal access
  • Implement certificate rotation: Script automated certificate renewal with shorter validity periods (4-8 hours)
  • Set up monitoring: Configure ELK or similar to aggregate and alert on SSH authentication logs
  • Test your hardening: Use ssh-audit.com to scan your server and address any findings
  • Practice incident response: Simulate a CA key compromise and practice certificate revocation procedures