32blogby Studio Mitsu

ssh & rsync Practical Guide: Remote Server Essentials

Set up SSH key auth, configure ssh_config, master port forwarding, and use rsync for efficient file sync and backups.

by omitsu14 min read
On this page

Tired of typing passwords every time you SSH into a server? Need to push files to production but scp feels painfully slow? Want to access a remote dev server from your local machine?

The answer is ssh and rsync. ssh provides encrypted remote connections, and rsync handles efficient file synchronization by transferring only what's changed. Together, they form the foundation of server management and deployment workflows.

This guide walks you through key authentication setup, config file tuning, port forwarding, and rsync backup patterns — all with ready-to-run commands.

What is SSH?

ssh (Secure Shell) is a protocol and command for securely operating remote machines over a network. All communication is encrypted, so passwords and commands never travel in plaintext.

The predecessor, telnet, sent everything unencrypted. ssh replaced it in 1995 and has become the de facto standard for server administration.

ssh does far more than just remote login:

  • Remote command execution: run one-liners like ssh user@server "ls -la /var/log"
  • File transfer: encrypted transfers via scp and sftp
  • Port forwarding: create tunnels between local and remote ports
  • Tunneling: use it as a SOCKS proxy for VPN-like functionality

Basic connection is straightforward:

bash
ssh user@192.168.1.100

To specify a port, use -p:

bash
ssh -p 2222 user@192.168.1.100

Setting up key authentication

ssh supports two main authentication methods: password-based and key-based.

Password authentication requires entering a password for every connection. It's simple but weak — vulnerable to brute-force attacks, and a leaked password means game over.

Key authentication uses a public-private key pair. Without the private key, login is impossible, making brute-force attacks ineffective. For production servers, key auth is essentially mandatory.

Generating keys

The 2026 standard is Ed25519. It provides equal or better security than RSA with shorter key lengths, and faster signing and verification.

bash
ssh-keygen -t ed25519 -C "your_email@example.com"

You'll be prompted for a save location and passphrase. Setting a passphrase is strongly recommended — it's the last line of defense if your private key is ever compromised.

If you need RSA for compatibility with older servers, use at least 4096 bits:

bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Copying your public key to the server

The easiest method is ssh-copy-id:

bash
ssh-copy-id user@server

This appends your local public key (~/.ssh/id_ed25519.pub) to the server's ~/.ssh/authorized_keys.

If ssh-copy-id isn't available, do it manually:

bash
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Setting permissions

ssh is strict about permissions. Key auth may be rejected if these aren't set correctly (see our chmod & chown guide for a deeper dive into Linux permissions):

bash
# Server side
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# Client side
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

Managing keys with ssh-agent

If you set a passphrase on your key, entering it for every connection gets tedious. Register your key with ssh-agent to skip passphrase prompts for the duration of your session:

bash
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

The ssh config file

~/.ssh/config stores connection parameters so you don't have to type them every time.

Basic syntax

text
Host myserver
    HostName 192.168.1.100
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Now ssh myserver is equivalent to ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@192.168.1.100.

Wildcards

Apply common settings to multiple servers:

text
Host *.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519_work

Keep-alive

Prevent idle SSH connections from being dropped:

text
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

This sends a keep-alive signal every 60 seconds. If the server fails to respond 3 times, the connection is terminated.

Multi-hop SSH (ProxyJump)

Connecting through a bastion host to reach internal servers is a common pattern. ProxyJump handles it in a single command:

text
Host bastion
    HostName bastion.example.com
    User admin

Host internal
    HostName 10.0.0.5
    User deploy
    ProxyJump bastion

ssh internal automatically routes through the bastion to reach the internal server.

Connection multiplexing (ControlMaster)

Speed up subsequent connections to the same server. The first connection creates a socket, and later connections reuse it:

text
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

Create the socket directory first:

bash
mkdir -p ~/.ssh/sockets

ControlPersist 600 keeps the socket alive for 600 seconds after the last connection closes. This makes a noticeable difference in deployment scripts that connect to the same server repeatedly.

Port forwarding

SSH port forwarding creates encrypted tunnels to relay traffic between ports. It's invaluable for accessing services behind firewalls or exposing local services externally.

Local forward (-L)

Forward a remote port to your local machine. This is the most common pattern:

bash
ssh -L 8080:localhost:3000 user@server

Access localhost:8080 on your machine, and traffic is forwarded to localhost:3000 on the remote server. Use this when you need to reach a remote dev server or admin dashboard from your local browser.

Practical example: connecting to a remote PostgreSQL instance

bash
ssh -L 5433:localhost:5432 user@db-server

In another terminal:

bash
psql -h localhost -p 5433 -U myuser mydb

You can connect to the remote PostgreSQL as if it were running locally. Combine with tmux for managing multiple terminals.

Remote forward (-R)

Expose a local port through the remote server:

bash
ssh -R 8080:localhost:3000 user@server

Access localhost:8080 on the remote server, and traffic is forwarded to localhost:3000 on your machine. Useful for showing a local development app to your team.

