Use when a Hetzner node is unreachable, SSH fails, cloud-init seems broken, or provisioning hangs. Boots into rescue mode via hcloud CLI to inspect filesystem, logs, SSH keys, sshd config, and cloud-init state without needing SSH access to the node itself.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
When a Hetzner Cloud server is unreachable (SSH hangs, provisioning stuck, cloud-init failure), this skill uses Hetzner's rescue mode to mount the node's filesystem and inspect everything from the outside — no working SSH required.
/debug-nodeWhen invoked, ask for:
hcloud server list)hcloud CLI installed and configured with a valid tokenhcloud server listLeap Micro uses a transactional-update system on btrfs. This is the mental model for everything below.
| Layer | Writable? | Persists reboot? | Persists Hetzner snapshot? |
|---|---|---|---|
/usr (snapshot) | No (read-only) | Yes | Yes |
/etc via transactional-update shell | Yes (new snapshot) | Yes (after reboot) | Yes |
/etc via direct edit on running system | Yes (volatile overlay) | No | No |
/var (separate subvolume) | Yes | Yes | Yes |
Rule: Any /etc change that must survive MUST go through transactional-update --continue shell.
Packer build phases:
install_packages: Inside transactional-update — changes persistclean_up: Volatile overlay — /etc changes are lost in the Hetzner snapshothcloud server list -o columns=id,name,status,ipv4 | grep <pattern>hcloud server enable-rescue <SERVER_ID> --type linux64
hcloud server reboot <SERVER_ID>
sleep 30Save the rescue root password from the output (usually key auth works, but just in case).
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@<SERVER_IP># Mount btrfs top-level
mount -o subvolid=5 /dev/sda3 /mnt
# List snapshots — highest number is active
ls /mnt/@/.snapshots/Layout:
/mnt/@/.snapshots/N/snapshot/ latest active snapshot — /etc lives here
/mnt/@/root/ /root home (/root/.ssh/authorized_keys)
/mnt/@/var/ /var (logs, cloud-init state, journal)Key: /etc is inside the snapshot. /var and /root are separate subvolumes at @/var and @/root.
mount /dev/sda1 /mntSet this once and use throughout:
SNAP=/mnt/@/.snapshots/N/snapshot # replace N with highest snapshot numberStart here — most provisioning failures trace back to cloud-init.
cat /mnt/@/var/lib/cloud/data/result.json
cat /mnt/@/var/lib/cloud/data/status.json
cat /mnt/@/var/lib/cloud/instance/datasource
# What Terraform actually sent
cat /mnt/@/var/lib/cloud/instance/user-data.txt
zcat /mnt/@/var/lib/cloud/instance/user-data.txt.i 2>/dev/null
# Logs
tail -100 /mnt/@/var/log/cloud-init.log
tail -100 /mnt/@/var/log/cloud-init-output.logExpected: DataSourceHetzner, no errors.
Watch for: Skipping modules — means cloud-init already ran for this instance-id.
Cloud-init facts on Hetzner + Leap Micro:
DataSourceHetzner (metadata API)cloudinit_config → gzip+base64 multipart MIME → user_datadisable_root: false prevents cloud-init from disabling root but does NOT unlock a locked accountssh_authorized_keys writes keys to /root/.ssh/authorized_keyscat /mnt/@/root/.ssh/authorized_keysCompare with your local pubkey. If missing, cloud-init failed to inject — check 5a logs.
Config loading order (first match wins):
1. /etc/ssh/sshd_config.d/40-kube-hetzner-authorized-keys-command.conf
2. /etc/ssh/sshd_config.d/50-cloud-init.conf
3. /etc/ssh/sshd_config.d/kube-hetzner.conf (MaxAuthTries 2)
4. /usr/etc/ssh/sshd_config.d/40-suse-crypto-policies.conf
5. /usr/etc/ssh/sshd_config (UsePAM yes)ls $SNAP/etc/ssh/sshd_config.d/
cat $SNAP/etc/ssh/sshd_config.d/*.conf
cat $SNAP/usr/etc/ssh/sshd_config
ls -la $SNAP/etc/ssh/ssh_host_*grep '^root:' $SNAP/etc/shadow| Pattern | Meaning | SSH pubkey works? |
|---|---|---|
root:*:... | Unlocked, no password | Yes |
root:!*:... or root:!:... | Locked | No (PAM rejects with UsePAM yes) |
This is fixed in packer (usermod -p '*' root inside transactional-update) with a cloud-init bootcmd safety net. If you see a locked account on a fresh node, the packer snapshot needs rebuilding.
journalctl -D /mnt/@/var/log/journal/ -u sshd --no-pager | tail -50
journalctl -D /mnt/@/var/log/journal/ -u k3s --no-pager | tail -30
journalctl -D /mnt/@/var/log/journal/ -u rke2-server --no-pager | tail -30
journalctl -D /mnt/@/var/log/journal/ -u rke2-agent --no-pager | tail -30ls $SNAP/etc/NetworkManager/system-connections/
cat $SNAP/etc/NetworkManager/system-connections/*.nmconnection 2>/dev/nullcat $SNAP/etc/rancher/k3s/config.yaml 2>/dev/null
cat $SNAP/etc/rancher/rke2/config.yaml 2>/dev/null
cat /mnt/@/var/lib/rancher/k3s/server/token 2>/dev/nullcat $SNAP/etc/selinux/config
chroot $SNAP rpm -qa | grep -iE 'selinux|k3s|rke2'
grep -i 'avc:.*denied' /mnt/@/var/log/audit/audit.log | tail -50
journalctl -D /mnt/@/var/log/journal/ --no-pager | grep -i 'avc:.*denied' | tail -50For workload denials, follow docs/selinux.md: collect the AVC lines,
workload name/version, k3s/RKE2 distribution, OS image, and udica result before
proposing upstream policy changes. Do not globally disable SELinux as the first
answer; use per-pool selinux = false only as the last resort for a workload or
nodepool that cannot run under policy.
Edit files in the active snapshot ($SNAP), not in @/ base.
# Unlock root account (if locked)
sed -i 's/^root:!*/root:*/' $SNAP/etc/shadow
# Fix authorized_keys
mkdir -p /mnt/@/root/.ssh
echo "ssh-ed25519 AAAA..." > /mnt/@/root/.ssh/authorized_keys
chmod 700 /mnt/@/root/.ssh && chmod 600 /mnt/@/root/.ssh/authorized_keys
# Regenerate host keys
mount --bind /proc $SNAP/proc && mount --bind /sys $SNAP/sys && mount --bind /dev $SNAP/dev
chroot $SNAP ssh-keygen -A
umount $SNAP/proc $SNAP/sys $SNAP/devNote: Rescue-mode edits are immediate fixes. The proper long-term fix belongs in the packer template or cloud-init.
umount /mnt 2>/dev/null
exithcloud server disable-rescue <SERVER_ID>
hcloud server reboot <SERVER_ID>
sleep 60
ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@<SERVER_IP> 'echo ok'| Symptom | Likely Cause | Check | Fix |
|---|---|---|---|
| SSH timeout | Firewall or network | Hetzner firewall rules | Open port 22 |
| SSH "Connection refused" | sshd not running | Journal logs | Fix sshd config syntax |
| SSH key rejected | Keys not injected | authorized_keys empty | Check cloud-init logs |
| SSH "Too many auth failures" | Agent offers too many keys | MaxAuthTries 2 | Use -o IdentitiesOnly=yes |
| SSH "unable to authenticate" | Root locked, or key mismatch | /etc/shadow, authorized_keys | Rebuild packer snapshot |
| Provisioner hangs "Still creating" | SSH can't connect | All above | Fix underlying SSH issue |
| Cloud-init skips modules | Already ran for instance-id | cloud-init.log | Clean /var/lib/cloud/instance |
| k3s/rke2 not starting | Config or SELinux | Journal + audit.log | Fix config or policy |
| Workload denied by SELinux | Missing workload policy | AVC lines in audit/journal | Follow docs/selinux.md; try udica before disabling a pool |
| Network/subnet destroy hangs | Autoscaler-created server outside Terraform state | hcloud server list for cluster-name or kh-ci-* leftovers | Delete only after control plane is dead, or scale autoscaler min_nodes = 0 first |
/etc change vanished | Edited outside transactional-update | Check packer phase | Move change to phase 2 |
# Verbose with specific key (avoids agent key spray hitting MaxAuthTries 2)
ssh -vvv -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@<SERVER_IP>
# In -vvv output:
# "Offering public key: ..." → key was offered
# "Server accepts key: ..." → success path
# "Authentications that can continue: ..." → key was REJECTED
# "Too many authentication failures" → agent sent too many keyssubvolid=5 — gets the real btrfs root, navigate to @/.snapshots/N/snapshot//etc lives/var is separate — logs and cloud-init are at /mnt/@/var/, not inside the snapshotjournalctl -D /path/to/journal/-o IdentitiesOnly=yes — kube-hetzner sets MaxAuthTries 2transactional-update outputscripts/destroy.sh from the Terraform root; it retries only the known ingress-LB detach race and prints a read-only orphan report. Use scripts/cleanup.sh only as the forceful fallback.a8b696d
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.