SSH for developers

By

SSH for developers: connect to VPS servers, create keys with ssh-keygen, use ~/.ssh/config aliases, and copy files with scp or rsync.

~~~

SSH lets you open an encrypted connection to another machine. We use it to manage servers, run remote commands, copy files, create tunnels, and authenticate Git operations.

The command looks simple:

ssh [email protected]

There are two separate trust questions behind it:

  1. Is this the server I intended to reach?
  2. Can I prove to that server who I am?

Host keys answer the first question. User keys or another authentication method answer the second.

What happens during an SSH connection

SSH does more than encrypt a terminal session.

First, your client opens a TCP connection to the server. Both sides exchange protocol versions and choose the encryption, key exchange, host key, and integrity algorithms they support.

The key exchange creates temporary session keys. The server also signs part of that exchange with its host private key. Your client checks that signature against the host key stored in known_hosts.

Only then does user authentication begin.

With public key authentication, the server checks whether your public key is authorized. Your client signs data tied to the current connection with the private key. The server verifies the signature with the public key.

The private key never travels across the network.

After authentication, SSH opens one or more channels inside the encrypted connection. An interactive shell is one channel. A remote command, file transfer, or forwarded port can use another.

Here is the complete flow:

sequenceDiagram
  participant C as Your computer
  participant S as SSH server
  C->>S: Open a TCP connection
  C->>S: Agree on algorithms and session keys
  S-->>C: Prove the server identity
  C->>C: Check known_hosts
  C->>S: Prove the user identity
  S-->>C: Open encrypted channels

This model helps when debugging. A connection can reach the server but fail host verification. It can pass host verification but fail user authentication. Or it can authenticate correctly while a forwarded service remains unreachable.

Connect to a server

The basic syntax is:

ssh user@host

Use -p when the server listens on another port:

ssh -p 2222 [email protected]

The IP address above belongs to the range reserved for documentation. Replace it with your server address.

If you are creating your first server, the free Ubuntu VPS course walks through provisioning, secure access, and deployment. The free Networking Foundations course explains addresses, ports, and routes.

Verify the host key

The first connection shows a fingerprint similar to this:

The authenticity of host '203.0.113.10' can't be established.
ED25519 key fingerprint is SHA256:...
Are you sure you want to continue connecting?

Do not type yes by reflex.

Compare the fingerprint with a value obtained through another trusted channel. A hosting provider may show it in the dashboard or console. A system administrator may give it to you directly.

After approval, SSH stores the host key in:

~/.ssh/known_hosts

Future connections compare the server with that saved key.

If the key changes, SSH warns us. The server may have been rebuilt, but a changed key can also mean the address now reaches another machine or someone is intercepting the connection.

Investigate before removing the old entry. Once the change is confirmed, remove that host entry with:

ssh-keygen -R 203.0.113.10

Then connect again and verify the new fingerprint.

Never disable strict host checking to make the warning disappear. Encryption without server identity can protect a connection to the wrong server.

Create an SSH key pair

Key-based authentication uses two related files:

  • the private key stays on our machine
  • the public key goes to the server

Create an Ed25519 key:

ssh-keygen -t ed25519 -C 'flavio@macbook'

SSH asks where to save it. The default files are:

~/.ssh/id_ed25519
~/.ssh/id_ed25519.pub

The .pub file is public. We can place it on servers and Git hosting services.

The file without .pub is private. Never email it, paste it into a dashboard, or commit it.

Add a passphrase. It encrypts the private key on disk, so a stolen file is not immediately usable.

The passphrase does not get sent to the server. Your computer uses it locally to unlock the private key.

You can inspect the fingerprint of a public key:

ssh-keygen -lf ~/.ssh/id_ed25519.pub

This is useful when a server, Git provider, or teammate shows several keys. Compare fingerprints instead of guessing from filenames.

Install the public key on the server

If the server accepts a password for initial setup, use:

ssh-copy-id [email protected]

This appends the public key to the remote user’s ~/.ssh/authorized_keys file and applies suitable permissions.

Some hosting providers install a public key when the server is created. In that case, no password login or ssh-copy-id step is needed.

If you install a key manually, copy the complete .pub line. Then run these commands on the server:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

The account must own both the directory and the file. OpenSSH commonly rejects authorized_keys when another user can modify it or its parent directory.

Each line can also restrict what a key may do. For example, an automated deployment key can run one command and nothing else:

command="/usr/local/bin/deploy-notes",restrict ssh-ed25519 AAAAC3...

restrict disables features such as forwarding, agent forwarding, and pseudo-terminal allocation. The forced command ignores the command requested by the client.

Use restrictions for automation keys. Your normal interactive key needs a shell, so this exact entry would be too narrow for it.