Dynamic forward (-D)

Create a SOCKS proxy:

bash
ssh -D 1080 user@server

Set localhost:1080 as your browser's SOCKS proxy, and all traffic routes through the SSH tunnel. Handy for encrypting traffic on public Wi-Fi or accessing region-restricted services.

Running tunnels in the background

If you only need the forwarding and don't need a shell, use -fN:

bash
ssh -fNL 8080:localhost:3000 user@server

-f runs ssh in the background, -N tells it not to execute any remote commands.

What is rsync?

rsync is a file synchronization tool specialized in delta transfers. It detects which parts of files have changed and transfers only those parts, making subsequent syncs dramatically faster.

Here's how it compares to scp:

Featurescprsync
Transfer modeFull copy every timeDelta only
Resume after interruptionNoYes, with --partial
CompressionNoYes, with -z
Exclusion patternsNoFlexible --exclude
MirroringNoYes, with --delete

For any non-trivial file transfer, rsync is the clear winner.

Basic syntax:

bash
rsync [options] source destination

Commonly used options:

  • -a (archive): recursive copy preserving permissions, timestamps, and ownership
  • -v (verbose): show file names during transfer
  • -z (compress): compress transfer data
  • -n (dry-run): show what would happen without actually transferring
  • --progress: display transfer progress

rsync in practice

Basic file sync

Local to remote:

bash
rsync -avz ./dist/ user@server:/var/www/

Remote to local:

bash
rsync -avz user@server:/var/log/ ./logs/

Always do a dry run first. Just add -n:

bash
rsync -avzn ./dist/ user@server:/var/www/

Exclusion patterns

Exclude specific files or directories from transfer:

bash
rsync -avz --exclude='node_modules' --exclude='.git' --exclude='.env' ./project/ user@server:/app/

When you have many exclusions, put them in a file. Create .rsyncignore:

text
node_modules
.git
.env
*.log
.DS_Store

Then reference it with --exclude-from:

bash
rsync -avz --exclude-from='.rsyncignore' ./project/ user@server:/app/

Backups

Mirror backup keeps the destination an exact copy of the source. --delete removes files from the backup that no longer exist in the source:

bash
rsync -avz --delete /data/ /backup/data/

Generational backup saves deleted or modified files to a dated directory:

bash
rsync -avz --backup --backup-dir="/backup/$(date +%Y%m%d)" --delete /data/ /backup/current/

/backup/current/ always contains the latest state, while deleted and updated files are preserved in directories like /backup/20260308/.

Deployment script

A practical example of automating web app deployment with rsync:

bash
#!/bin/bash
set -euo pipefail

REMOTE_USER="deploy"
REMOTE_HOST="server.example.com"
REMOTE_PATH="/var/www/myapp"
LOCAL_PATH="./dist/"

echo "Starting deployment to ${REMOTE_HOST}..."

rsync -avz --delete \
  --exclude='.git' \
  --exclude='node_modules' \
  --exclude='.env' \
  --exclude='*.log' \
  "${LOCAL_PATH}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/"

echo "Deployment complete."

set -euo pipefail is essential for script error handling — it stops execution as soon as any command fails. For more on writing robust shell scripts, check out our shell scripting guide.

Advanced techniques

SSH agent forwarding (ForwardAgent)

When you need to SSH from a bastion host to an internal server, you shouldn't store private keys on the bastion. Agent forwarding lets internal servers use your local key through the tunnel.

text
# ~/.ssh/config
Host bastion
    HostName bastion.example.com
    User admin
    ForwardAgent yes

Host internal-*
    User deploy
    ProxyJump bastion
    ForwardAgent yes
bash
# Connect through bastion to internal, then git pull works
ssh internal-web
git pull  # your local key is forwarded, so authentication succeeds

Security note: Only set ForwardAgent yes for servers you trust. Anyone with root access on the bastion can access your agent socket and use your keys. Never set it globally with Host * — specify it explicitly per host.

rsync resume and progress (-P)

When transferring large files over unreliable connections, -P lets you resume from where you left off.

bash
# -P is shorthand for --partial --progress
rsync -avzP user@server:/data/large-dump.sql.gz ./backups/

--partial keeps partially transferred files instead of deleting them (the default). The next rsync run transfers only the remaining portion. --progress shows real-time transfer progress.

bash
# Sync large files with partial files stored in a hidden directory
rsync -avzP --partial-dir=.rsync-partial user@server:/data/ ./local-data/

--partial-dir stores incomplete files in a separate directory, automatically cleaned up after transfer completes. Keeps your working directory clean.

rsync bandwidth limiting (--bwlimit)

Prevent large transfers from saturating the network on production servers.

bash
# Limit bandwidth to 5MB/s
rsync -avz --bwlimit=5000 ./release/ user@production:/app/

# Go easy during business hours
rsync -avz --bwlimit=1000 ./data/ user@server:/backup/

--bwlimit is in KBytes/s. 5000 = roughly 5MB/s. Adding this to deploy scripts prevents the "the site slowed down after deploy" problem.

