Skip to main content

10 — Your fake VPS

Read this first: this lesson builds the "server" you will use for the rest of the course — a real Linux box you SSH into, running on your own machine, that you can destroy and rebuild in twenty seconds. It covers SSH keys, running a command remotely, copying files, and the permission rule that makes SSH fail with a message that explains nothing.

Time: about 45 minutes. Assumes lesson 05.

What we are building and what we are pretending​

The lab box is a container running Ubuntu 24.04, sshd, and its own Docker daemon. You log in as a deploy user, it has sudo, code lives in /srv/, and every command you learn transfers to a real server.

What it cannot teach you, stated up front because a course that pretends otherwise produces exactly the false confidence this one exists to fix:

Not availableWhy
Real DNSNothing on your laptop has a public name
Real TLS certificatesLet's Encrypt must reach you from the internet to issue one
A real public IPAnd therefore no real hostile traffic
systemdContainers do not boot an init system; ours runs sshd directly

Those four get read-only tours in Track B rather than labs.

Start it​

cd learn-devops/lab-vps
./up.sh

On the first run this generates a keypair, builds the image, and starts the box. Then:

./ssh.sh
deploy@lab-vps:~$

You are on a different machine now — a different filesystem, a different process list, a different hostname. Look around:

whoami; hostname; id
deploy
lab-vps
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),27(sudo),996(docker)
grep PRETTY /etc/os-release
PRETTY_NAME="Ubuntu 24.04.4 LTS"

Type exit to come back.

Keys, not passwords​

The box has PasswordAuthentication no in its sshd config — exactly like the real VPS after deploy/bootstrap.sh hardens it. The only way in is a key.

A keypair is two files:

FileWhat it isWho gets it
id_ed25519The private keyNobody. Ever. It stays on your machine.
id_ed25519.pubThe public keyCopied to the server, into ~/.ssh/authorized_keys

The mental model: the public key is a lock you mail out; the private key is the only thing that opens it. A server holding your public key can verify you without ever being able to impersonate you.

up.sh generated the pair with:

ssh-keygen -t ed25519 -f keys/id_ed25519 -N '' -C 'learn-devops-lab'

-N '' means no passphrase, which is fine for a throwaway key that never leaves your machine and wrong for anything that can reach something real.

Running one command instead of opening a shell​

This is the part that matters for CI, and it is easy to miss:

./ssh.sh 'docker ps'

SSH ran that command on the box and gave you the output. No interactive session, no terminal. That is precisely what a deploy step in GitHub Actions does — it opens an SSH connection, runs a script, and reads the exit code. Everything you learned about exit codes in lesson 01 applies across the network.

Copy a file up with scp:

scp -i keys/id_ed25519 -P 2222 ./somefile deploy@localhost:/srv/greeter/

Note -P 2222 for scp, capital P — ssh uses lowercase -p. This inconsistency has wasted more time than it has any right to.

Break it on purpose: the private key everyone can read​

Predict: what happens if your private key is readable by other users on your machine?

chmod 644 keys/id_ed25519
./ssh.sh
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'keys/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.

SSH refuses to use it. That is the client protecting you from yourself. Fix it:

chmod 600 keys/id_ed25519

600 is rw-------: owner reads and writes, nobody else sees anything. Those are the permission bits from lesson 01, and this is the first time they have refused to let you proceed.

Break it on purpose: the error that explains nothing​

This one is worth your full attention, because the client-side message is actively misleading.

Predict: on the server, ~/.ssh/authorized_keys is owned by the wrong user. What does the client see?

deploy@localhost: Permission denied (publickey).

That is all. No mention of ownership, no mention of the file. The real reason is in the server's log:

docker logs lab-vps | tail -3
Authentication refused: bad ownership or modes for file /home/deploy/.ssh/authorized_keys
Connection closed by authenticating user deploy 172.16.11.1 port 52236 [preauth]

sshd refuses to trust an authorized_keys file that anyone but the owner could have edited. If it did trust one, any user who could write that file could grant themselves access to the account.

The rules are:

PathRequired
~/.sshOwned by the user, mode 700
~/.ssh/authorized_keysOwned by the user, mode 600

The lesson to carry: when SSH says Permission denied (publickey), the useful information is on the server, not the client. Get at it with docker logs lab-vps here, or journalctl -u ssh on a real box. The client will never tell you.

This is a real trap and not a contrived one. This very lab hit it during construction: the public key was bind-mounted straight onto authorized_keys, which kept its owner from the host — a different uid than the container's deploy user. The box was correct, the key was correct, and login failed with four words. The entrypoint now installs the key with the right owner and mode instead.

Diagnose from the client side with -vvv, which prints every step of the negotiation:

ssh -vvv -i keys/id_ed25519 -p 2222 deploy@localhost

It will show the key being offered and rejected. It still will not tell you why — but knowing that the key was offered rules out half the possible causes.

Your escape hatch​

./reset.sh

Destroys the box and builds a fresh one in about twenty seconds, keeping the image cache. Every dangerous drill from here on tells you to break something; this is how you get back. Use it freely — the entire reason the lab is a container is that the reset button is free.

If something is deeply confused (usually iptables state after lesson 13):

./reset.sh --cold

Where this shows up in MotorPH​

  • deploy/bootstrap.sh creates the deploy user, installs a public key, and only then turns off password authentication — with a comment noting that hardening sshd before a key is installed is how you lose access to the box.
  • Every deploy job uses appleboy/ssh-action, which is this same "run a command over SSH" mechanism with the key supplied from a GitHub Secret. You will use it in lesson 18.
  • The real workflows connect to VPS_HOST as an IP address, not the domain — the domain resolves to the CDN, and a CDN does not proxy SSH.

Recap​

  • A keypair is a public lock you distribute and a private key you never do; the server holds only the public half.
  • ssh host 'command' runs one command and returns its exit code — that is exactly what a CI deploy step does.
  • Permissions are enforced on both ends: 600 on your private key, 700/600 on the server's .ssh directory and authorized_keys.
  • Permission denied (publickey) tells you nothing. The server's log tells you everything.

Next: 11 — Users and permissions.