Test the key in a new terminal:

ssh [email protected]

Keep the original session open while testing. Before disabling password login, prove that a second connection works. This simple habit prevents lockouts.

The server authorizes the public key for one user. A key installed for deploy does not automatically grant access as root.

Protect local SSH files

SSH expects private configuration to have restrictive permissions:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 600 ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519.pub

If a private key is readable by other users, SSH may refuse to use it.

Back up private keys carefully. A backup containing an unencrypted private key is another copy an attacker can steal.

Use ~/.ssh/config

Typing a user, address, port, and key every time gets old. Put host-specific settings in ~/.ssh/config:

Host notes-server
  HostName 203.0.113.10
  User deploy
  Port 2222
  IdentityFile ~/.ssh/notes_server_ed25519
  IdentitiesOnly yes

Now connect with:

ssh notes-server

Host is the local alias. HostName is the real DNS name or IP address.

IdentitiesOnly yes tells SSH to use the configured identity instead of offering every key loaded in the agent. This avoids authentication failures when an agent contains many keys.

SSH reads command-line options first, then user configuration, then system configuration. For most settings, the first value found wins.

This makes file order matter. Put specific hosts before broad wildcard rules:

Host production
  HostName 203.0.113.10
  User deploy

Host *.internal.example
  User flavio

Inspect the final configuration SSH will use:

ssh -G notes-server

This prints the resolved client configuration without connecting. It is very useful when aliases, wildcard blocks, or included files interact.

You can split a large configuration with Include:

