SSH Keys & Read-Only SFTP

RunHacks SSH keys and read-only SFTP access

Your RunHacks home directory, on your own machine, with your own tools. Enrol a public key, connect over SFTP, read anything, change nothing.

What this is

RunHacks runs an SFTP server on runhacks.sh, port 2222. It serves exactly the files the Files app shows you inside RunHacks OS: your home directory, your challenge tree, your generated files. Nothing else, and nobody else's.

sftp -P 2222 your-username@runhacks.sh

Your username is the same name you use for SMTP and IMAP, the part before @runhacks.sh. The port is 2222, not 22. Port 22 on a public address gets scanned around the clock, so we moved off it.

Authentication is by SSH public key only. There is no password prompt and there is no keyboard-interactive fallback. Add a key in RunHacks OS first: press alt+x, or open the SSH app from the menubar.

Why it is read-only

Every mutating operation is refused: upload, write, append, mkdir, rmdir, delete, rename, symlink, chmod, chown, truncate. So is an interactive shell, so is ssh user@host command, and so is port forwarding. The only subsystem the server speaks is SFTP, and only its read half.

That is not caution for its own sake. Your home directory is not a disk. It is rendered on demand by the challenge engine from your progress, your mailbox, and the challenge definitions. There is no byte on a filesystem for an upload to land in, so a write would either be silently discarded or would have to invent storage that the game does not model. Refusing is the honest answer. If you want something in that tree to change, change it through the challenge.

Generate a key

ssh-keygen -t ed25519 -C "work-laptop"

That produces two files in ~/.ssh:

  • id_ed25519 is the private key. It stays on this machine forever. It is the credential.
  • id_ed25519.pub is the public key. It is one line of text, it is safe to hand out, and it is the only half we ever want.

Print the public half and copy the whole line:

cat ~/.ssh/id_ed25519.pub

Paste it into the SSH app in RunHacks OS with a label that names the device. The label is what you will use later to work out which key to revoke.

We never want your private key

RunHacks will never ask for a private key, and neither will any legitimate service. Not support, not a password reset, not a "verification" step, not a challenge character who sounds like they have authority. If anything asks you for one, that is the attack, and noticing it is the skill.

A private key file starts with a line reading -----BEGIN OPENSSH PRIVATE KEY-----. If you are ever about to paste that into a form, a chat window, a ticket, or a terminal that is not yours, stop. The moment it leaves your machine it is burned: generate a new keypair and revoke the old one everywhere it was enrolled.

The enrolment form refuses anything containing PRIVATE KEY before it sends the request, so a slip of the clipboard does not reach us. Treat that as a seatbelt, not a plan.

Ed25519 over RSA

Use Ed25519. The keys are short, signing is fast, there are no parameters to get wrong, and there is no bit-size decision to make badly. An Ed25519 public key fits on one line of a terminal, which makes it easy to eyeball.

RSA still works and we accept it, but only at 2048 bits or more. Anything smaller is trivially breakable with rented compute and is refused. DSA (ssh-dss) is refused outright: it is 1024 bits by definition, and one bad nonce leaks the private key. If you have a DSA key lying around, it is old enough to retire.

ECDSA on the NIST P-256, P-384 and P-521 curves is accepted for people who need it for hardware reasons. Ed25519 is still the better default.

Use a passphrase

A private key with no passphrase is a plaintext credential sitting on disk. Anyone who reads that file, or restores it from a backup, or picks the laptop up off a table, has your access.

ssh-keygen asks for a passphrase at generation time. Answer it. If you already made one without, add a passphrase in place, no new key required:

ssh-keygen -p -f ~/.ssh/id_ed25519

Use ssh-agent

A passphrase you type on every single connection is a passphrase you will eventually remove. Load the key into an agent once per session instead:

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

On macOS, store it in the keychain so the agent picks it up after a reboot:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Set an agent lifetime if you leave your machine unattended: ssh-add -t 8h ~/.ssh/id_ed25519. Agent forwarding is a separate thing, it is refused by our server, and you should be wary of it everywhere else too.

One key per device

One key per device. Never copy a private key from your laptop to your desktop, your phone, a VM, or a container. Generate a fresh one on each machine and enrol each public half separately.

