A Deploy Key That Can Only Pull

root on vmi2633427 has no GitHub identity of its own, and the deploy only ever needs to pull — so the key it authenticates with is generated, scoped, and verified to be read-only before it ever touches the server.

A Deploy Key That Can Only Pull

The nextcloud-selfhosted README states the operating model in one line: "The server's deploy key is read-only. Commit and push from the workstation, then git pull on the server." Everything about how the key is generated, installed, and verified exists to make that sentence actually true, not just documented.

A key scoped to pull, verified to reject push
A key scoped to pull, verified to reject push

The repo's identity confirms which key answered — not just that some key did.

Generating a key with no other purpose

ssh-keygen -t ed25519 -N '' -f /root/.ssh/github_nextcloud -C 'vmi2633427-nextcloud-deploy'
cat /root/.ssh/github_nextcloud.pub     # add at repo → Settings → Deploy keys, write access OFF

This key is generated specifically for this repo, on this machine, with a comment that names exactly what it's for. It is added to the GitHub repository's own Deploy keys list — a mechanism distinct from a personal SSH key or a personal access token — with write access explicitly left off. GitHub enforces that boundary server-side: even if the private key were somehow used to attempt a push, GitHub rejects it, because the deploy key itself carries no write grant.

Forcing the right identity to be used

A deploy key only protects anything if the server actually authenticates with it, rather than falling back to some other SSH identity that happens to have broader access. /root/.ssh/config pins that:

Host github-nextcloud
  HostName github.com
  User git
  IdentityFile /root/.ssh/github_nextcloud
  IdentitiesOnly yes

IdentitiesOnly yes is the line that matters most and the one easiest to skip. Without it, ssh will offer every identity it has available — including, say, an admin's personal key if one happens to be loaded in an agent reachable from that shell — before falling back to the one named in IdentityFile. If the server ever authenticated as a full-access personal identity instead of the scoped deploy key, the entire read-only design would be silently bypassed, and nothing about the connection succeeding or failing would tell you that happened.

Proving it, not assuming it

ssh -T git@github-nextcloud
# "Hi Nissaar/nextcloud-selfhosted!" — naming the REPO, not just the user

The response text is the actual proof. GitHub's SSH greeting names the repository when a deploy key answers, and names the username when a personal key answers. Getting "Hi Nissaar!" back instead of "Hi Nissaar/nextcloud-selfhosted!" would mean the wrong identity is being used — a personal key with full push access, silently doing the job a scoped key was supposed to do. The current key's fingerprint, SHA256:8TVonJn3DZkJ0l1M2jxOQP3VhyVEsHjUoQ6cQ16eAzg, is recorded in the README precisely so a future audit can confirm which key is actually installed without needing to regenerate anything.

Why this matters for a box that runs as root

vmi2633427 runs its deploys as root, with no GitHub SSH identity of its own by default — that's stated as a design fact, not an oversight. A box that runs as root and can push is a box where a compromised deploy pipeline, or a mistaken git push typed on the wrong host, can rewrite the source of truth for the entire infrastructure repo. Making push structurally impossible for that identity — not "we don't push from there," but "that key cannot push, full stop, enforced by GitHub" — removes an entire class of mistake regardless of what runs on the server or what compromises it.

Series: The Self-Hosting Stack. Next: why the infra repo should never contain the data it manages.