Include ~/.ssh/config.d/*

SSH processes included files at that position. Keep their permissions as restrictive as the main config.

Manage several keys

I prefer a separate key when identities have different security boundaries, such as personal Git hosting and a production server.

Generate a named key:

ssh-keygen -t ed25519 -f ~/.ssh/notes_server_ed25519

Connect with it once from the command line:

ssh -i ~/.ssh/notes_server_ed25519 [email protected]

Then move that selection into ~/.ssh/config.

Reusing one key across every server is easy, but revoking it affects everything. One key per server can become hard to manage. Group keys around real identities and risk boundaries instead of following an absolute rule.

This is the same key mechanism used for Git over SSH.

Use ssh-agent

A passphrase protects the key, but typing it for every connection is annoying. ssh-agent keeps unlocked keys in memory.

Start an agent in a shell and add the key:

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

List loaded keys:

ssh-add -l

Remove one key:

ssh-add -d ~/.ssh/notes_server_ed25519

Desktop operating systems often start an agent and integrate it with their credential store. Check the platform’s OpenSSH instructions before adding shell startup commands that create a new agent on every terminal.

Avoid agent forwarding by default

Agent forwarding makes the local agent available through the remote host:

ssh -A notes-server

This does not copy the private key. However, a privileged attacker on the remote host may use the forwarded agent to authenticate elsewhere while the connection remains available.

Do not enable ForwardAgent yes globally. Use a jump host or a narrowly scoped deploy key instead when possible.

Run one remote command

SSH does not require an interactive shell:

ssh notes-server 'systemctl --user status notes-api'

The remote command’s output appears locally. Its exit status becomes the ssh command’s exit status.

This makes SSH useful in scripts:

if ssh notes-server 'test -f /var/www/notes/current.txt'; then
  echo 'Release marker exists'
else
  echo 'Release marker is missing'
fi

Be careful with quoting. The local shell parses the command line first, then the remote shell parses the command string. For complicated logic, copy a tested script to the server and run it there.

Do not place secrets directly in the command. They may appear in shell history, process listings, or logs.

Copy files with scp

Copy a local file to the server:

scp deploy.zip notes-server:/var/www/notes/

Copy a remote log to the current folder:

scp notes-server:/var/log/nginx/error.log ./error.log

Copy a directory recursively:

scp -r ./assets notes-server:/var/www/notes/

scp is good for a few files. See download a file with scp for more examples.

Synchronize folders with rsync

rsync compares source and destination, then sends the differences over SSH:

rsync -av ./dist/ notes-server:/var/www/notes/

The trailing slash on ./dist/ means “copy the contents of this folder.” Without it, rsync creates a dist folder at the destination.

Preview a destructive synchronization with --dry-run:

rsync -av --delete --dry-run \
  ./dist/ notes-server:/var/www/notes/

--delete removes destination files absent from the source. Review the dry run, confirm both paths, then run the real command only when deletion is intended.

rsync is synchronization, not a backup. Mirroring a mistaken deletion can remove the other copy too.

Forward a port to your computer

Local port forwarding exposes a remote-side service on our laptop.

Suppose Postgres listens only on the server’s loopback interface at port 5432. Create a tunnel:

ssh -N -L 5433:127.0.0.1:5432 notes-server

Now a local client can connect to 127.0.0.1:5433. SSH carries the traffic to 127.0.0.1:5432 as seen from notes-server.

-N means no remote command. The connection exists only for forwarding.

Add an option so SSH fails when it cannot create the tunnel:

ssh -N \
  -o ExitOnForwardFailure=yes \
  -L 5433:127.0.0.1:5432 \
  notes-server

By default, the local listener is intended for the local machine. Do not bind a database tunnel to every network interface unless other machines genuinely need access and the exposure is understood.

The easiest way to read -L is:

-L local_port:destination_seen_from_server:destination_port

The destination is resolved from the SSH server, not from your laptop. That difference matters when the service uses a private hostname.

Forward a local service to the server

Remote forwarding goes in the other direction.

Suppose an application runs on your laptop at port 3000. Make it available from the server at port 8080:

ssh -N -R 8080:127.0.0.1:3000 notes-server

Now a process on notes-server can connect to 127.0.0.1:8080. SSH carries that traffic back to port 3000 on your laptop.

Remote forwarding is useful for short-lived previews and callback testing. Do not treat it as a permanent deployment system.

The SSH server controls whether remote forwarding is allowed. It also controls whether the listening port stays on loopback or can bind to other interfaces.

Create a SOCKS proxy

Dynamic forwarding lets applications choose the destination:

ssh -N -D 1080 notes-server

This creates a SOCKS proxy at 127.0.0.1:1080. Configure a browser or another SOCKS-aware client to use it.

Traffic from that application exits through notes-server. Other applications continue using their normal network path.

Be careful with DNS. Some clients resolve hostnames locally before using the proxy. Choose the client’s remote-DNS option when DNS queries must travel through the tunnel too.

Keep long connections healthy

Networks and firewalls sometimes drop idle connections. Add per-host keepalives:

Host notes-server
  ServerAliveInterval 30
  ServerAliveCountMax 3

The client sends a message after 30 seconds of inactivity. It gives up after three unanswered messages.

This detects a dead connection. It does not keep a crashed remote process alive. Use tmux, screen, or a service manager for long-running server work.

Leave a stuck session

SSH has escape commands for an interactive session. Press Enter, then type:

~.

This disconnects the client when the remote shell or network is stuck. The escape character must appear at the start of a new line.

Use ~? to see the available escape commands. These sequences are interpreted by your local SSH client, not by the remote shell.

Debug a failed connection

Add verbose output:

ssh -v notes-server

Use -vv or -vvv for more detail.

The log shows configuration, address resolution, host-key negotiation, and authentication attempts. It often reveals the exact key SSH offered.

Common errors have different meanings:

  • Connection timed out: routing, firewall, or wrong address
  • Connection refused: the host answered but nothing accepts that port
  • Host key verification failed: identity is unknown or changed
  • Permission denied (publickey): the server rejected every offered identity
  • Too many authentication failures: the client offered too many keys before the right one

Do not solve every error by weakening security. Read the verbose output and fix the failing layer.

Test authentication without opening an interactive shell:

ssh -o BatchMode=yes notes-server true

BatchMode=yes disables password and passphrase prompts. This makes the command useful in automation because it fails instead of waiting for input.

If you administer the server, inspect its logs while making a test connection. On Ubuntu, a common command is:

sudo journalctl -u ssh --since '10 minutes ago'

Client logs explain what your computer attempted. Server logs explain why sshd accepted or rejected it. You often need both sides.

When you change server configuration, keep an existing session open. Validate the configuration before reloading:

sudo sshd -t

No output means the syntax check passed. This does not prove every access rule behaves as intended, so test a second login before closing the first session.

Server-side security basics

For a VPS, I use these boundaries:

  • a named administrative or deployment user instead of routine root login
  • key authentication with a passphrase
  • a firewall that exposes only required ports
  • password login disabled only after key access is proven
  • separate keys or users for automation where practical
  • prompt operating system and OpenSSH updates

SSH protects transport and authentication. It does not make every remote command safe. The connected user can still delete files or expose secrets allowed by their permissions.

How I use SSH

I put every regular server in ~/.ssh/config. The alias records the user, address, port, and identity once. Commands, scp, rsync, and Git can all reuse it.

I verify host keys, use passphrases, and keep agent forwarding off. For deployments, I prefer a narrow user and one tested remote script over a large quoted command.

I would not expose a database publicly just to reach it from my laptop. A local SSH tunnel gives temporary access through the existing authenticated connection.

The free SSH course goes deeper into keys, server hardening, automation, rotation, and recovery. The Ubuntu VPS course puts SSH into a complete server workflow.

Tagged: CLI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cli: