Skip to content

Adopt a host

nerthus-config host adopt is how a rented machine becomes a host of the estate. It is one of the three times the tool reaches a host over SSH; after it, the host's own agent pulls its declaration every five minutes and nothing pushes to it again. This page is the procedure, from a provider's delivery mail to a merged declaration.

What adopt does, in order

  1. Confirms the host key before anything else is sent. The key must be the one the provider console shows: the host's row in inventory/hosts.yaml names it (ssh_host_key_sha256), or you pass it as --fingerprint, or you confirm it at the prompt. A host presenting any other key is not even logged into.
  2. Reads the host: its OS family and release against the declaration, its architecture, and every key in root's and nerthus-admin's authorized_keys. A key that is neither yours nor declared in the row stops the adopt here, before any write, and is printed with its fingerprint and comment.
  3. Installs the agent the declaration pins (pins/agent.yaml), checking its SHA-256 on the host.
  4. Converges the adopt cookbook: git, an SSH client and sudo; the admin account nerthus-admin with your key and no password; root's initial password discarded; password logins off; the agent's configuration. It then plans the cookbook again and refuses unless that second plan changes nothing.
  5. Runs the agent's init, which mints the host's age identity and the SSH key it reads Nerthus.Infra with, both on the host. Only their public halves come back.
  6. Reads everything back: the authorized keys by fingerprint, sshd -T, root's password field, the agent's own verify.
  7. Declares the host on your checkout's branch: the row gains the confirmed key, the identity, ssh_admin_user: nerthus-admin and the keys that may log in; a read-only deploy key on nerthus.infra carries the read key; a sops_recipient gives the host vault/hosts/<host>/; the console password row moves to retired: and its file is deleted. validate judges the result, and edits that add a finding are undone.

The provider's console password is gone after this. Root has no password, so the console's password prompt no longer opens the machine; recovering from a broken sshd is a reinstall at the provider. Everything on the host can be rebuilt from Nerthus.Infra.

Before you start

  • The host key, read at the provider console. Log in there with the delivered password and run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub. What it prints is the value you confirm; what the network answers is not evidence.
  • Your key on root. The delivery comes with a password; put your public key into /root/.ssh/authorized_keys (the provider's panel, or one ssh-copy-id over that password) and prove it with ssh -o PasswordAuthentication=no root@<address> true.
  • The pinned agent. Download the file pins/agent.yaml names from nerthus.config's package registry (nerthus-config-agent/<version>/nerthus-config-agent-<version>-x86_64-unknown-linux-musl). Adopt refuses a file whose SHA-256 differs.
  • A Nerthus.Infra checkout on a task branch, cut from origin/main. Adopt commits there and refuses main.

The procedure

Dry run first. It confirms the key, reads the host and prints the cookbook's plan, and changes nothing:

cd ~/gitlab/margonem/nerthus/it/Nerthus.Infra
nerthus-config host adopt nerthus-host-2 --agent ~/Downloads/nerthus-config-agent-0.7.0-x86_64-unknown-linux-musl

A host that is not declared yet takes its facts as flags, and gets a row with no roles:

nerthus-config host adopt nerthus-host-11 --address 203.0.113.7 --family debian --release 13 \
  --provider instantnode --console-credential NERTHUS_HOST_11_CONSOLE_PASSWORD --agent 

Then the adopt itself:

nerthus-config host adopt nerthus-host-2 --agent  --no-dry-run

It pushes the branch and opens the merge request (--no-push keeps it local). After the merge, nerthus-config apply . --from laptop --only gitlab_deploy_key creates the host's read key at GitLab; until then the agent's tick refuses with fetch.failed, and that is expected.

How you know it worked

On the host, journalctl -u nerthus-config-agent (Alpine: /var/log/messages) shows a tick every five minutes ending in plan: empty (no role is declared) and converged <commit>, and /var/lib/nerthus/facts.yaml exists. A second host adopt of the same host refuses: its row carries an identity.

When the agent refuses

The agent exits 0 on an ordinary refusal and names the cause:

Cause What it means
fetch.failed the read key is not at GitLab yet, or the network is down
host.unknown the host's row is not on main — the merge request is not merged
pin.backwards pins/agent.yaml names an agent older than the one the host converged with; a rollback ships as a higher version
agent.behind the pin names a newer agent; host adopt <host> --reinstall --agent <file> --no-dry-run installs it
secrets.not-tmpfs the secrets directory is not on tmpfs, and a decrypted secret is written nowhere else

It exits non-zero only on a broken installation: no configuration, or an identity that anyone but root can read. The timer's unit refuses to start in that case, and says why in the journal.

Replacing a host's identity

nerthus-config host rekey <host> --identity <recovery identity file> --no-dry-run mints a new identity and read key on the host, re-encrypts vault/hosts/<host>/ to them with the recovery identity, and declares both. Use it when a host's disk may have been read by somebody else.