The reason is revocation. A key that exists on exactly one device can be revoked without touching any other device: you lose the laptop, you delete that one key, and every other machine you own keeps working. A key copied to four places has to be replaced in four places, under time pressure, on the worst day. That is the entire point of the label field.

Browse it

Interactive SFTP, which is the easiest way to look around:

sftp -P 2222 your-username@runhacks.sh
sftp> ls
sftp> cd challenges
sftp> get README.md
sftp> bye

Copy a single file, or a whole subtree, without an interactive session:

scp -P 2222 your-username@runhacks.sh:docs/sftp.txt .
scp -P 2222 -r your-username@runhacks.sh:challenges ./challenges

Note that scp in modern OpenSSH runs over the SFTP protocol, which is why it works here. The legacy scp wire protocol is refused, as is rsync, which needs a shell on the far end.

Mount it

With sshfs, so your editor and your grep can see the tree as ordinary files:

mkdir -p ~/mnt/runhacks
sshfs -p 2222 your-username@runhacks.sh:/ ~/mnt/runhacks -o ro,reconnect,idmap=user

Unmount with fusermount -u ~/mnt/runhacks on Linux, or umount ~/mnt/runhacks on macOS. The ro option is not what makes the mount read-only, the server is, but it keeps your tools from trying and gives you a clearer error when they do.

With rclone, if you would rather sync a local copy than hold a mount open:

rclone config create runhacks sftp \
    host runhacks.sh port 2222 \
    user your-username key_file ~/.ssh/id_ed25519
rclone ls runhacks:
rclone copy runhacks:challenges ./challenges

Keep the traffic reasonable. A recursive mirror on a loop is the kind of automated hammering the Rules of Engagement ask you not to do.

Verify the host key

The first time you connect, your client prints a fingerprint and asks whether to trust it. That prompt is the only moment you get to check you are talking to us and not to whoever happens to control the network you are on. Do not just type yes and move on.

This is our ED25519 host key fingerprint. You are reading it over HTTPS, which is a different path than SSH takes, and that is the whole point of publishing it here:

SHA256:OcjyDBUOfpdXVuAUsM/JpatJJIwad+HZoD1JfmWW4TE

Print the fingerprint the server is presenting and check the SHA256 value against the line above. The text after it is just a label and will differ depending on how you ask:

ssh-keyscan -p 2222 runhacks.sh 2>/dev/null | ssh-keygen -lf -

Run it from a network you trust, compare it with what your client showed you, and if you want real assurance, run it again from somewhere else entirely: a phone tether, a different office, a cheap VPS. An attacker who can sit between you and us on one network usually cannot sit between us and all of them. Matching fingerprints across independent paths is a decent signal. Write the value down once, in your notes, next to the label.

Our host key is fixed and does not rotate. Once it is in your ~/.ssh/known_hosts, it should never change again. If your client ever warns you that the host identification has changed, that is not routine maintenance. Stop, do not type past it, and mail security@runhacks.sh with the fingerprint you saw.

Revoking a lost device

Open RunHacks OS, press alt+x for the SSH app, select the key labelled with that device, and press x. Access ends at the next connection attempt.

Then rotate whatever else that device held: your RunHacks API keys, anything in a shell history, anything in a password manager that was unlocked. A lost laptop is rarely one credential.

What we store

Per key: the public key text, its SHA256 fingerprint, the key type and size, the comment that came with it, the label you gave it, when you added it, and the last time it was used to log in. That is the whole list.

We do not have your private key, we cannot recover it for you, and there is no support process that would ever involve you sending it to us.

In scope

runhacks.sh on port 2222 is in scope for your own account, on the same terms as the mail server. Poke at the protocol, see what the server refuses and how it refuses it, and try to make it hand you something that is not yours. Other players' files are not in scope, and neither is the host underneath.

If you find a way to read someone else's home directory, or to make a write stick, that is a real vulnerability rather than a flag. Stop and mail security@runhacks.sh.

RunHacks OS v1.0 · kernel rh-tui 0.9.2 · build 2026.08.25  ·  about RunHacks