Mount remote filesystems with sshfs

Mount a remote server's directory locally and work with files as if they were on your machine. sshfs uses SFTP under the hood.

bash
# Mount remote directory
mkdir -p ~/mnt/server
sshfs user@server:/var/www ~/mnt/server

# Edit files with your local editor
code ~/mnt/server/index.html

# Unmount
fusermount -u ~/mnt/server

Your ~/.ssh/config settings are used automatically, so sshfs myserver:/var/www ~/mnt/server works with shorthand hostnames.

Note that sshfs is directly affected by network latency, so it's not ideal for bulk file operations. Best suited for checking config files or small edits.

Security considerations

SSH security and server security go hand in hand. Here's the minimum you should be doing.

Key changes in OpenSSH 10.0

OpenSSH 10.0 (April 2025) introduced significant security changes:

  • DSA completely removed: DSA keys can no longer be generated or used. Migrate to Ed25519
  • ML-KEM support: post-quantum key exchange algorithm (mlkem768x25519-sha256) enabled by default, providing preemptive protection against quantum computing threats

CVE-2025-26465 (MITM attack)

A vulnerability discovered by Qualys TRU in OpenSSH 6.8p1 through 9.9p1 allowed man-in-the-middle attacks when VerifyHostKeyDNS was enabled. Fixed in 9.9p2 (February 2025). Note that VerifyHostKeyDNS is off by default, so the impact is limited to users who explicitly enabled it.

Configure /etc/ssh/sshd_config on the server side:

Only set PasswordAuthentication no after key auth is fully working. Doing it before locks you out of the server.

rsync security

rsync 3.4.0 (January 2025) fixed six critical CVEs discovered by Google Cloud researchers, including CVE-2024-12084 (heap buffer overflow enabling remote code execution). If you're running 3.3.x or earlier, update immediately. Version 3.4.1 is a regression fix release, so 3.4.1 is the recommended target.

Check your version:

bash
rsync --version

Firewall and fail2ban

Restrict SSH port access (default: 22) by IP:

bash
# Using ufw
sudo ufw allow from 203.0.113.0/24 to any port 22
sudo ufw deny 22

fail2ban detects failed login attempts and automatically blocks offending IPs. It's the standard defense against SSH brute-force attacks:

bash
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

FAQ

Is SSH key authentication more secure than password authentication?

Yes, significantly. Password authentication is vulnerable to brute-force attacks and phishing. Key authentication uses cryptographic key pairs — without the private key file, login is mathematically impossible. Production servers should always disable password authentication once key auth is configured.

Can I use the same SSH key for multiple servers?

Yes. You copy the same public key (~/.ssh/id_ed25519.pub) to each server's ~/.ssh/authorized_keys. However, some security-conscious teams prefer separate keys per environment (e.g., one for production, another for development) to limit blast radius if a key is compromised.

How do I fix "Permission denied (publickey)" errors?

Check these in order: (1) Ensure ~/.ssh is 700 and ~/.ssh/authorized_keys is 600 on the server. (2) Verify the correct public key is in authorized_keys. (3) Confirm PubkeyAuthentication yes is set in sshd_config. (4) Check ssh -vvv user@server output for the specific rejection reason.

What's the difference between scp and rsync?

scp copies entire files every time, while rsync transfers only the changed portions (delta transfer). rsync also supports resume after interruption (--partial), compression (-z), exclusion patterns (--exclude), and mirroring (--delete). For anything beyond a quick one-off copy, rsync is the better choice.

Does the trailing slash in rsync actually matter?

Yes, and getting it wrong is one of the most common rsync mistakes. rsync -avz src/ dest/ copies the contents of src into dest. But rsync -avz src dest/ copies src as a directory into dest/src/. Always dry-run with -n if you're not sure.

Can rsync work over a non-standard SSH port?

Yes. Use the -e flag: rsync -avz -e 'ssh -p 2222' ./files/ user@server:/data/. If you've already configured the port in ~/.ssh/config, rsync picks it up automatically when you use the hostname alias.

Is it safe to use ForwardAgent?

Only for servers you fully trust. When ForwardAgent yes is set, anyone with root access on that server can use your forwarded SSH key to authenticate as you on other servers. Never set it globally with Host * — specify it per host, and only for bastion hosts or CI servers under your control.

Wrapping Up

ssh and rsync are the foundation of remote server work. Authenticate securely with keys, streamline connections with config files, and sync files efficiently with rsync.

Key takeaways:

  • Use Ed25519 keys. DSA was removed in OpenSSH 10.0
  • Centralize connection parameters in ~/.ssh/config. Use ProxyJump for multi-hop connections
  • Port forwarding lets you access remote services from localhost
  • rsync basics: -avz for most operations. Always dry-run with -n first
  • The trailing slash changes rsync behavior entirely — verify it until it's muscle memory
  • On the server side, set PasswordAuthentication no and PermitRootLogin no

Pair ssh with tmux to make remote terminal work significantly more comfortable.