Day 10 - SSH, Keys, and Secure File Transfer

2025-09-307 min read

linuxsshopensshscprsyncsftpsecurityssh-agent

Day10 SSH server connection

Secure Shell provides encrypted remote login and file transfer. This lesson covers key generation, agent setup, per host configuration, safe permissions, copying public keys to servers, verifying host keys, and moving files with scp and rsync. It also shows useful port forwarding patterns and reliability settings.

What you learn today

Create modern SSH keys, log in without passwords, store per host options in ~/.ssh/config, transfer files, and keep sessions stable on flaky networks.

Prerequisites

  • Day 1 through Day 9 completed
  • A test VM that can accept SSH or a second machine on the network

Generate a key pair

Use an Ed25519 key for most cases. Add a comment to identify the key.

bash
ssh-keygen -t ed25519 -C "majd@laptop" -f ~/.ssh/id_ed25519

Prompts:

  • passphrase: protects the private key at rest
  • files created: ~/.ssh/id_ed25519 and ~/.ssh/id_ed25519.pub

Set correct permissions.

bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
Never share the private key

The file ~/.ssh/id_ed25519 is private. Share only the .pub file.

Load the key into ssh-agent

The agent holds decrypted keys in memory so repeated logins do not ask for the passphrase.

bash
# start an agent in the current shell if not already running
eval "$(ssh-agent -s)"

# add the key
ssh-add ~/.ssh/id_ed25519

# list keys currently loaded
ssh-add -l

Desktop environments often start an agent automatically. Keys can be loaded on first use by entering the passphrase.

Store passphrases securely

Use the desktop keyring on Linux or the built in keychain on macOS to remember the passphrase. On servers, consider entering the passphrase per login instead of storing it.

Copy the public key to a server

Use ssh-copy-id when available. The target must allow password login once to install the public key.

bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

Manual method if ssh-copy-id is not present:

bash
cat ~/.ssh/id_ed25519.pub | ssh user@server 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

Test login.

bash
ssh user@server
Harden the server after keys work

Edit /etc/ssh/sshd_config to limit authentication methods. Example lines:

text
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
MaxAuthTries 3
LoginGraceTime 30

Restart the service after a syntax check:

bash
sudo sshd -t && sudo systemctl restart sshd 2>/dev/null || sudo systemctl restart ssh

Verify host keys and known hosts

On first connect, SSH records the server fingerprint in ~/.ssh/known_hosts. Verify the fingerprint out of band when possible.

bash
ssh -o StrictHostKeyChecking=ask user@server
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub 2>/dev/null | awk '{print $2, $3}'

If a host key changes unexpectedly, SSH warns about a potential attack. Investigate before accepting the new key.

Do not disable host key checking

Avoid StrictHostKeyChecking no except in throwaway test environments. Host key checks prevent man in the middle attacks.

Per host settings in ~/.ssh/config

Create a configuration file to avoid long command lines and to set safe defaults.

bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh

cat > ~/.ssh/config <<'EOF'
Host server
  HostName 203.0.113.10
  User user
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 30
  ServerAliveCountMax 4
  AddKeysToAgent yes
  IdentitiesOnly yes

# jump host example
Host bastion
  HostName 198.51.100.5
  User admin

Host app-*
  User deploy
  ProxyJump bastion
  IdentityFile ~/.ssh/id_ed25519
  Compression yes
  ForwardAgent no
EOF
chmod 600 ~/.ssh/config

Now connect with a short name.

bash
ssh server
ssh app-01
Multiplexing for speed

Connection sharing reduces repeated handshakes. Add these lines:

text
Host *
  ControlMaster auto
  ControlPath ~/.ssh/ctl-%C
  ControlPersist 10m

Copy files with scp

scp copies files over SSH. Basic patterns:

bash
# upload
scp local.txt user@server:/home/user/

# download
scp user@server:/var/log/syslog ./

# copy a directory recursively
scp -r site/ user@server:/var/www/site/

Flags:

  • -P port number when the server does not use 22
  • -i identity file if not in config
  • -C compression for text heavy transfers
Be mindful of permissions

When copying to system locations such as /var/www, prefer uploading to a home directory and then using sudo on the server to move files into place with correct ownership.

Sync directories with rsync over SSH

rsync is efficient for repeated transfers. It sends only changed blocks.

bash
# archive mode, verbose, compress, show progress
rsync -avzP -e ssh ./site/ user@server:/srv/site/

# delete files on the destination that no longer exist locally (use with care)
rsync -avzP --delete -e ssh ./site/ user@server:/srv/site/

# use a non default port
rsync -avzP -e 'ssh -p 2222' ./data/ user@server:/srv/data/

Trailing slashes matter:

  • ./site/ copies the contents into /srv/site/
  • ./site creates a subdirectory /srv/site/site
Dry run before destructive syncs

Add --dry-run to preview changes before using --delete.

Port forwarding and jump hosts

Local forwarding exposes a remote service on a local port.

bash
# open localhost:8080 to reach remote 127.0.0.1:80
ssh -L 8080:127.0.0.1:80 user@server

Remote forwarding exposes a local service to the remote host.

bash
# on the client, expose local 3000 as remote 9000
ssh -R 9000:127.0.0.1:3000 user@server

Dynamic forwarding creates a SOCKS proxy on localhost.

bash
ssh -D 1080 user@server

Jump through a bastion host with -J or ProxyJump.

bash
ssh -J bastion user@app-01
Agent forwarding

ForwardAgent yes exposes keys to the remote host while the session is active. Enable only on trusted hosts, or prefer signed SSH certificates when available.

Keep sessions healthy

Add keepalives to reduce disconnects on idle links.

bash
# client side in ~/.ssh/config
ServerAliveInterval 30
ServerAliveCountMax 4

# server side in /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3
IPv6 and multiple addresses

Servers may have both IPv4 and IPv6. Use ssh -4 or ssh -6 to force a family if connectivity issues appear.

Practical lab

  1. Generate a key and install it on the target server.
bash
ssh-keygen -t ed25519 -C "lab@client"
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
ssh user@server 'echo ok'
  1. Create a per host entry and test a short ssh command.
bash
cat >> ~/.ssh/config <<'EOF'
Host lab
  HostName server
  User user
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 30
  IdentitiesOnly yes
EOF
ssh lab 'uname -a'
  1. Transfer files with scp and rsync.
bash
scp /etc/hosts lab:/tmp/hosts.copy
rsync -avzP -e ssh /etc/ lab:/tmp/etc-copy/ --dry-run
  1. Try a local port forward to view a remote web app.
bash
ssh -L 8080:127.0.0.1:80 lab
# then open http://127.0.0.1:8080 in a browser from the client

Troubleshooting

  • Permission denied (publickey) The public key is missing from authorized_keys, permissions are too open, or the wrong identity is used. Check -i, IdentitiesOnly yes, and directory modes: ~/.ssh 700, authorized_keys 600.
  • Host key mismatch after a server rebuild. Remove the old line for that host from ~/.ssh/known_hosts only after confirming the new fingerprint out of band.
  • Slow logins. Check DNS reverse lookups on the server. Disable with UseDNS no in sshd_config if appropriate, or fix name resolution.
  • scp permission errors to system paths. Upload to a home directory, then use sudo on the server to move files with the correct ownership.
  • rsync transfers no files. Confirm the trailing slash usage and that the remote path exists and is writable.

Next steps

Day 11 introduces disks and filesystems. It covers lsblk, partitions versus filesystems, mounting with mount and /etc/fstab, checking space with df and du, and a safe workflow to add a second disk in a VM.