DEV Community

Matheus
Matheus

Posted on • Originally published at releaserun.com

SSH and SCP in 2026: Configuration, Security Hardening, and Advanced Tips

SSH and SCP: What They Are and Why They Still Matter

SSH (Secure Shell) is the standard protocol for encrypted remote access to Linux and Unix systems. Git over SSH, CI/CD deploy pipelines, tunneling database connections, and ad hoc server administration all run on top of it. SCP (Secure Copy Protocol) is the companion tool for transferring files over an SSH connection. While OpenSSH has deprecated the legacy SCP protocol in favor of SFTP internals, the scp command-line interface remains widely used.

This guide covers SSH and SCP usage in 2026: modern key types, effective SSH configurations, server hardening, tunneling, and CI/CD integration. Whether you manage a handful of virtual machines or orchestrate access across a fleet of Kubernetes nodes, the fundamentals here apply.

SSH Fundamentals: Protocol and Authentication

SSH operates as a client-server protocol over TCP, defaulting to port 22. When you run ssh user@host, the following sequence occurs under the hood:

  1. TCP connection. The client opens a TCP connection to the server on port 22 (or whatever port is configured).
  2. Protocol version exchange. Both sides announce supported SSH protocol versions. Modern systems use version 2 exclusively.
  3. Key exchange (KEX). The client and server negotiate a shared session key. In OpenSSH 9.x, the default is sntrup761x25519-sha512@openssh.com, a hybrid algorithm combining classical X25519 elliptic-curve Diffie-Hellman with the NTRU Prime post-quantum key encapsulation mechanism. Even if a quantum computer eventually breaks X25519, the session key remains secure as long as NTRU Prime holds.
  4. Server authentication. The server presents its host key. The client checks this against its known_hosts file. If the key has changed, the client refuses to connect -- protection against man-in-the-middle attacks.
  5. User authentication. The client proves its identity using public key authentication, password, keyboard-interactive, GSSAPI (Kerberos), or certificate-based authentication.
  6. Session establishment. The server allocates a pseudo-terminal or executes a command. All traffic is encrypted with the negotiated session key.

Authentication Methods in Practice

Public key authentication is the standard for production environments. Password authentication is acceptable for initial setup but should be disabled once keys are configured.

  • Public key authentication -- The client proves possession of a private key through a cryptographic challenge-response without transmitting the key itself. The default and recommended method.
  • Password authentication -- The client sends a password over the encrypted channel. Vulnerable to brute-force attacks. Should be disabled on internet-facing servers.
  • Certificate-based authentication -- Both host and user present certificates signed by a trusted CA, eliminating the need to distribute authorized_keys and known_hosts across your infrastructure. Covered in the CI/CD section below.
  • FIDO2/WebAuthn hardware keys -- OpenSSH 8.2+ supports FIDO2 security keys (YubiKey, SoloKey, Google Titan) as SSH keys. The private key material never leaves the hardware device.

Key Management: Generating, Storing, and Rotating SSH Keys

Generating Ed25519 Keys

Ed25519 is the recommended key type for SSH in 2026. It produces compact 256-bit keys that are faster to generate and verify than RSA, with a simpler implementation and fewer opportunities for side-channel attacks. Unless a specific compatibility requirement forces RSA, use Ed25519.

# Generate an Ed25519 key pair
ssh-keygen -t ed25519 -C "yourname@example.com"

# If you need to specify a custom path
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "yourname@company.com"

# For legacy systems that require RSA, use at minimum 4096 bits
ssh-keygen -t rsa -b 4096 -C "yourname@example.com"
Enter fullscreen mode Exit fullscreen mode

Always set a passphrase on your private key. A passphrase-protected key means that even if someone obtains the key file, they cannot use it without the passphrase. Combined with ssh-agent, you only need to type the passphrase once per session.

ssh-agent: Avoiding Passphrase Fatigue

ssh-agent is a daemon that holds your decrypted private keys in memory. Once you add a key to the agent, you can authenticate to any server that trusts that key without re-entering your passphrase.

# Start the agent (many desktop environments do this automatically)
eval "$(ssh-agent -s)"

