# Bulk SSH Key Injection via Hetzner Rescue Mode

When deploying the `itpp-infra` key to multiple servers simultaneously, this pattern saves time over one-at-a-time rescue.

## Process

1. **Register the SSH key** in the Hetzner project (one-time per key, SKILL.md step 1)
2. **Enable rescue mode + reboot on all targets** — batch via Python:
   ```python
   targets = [(SERVER_ID, "hostname", "IP"), ...]
   for sid, name, ip in targets:
       data = json.dumps({"type": "linux64", "ssh_keys": [KEY_ID]})
       req = urllib.request.Request(
           f'https://api.hetzner.cloud/v1/servers/{sid}/actions/enable_rescue',
           data=data.encode(), headers=headers, method='POST')
       urllib.request.urlopen(req, timeout=15)
       req2 = urllib.request.Request(
           f'https://api.hetzner.cloud/v1/servers/{sid}/actions/reboot',
           data=b'{}', headers=headers, method='POST')
       urllib.request.urlopen(req2, timeout=15)
   ```
3. **Wait 60-90s** — poll SSH port on each:
   ```bash
   for ip in "ip1" "ip2" ...; do
       timeout 5 bash -c "echo > /dev/tcp/$ip/22" 2>/dev/null && echo "$ip: UP" || echo "$ip: WAITING"
   done
   ```
4. **Inject key on all** — loop over each rescue host:
   ```bash
   PUBKEY=$(cat ~/.ssh/itpp-infra.pub)
   for ip in ...; do
       ssh -o StrictHostKeyChecking=no -i ~/.ssh/itpp-infra root@$ip "
           mount /dev/sda1 /mnt 2>/dev/null || mount /dev/vda1 /mnt
           echo '$PUBKEY' >> /mnt/root/.ssh/authorized_keys
           chmod 600 /mnt/root/.ssh/authorized_keys
           umount /mnt
       "
   done
   ```
5. **Disable rescue + reboot all** (same pattern as step 2, but `disable_rescue` + `reboot`)
6. **Wait 90s**, then verify SSH access with `hostname` command

## Pitfalls

- **Not all servers use `sda1`** — Hetzner rescue only has access to the raw block device. Check with `lsblk` on first SSH. On CPX11/CPX21 it's typically `sda1`; on systems with NVMe it could be `nvme0n1p1`. Fall back to `for part in sda1 vda1 nvme0n1p1; do mount /dev/$part /mnt 2>/dev/null && break; done`.
- **Remount after first mount attempt** — If a partition is already auto-mounted from a previous rescue, the second `mount` fails. Check `mountpoint -q /mnt` before mounting.
- **Boot order matters** — Enable rescue FIRST, then reboot. Doing them in the wrong order leaves the server off.
- **`disable_rescue` BEFORE `reboot`** — Reboot with rescue still enabled just boots back into rescue.
- **Server POST times vary** — CPX11 (~40s) vs CPX41 (~60s). Wait for SSH port before attempting connection.
- **app1.itpropartner.com (87.99.144.163) has intermittent SSH delay** — port 80 responds quickly but SSH can take an extra 30-60s. Retry with longer timeout.
- **ai.itpropartner.com (178.156.167.181, CPX41) hosts the LLM proxy** — Rebooting it takes the agent offline. SSH key injection via rescue mode failed on this tier — the `ssh_keys` parameter was accepted (rescue enabled, rebooted) but the key was never authorized, despite the same procedure working on 8 other Hetzner boxes (CPX11, CPX21). Root cause unknown. The key IS registered in the project (ID 114709791) and will work if the server is ever rebuilt. A local ollama+Qwen2.5 7B fallback is installed on the netcup box for maintenance periods. Alternative injection method: use the rescue `root_password` from `enable_rescue` instead of key injection.
