VMs & SSH#

Sometimes you just need a machine. Rent one, reach it safely with keys, and keep your work alive after you disconnect.

โฑ ~9 min read ยท ~20 min hands-on ๐Ÿ”— needs: Bash Scripting ยท Deployment Platforms

Serverless covers most workloads, but some jobs want a persistent box: a long scrape, a GPU fine-tune, a database you control. That means a VM โ€” and SSH is how you live on it.

Try it in 5 minutes โ€” keys, not passwords#

Generate a modern key pair and copy the public half to the server:

ssh-keygen -t ed25519 -C "[email protected]"     # ed25519: short, fast, secure
ssh-copy-id [email protected]                  # installs the PUBLIC key
ssh [email protected]

โœ… You’re in, with no password. The private key (~/.ssh/id_ed25519) never leaves your machine โ€” treat it like a password and give it a passphrase.

Make it pleasant with ~/.ssh/config:

Host scraper
    HostName 203.0.113.10
    User ubuntu
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60

Now it’s just ssh scraper.

Harden the box before you use it#

A fresh VM with a public IP starts getting password-guessing attempts within minutes. Three changes remove most of that risk โ€” in /etc/ssh/sshd_config:

PasswordAuthentication no      # keys only
PermitRootLogin no             # no direct root

then sudo systemctl restart ssh. Plus a firewall:

sudo ufw allow OpenSSH && sudo ufw enable

โš ๏ธ Keep your current session open while testing a new SSH config. If you lock yourself out with the only session closed, you need console access to recover.

Work that survives disconnection#

Close your laptop and a plain SSH job dies with the connection. Use a terminal multiplexer:

tmux new -s scrape      # start a named session
# run your long jobโ€ฆ
# Ctrl-B then D          detach โ€” the job keeps running
tmux attach -t scrape   # come back later, from anywhere

For a job that must restart on boot or after a crash, make it a systemd service instead:

# /etc/systemd/system/scraper.service
[Unit]
Description=Scheduled scraper
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/app
ExecStart=/home/ubuntu/.local/bin/uv run scraper.py
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now scraper && journalctl -u scraper -f

Moving files, and tunnelling#

scp data.parquet scraper:/home/ubuntu/            # one file
rsync -avz --progress ./data/ scraper:~/data/     # resumable, only changed files
ssh -L 8080:localhost:8000 scraper                # local:8080 โ†’ the VM's port 8000

That last one is a local port forward โ€” reach a service bound to the VM’s localhost without exposing it publicly. It’s the safe way to check an internal dashboard. (Cloudflare Tunnels solves the same problem without a public IP at all.)

When it fails#

SymptomCauseFix
Permission denied (publickey)Key not installed, or wrong userssh-copy-id; check the image’s default user
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGEDServer rebuilt, or a real MITMVerify, then remove the old known_hosts entry
Locked out after editing sshd_configBroke auth with no open sessionUse the provider’s web console; test before closing
Job dies when laptop sleepsRan in a bare SSH sessiontmux, or a systemd service
Connection drops when idleNAT/firewall timeoutServerAliveInterval 60
Disk full mid-runLogs/scrape output filled itdf -h, log rotation, mount a volume

Your turn (โ‰ˆ20 min)#

  1. Create the smallest VM your provider offers; connect with an ed25519 key.
  2. Add a ~/.ssh/config entry and connect with a one-word host alias.
  3. Disable password auth and confirm keys still work โ€” keep a session open.
  4. Start a long job in tmux, disconnect, reconnect, and confirm it survived.
  5. Convert it to a systemd service with Restart=on-failure; reboot and verify it comes back.

Checklist#

  • I use ed25519 keys and never share a private key.
  • I disable password auth and root login on public VMs.
  • I keep a session open while changing SSH config.
  • I use tmux or systemd so long jobs survive disconnection.
  • I can port-forward to reach an internal service safely.

Go deeper#