# Add your key (will prompt for passphrase)
ssh-add ~/.ssh/id_ed25519

# List loaded keys
ssh-add -l

# Add a key with a lifetime (auto-removes after 8 hours)
ssh-add -t 8h ~/.ssh/id_ed25519

# Remove all keys from the agent
ssh-add -D
Enter fullscreen mode Exit fullscreen mode

The -t flag is particularly useful for security-conscious workflows. Setting a lifetime means your keys are automatically unloaded from memory after a set period, reducing the window of exposure if your workstation is compromised while unlocked.

FIDO2 Hardware Keys

FIDO2 security keys provide the strongest form of SSH key protection. The private key is generated on the hardware device and cannot be extracted -- not by malware, not by a compromised operating system, not even by the user. OpenSSH supports two FIDO2 key types:

# Generate an Ed25519 key backed by a FIDO2 device
# -O resident stores the key handle on the device itself (discoverable credential)
ssh-keygen -t ed25519-sk -O resident -C "yourname@example.com"

# Generate an ECDSA key backed by a FIDO2 device (wider hardware compatibility)
ssh-keygen -t ecdsa-sk -C "yourname@example.com"

# Require user verification (PIN + touch) for every authentication
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "yourname@example.com"
Enter fullscreen mode Exit fullscreen mode

With -O resident, the key handle is stored on the device, so you can move between workstations by plugging in your key and running ssh-add -K. The -O verify-required option adds a PIN check on top of the physical touch, giving you two-factor authentication for every SSH connection.

Post-Quantum Considerations

The default key exchange in OpenSSH 9.x already uses a hybrid post-quantum/classical construction, protecting session confidentiality against "harvest now, decrypt later" attacks. However, authentication keys (Ed25519, RSA) are not yet post-quantum resistant. The OpenSSH project is tracking NIST post-quantum signature standards (ML-DSA, formerly CRYSTALS-Dilithium). For most organizations, the default KEX configuration provides adequate forward secrecy, and switching authentication key types can wait until post-quantum signatures are production-ready.

Key Rotation

SSH keys should be rotated periodically, especially for service accounts and deploy keys. A practical rotation process looks like this:

  1. Generate a new key pair.
  2. Add the new public key to authorized_keys on all target hosts (or issue a new certificate if using certificate-based auth).
  3. Test the new key by connecting with ssh -i ~/.ssh/id_ed25519_new user@host.
  4. Update your SSH config to point to the new key.
  5. Remove the old public key from all target hosts.
  6. Delete or archive the old private key.

For infrastructure at scale, managing authorized_keys files manually is unsustainable. SSH certificates (covered in the CI/CD section) solve this by issuing short-lived credentials from a central authority, eliminating the need to touch individual servers during rotation.

SSH Config File Mastery

The client configuration file at ~/.ssh/config lets you define host-specific settings once instead of typing long commands with multiple flags.

Basic Structure

# ~/.ssh/config

# Global defaults (apply to all connections)
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes

# Production bastion host
Host bastion-prod
    HostName bastion.prod.example.com
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_prod

# Application server (accessed through bastion)
Host app-prod-*
    HostName %h.internal.example.com
    User deploy
    ProxyJump bastion-prod
    IdentityFile ~/.ssh/id_ed25519_prod

# Staging environment (different key, different user)
Host *.staging.example.com
    User staging-deploy
    IdentityFile ~/.ssh/id_ed25519_staging
    StrictHostKeyChecking accept-new

# Personal server
Host personal
    HostName 203.0.113.42
    User admin
    IdentityFile ~/.ssh/id_ed25519_personal
Enter fullscreen mode Exit fullscreen mode

With this configuration, connecting to a production app server behind the bastion is simply:

ssh app-prod-web01
Enter fullscreen mode Exit fullscreen mode

SSH resolves %h to app-prod-web01, sets the hostname to app-prod-web01.internal.example.com, and automatically tunnels through bastion-prod. No manual proxy commands, no remembering which key goes where.

ProxyJump: Modern Bastion Access

ProxyJump replaced the older ProxyCommand directive for connecting through bastion hosts. It is simpler to configure and supports chaining.

# Single jump
Host internal-server
    HostName 10.0.1.50
    ProxyJump bastion.example.com

# Multi-hop: client -> bastion1 -> bastion2 -> target
Host deep-internal
    HostName 10.0.2.100
    ProxyJump bastion1.example.com,bastion2.internal.example.com

# Command-line equivalent (no config needed)
ssh -J bastion.example.com user@10.0.1.50

# Multi-hop on the command line
ssh -J bastion1.example.com,bastion2.internal.example.com user@10.0.2.100
Enter fullscreen mode Exit fullscreen mode

An important security note: ProxyJump creates a direct encrypted channel from your client to the final destination. The bastion host forwards TCP traffic but cannot see the contents of your SSH session to the target server. This is a meaningful improvement over agent forwarding, which exposes your private key to the bastion host's memory.

ControlMaster: Connection Multiplexing

SSH multiplexing allows multiple sessions to share a single TCP connection. This speeds up repeated connections (no handshake, key exchange, or authentication for subsequent sessions) and is especially valuable with Ansible, rsync, or other tools that open many short-lived SSH sessions.

# ~/.ssh/config -- Enable multiplexing globally
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600
Enter fullscreen mode Exit fullscreen mode
# Create the sockets directory
mkdir -p ~/.ssh/sockets
Enter fullscreen mode Exit fullscreen mode

ControlMaster auto creates a master connection if none exists, or reuses one that does. ControlPath specifies the Unix socket location (%r = user, %h = host, %p = port). ControlPersist 600 keeps the master alive for 10 minutes after the last session disconnects.

# Check the status of a multiplexed connection
ssh -O check bastion-prod

# Manually close a multiplexed connection
ssh -O exit bastion-prod
Enter fullscreen mode Exit fullscreen mode

SCP Usage and Modern Alternatives

SCP copies files between hosts over SSH. Its syntax mirrors cp with the addition of user@host: prefixes for remote paths.

Basic SCP Commands

# Copy a local file to a remote server
scp ./deploy.tar.gz deploy@app-prod-web01:/opt/releases/

# Copy a remote file to local
scp deploy@app-prod-web01:/var/log/app.log ./

# Copy an entire directory recursively
scp -r ./config/ deploy@app-prod-web01:/opt/app/config/

# Copy between two remote hosts (traffic goes through your local machine)
scp deploy@host1:/data/backup.sql deploy@host2:/data/backup.sql

# Specify a custom SSH key and port
scp -i ~/.ssh/id_ed25519_prod -P 2222 ./file.txt deploy@bastion:/tmp/

# Use compression for large text files
scp -C ./large-logfile.txt deploy@host:/tmp/
Enter fullscreen mode Exit fullscreen mode

Why SCP Is Deprecated (and Why It Still Works)

OpenSSH 9.0 changed SCP's default behavior: it now uses the SFTP protocol internally instead of the legacy SCP/RCP protocol. The old SCP protocol had security issues -- it relied on the remote shell to interpret filenames, which could lead to unexpected behavior with specially crafted filenames. The command-line interface (scp) remains unchanged; only the underlying transfer protocol switched to SFTP. If you encounter a legacy server that does not support SFTP, you can force the old protocol with scp -O (capital O).

rsync Over SSH: The Better Alternative

For most file transfer tasks beyond simple one-off copies, rsync over SSH is superior to SCP:

# Sync a directory to a remote host (only transfers changed files)
rsync -avz --progress -e ssh ./project/ deploy@host:/opt/project/

# Use a specific SSH key and port
rsync -avz -e "ssh -i ~/.ssh/id_ed25519_prod -p 2222" ./project/ deploy@host:/opt/project/

# Dry run -- see what would be transferred without actually doing it
rsync -avzn ./project/ deploy@host:/opt/project/

# Exclude files from the transfer
rsync -avz --exclude='*.log' --exclude='.git/' ./project/ deploy@host:/opt/project/

# Delete files on the remote that no longer exist locally (mirror)
rsync -avz --delete ./project/ deploy@host:/opt/project/
Enter fullscreen mode Exit fullscreen mode

The key advantages of rsync are delta transfers (only changed portions of files are sent), the ability to resume interrupted transfers, and the --delete flag for true directory synchronization. SCP always copies entire files, making it wasteful for large directories that change incrementally.

Server Hardening: sshd_config Best Practices

A default OpenSSH server configuration is permissive by design. For any production or internet-facing server, you need to lock it down: disable everything you do not need, restrict what remains to minimum required access.

Recommended sshd_config Settings

# /etc/ssh/sshd_config -- Hardened configuration for production servers

# Protocol and port
Port 2222
AddressFamily inet
ListenAddress 0.0.0.0

# Authentication
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 20
PermitEmptyPasswords no

# User and group restrictions
AllowUsers deploy monitor
DenyUsers root admin

# Session security
ClientAliveInterval 300
ClientAliveCountMax 2
MaxSessions 5
MaxStartups 10:30:60

# Forwarding restrictions
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no

# Logging
LogLevel VERBOSE
SyslogFacility AUTH

# Misc hardening
Banner /etc/ssh/banner.txt
HostbasedAuthentication no
IgnoreRhosts yes
UseDNS no
Enter fullscreen mode Exit fullscreen mode

After editing sshd_config, always validate the configuration before restarting the service:

# Test configuration for syntax errors
sudo sshd -t

# Restart the SSH service (on systemd-based systems)
sudo systemctl restart sshd

# IMPORTANT: Keep your current session open and test with a new connection
# before closing it. If the config is broken, you could lock yourself out.
Enter fullscreen mode Exit fullscreen mode

fail2ban: Automated Brute-Force Protection

Even with password authentication disabled, SSH brute-force attempts generate log noise and consume resources. fail2ban monitors log files and temporarily bans IP addresses that exhibit suspicious behavior.

# Install fail2ban
sudo apt install fail2ban    # Debian/Ubuntu
sudo dnf install fail2ban    # RHEL/Fedora

# Create a local configuration override
sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
banaction = iptables-multiport
EOF

# Start and enable fail2ban
sudo systemctl enable --now fail2ban

# Check banned IPs
sudo fail2ban-client status sshd
Enter fullscreen mode Exit fullscreen mode

SSH Tunneling and Port Forwarding

SSH tunneling lets you securely access services not directly reachable from your network -- databases behind firewalls, internal web applications, admin interfaces -- by forwarding traffic through an SSH connection.

Local Port Forwarding

# Access a remote PostgreSQL database (port 5432) through an SSH tunnel
ssh -L 5432:db.internal:5432 deploy@bastion.example.com

# Forward multiple ports in a single command
ssh -L 5432:db.internal:5432 -L 6379:redis.internal:6379 deploy@bastion.example.com

# Run the tunnel in the background without opening a shell
ssh -fNL 5432:db.internal:5432 deploy@bastion.example.com
Enter fullscreen mode Exit fullscreen mode

Remote Port Forwarding

# Expose local port 3000 (your dev server) on the remote server's port 8080
ssh -R 8080:localhost:3000 deploy@staging.example.com
Enter fullscreen mode Exit fullscreen mode

Dynamic Port Forwarding (SOCKS Proxy)

# Create a SOCKS5 proxy on local port 1080
ssh -D 1080 deploy@bastion.example.com

# Use with curl
curl --proxy socks5h://localhost:1080 http://internal-dashboard.example.com:8080

# Background version
ssh -fND 1080 deploy@bastion.example.com
Enter fullscreen mode Exit fullscreen mode

SSH in CI/CD and Automation

Automated systems -- CI/CD pipelines, configuration management tools, deployment scripts -- need to authenticate over SSH without human interaction. This requires careful key management to avoid creating persistent, overprivileged credentials that become security liabilities.

Deploy Keys

A deploy key is an SSH key pair where the public key is added to a specific repository or server, and the private key is stored as a CI/CD secret. Best practices for deploy keys:

  • Generate one key per repository per environment (do not share keys across repos or environments).
  • Use read-only deploy keys for pull operations; only grant write access when the pipeline needs to push.
  • Store the private key in your CI/CD platform's secret management, never in the repository itself.
  • Set the command= option in authorized_keys to restrict what a deploy key can execute on the server.
# authorized_keys entry that restricts a deploy key to a specific command
command="/opt/deploy/run.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3Nz... deploy-pipeline@ci
Enter fullscreen mode Exit fullscreen mode

SSH Certificates: Scalable Authentication

SSH certificates solve the scalability problem of authorized_keys: instead of adding each user's public key to every server, you set up a CA that signs user keys. Servers trust the CA, and any signed key is accepted.

# Step 1: Generate a CA key pair (do this once, guard the private key carefully)
ssh-keygen -t ed25519 -f /etc/ssh/ca_user_key -C "SSH User CA"

# Step 2: Sign a user's public key to create a certificate
ssh-keygen -s /etc/ssh/ca_user_key \
  -I "jane.doe@example.com" \
  -n deploy,monitor \
  -V +12h \
  -z 1001 \
  ~/.ssh/id_ed25519.pub

# Step 3: Configure the server to trust the CA
# Add to /etc/ssh/sshd_config:
# TrustedUserCAKeys /etc/ssh/ca_user_key.pub

# Step 4: Inspect a certificate
ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
Enter fullscreen mode Exit fullscreen mode

The -V +12h flag is the critical piece: it issues a certificate that expires in 12 hours. This means that even if the certificate is stolen, the window of exposure is limited.

SSH in a Kubernetes World

In a well-architected Kubernetes environment, you rarely SSH into individual nodes. You interact with workloads through kubectl exec, kubectl logs, and kubectl port-forward. But SSH has not disappeared -- it has moved to different layers of the stack.

kubectl exec vs SSH

kubectl exec provides shell access to containers through the Kubernetes API server, but it is not a full replacement for SSH:

  • kubectl exec works at the container level. You are inside the container's filesystem and namespace, not on the underlying node.
  • SSH to nodes is still needed for node-level troubleshooting: kernel issues, kubelet debugging, container runtime problems, network stack inspection, and hardware diagnostics.
  • Security model differences: kubectl exec is authorized by Kubernetes RBAC. SSH is authorized by the node's local auth configuration. These are independent systems with different audit trails.

Bastion Patterns for Kubernetes Clusters

# SSH config for a Kubernetes cluster with a bastion
Host k8s-bastion
    HostName bastion.k8s.example.com
    User ops
    IdentityFile ~/.ssh/id_ed25519_k8s
    LocalForward 6443 k8s-api.internal:6443

Host k8s-node-*
    HostName %h.internal.k8s.example.com
    User ops
    ProxyJump k8s-bastion
    IdentityFile ~/.ssh/id_ed25519_k8s
Enter fullscreen mode Exit fullscreen mode

Putting It All Together: A Practical Checklist

  1. Use Ed25519 keys for all new key generation.
  2. Protect private keys with passphrases and use ssh-agent with time-limited key loading.
  3. Consider FIDO2 hardware keys for high-privilege access.
  4. Write an SSH config file. Define hosts, keys, ProxyJump chains, and multiplexing settings.
  5. Disable password authentication on all servers.
  6. Restrict SSH access with AllowUsers or AllowGroups. Disable root login.
  7. Use ProxyJump instead of agent forwarding.
  8. Deploy fail2ban on internet-facing servers.
  9. Use SSH certificates for automation. Short-lived certificates are more secure than long-lived deploy keys.
  10. Prefer rsync over SCP for file transfers.
  11. Enable connection multiplexing (ControlMaster) for hosts you connect to frequently.
  12. Keep OpenSSH updated. The post-quantum key exchange in 9.x is on by default.

SSH has been the backbone of remote system administration for nearly three decades. The ecosystem continues to evolve -- post-quantum cryptography, hardware-backed keys, certificate-based authentication, zero-trust access -- but the fundamentals remain the same. Master your config, harden your servers, manage your keys, and SSH will remain the most reliable tool in your infrastructure toolkit.

Top comments (0)