Documentation
¶
Overview ¶
Package sshutil holds the small SSH helpers shared by the CLI and the TUI: installing a public key into a live guest's authorized_keys over SSH. It lives outside cmd so internal/tui (which cannot import cmd) can reuse the exact same logic behind `hlab {vm,ct} add-ssh-key` and the dashboard's inject-key action.
Index ¶
- func AgentHasKeys() bool
- func AppendAuthorizedKey(user, ip, pubKey string) error
- func AuthorizedKeyCommand(pubKey string) string
- func AuthorizedKeyCommandFor(user string) string
- func Forget(host string) error
- func HostKeysMismatch(recorded, live map[string]string) bool
- func IsHostKeyMismatch(sshOutput string) bool
- func KeylessAddKeyError(name string, isLXC bool) error
- func KnownHostsFiles(host string) []string
- func LiveHostKeys(host string) (map[string]string, error)
- func RecordedHostKeys(host string) map[string]string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AgentHasKeys ¶ added in v0.10.4
func AgentHasKeys() bool
AgentHasKeys reports whether the local SSH agent holds at least one identity.
It gates the dotfiles catalog entry: the playbook clones dotfiles_repo (an SSH URL) from inside the guest over the operator's forwarded agent, so an agent with no identities cannot clone it. It lives here rather than in cmd because the engine — which both the CLI and the TUI go through — is where that check has to happen for every caller to get it.
It fails closed: an ssh-add that won't run at all counts as "no keys", since a forwarded agent hlab cannot even interrogate is not one Ansible can clone with.
func AppendAuthorizedKey ¶
AppendAuthorizedKey installs a public key into a live guest's ~/.ssh/authorized_keys over SSH, non-interactively (BatchMode) and idempotently: it creates ~/.ssh (700) and authorized_keys (600) if missing and appends the key only when the exact line isn't already present (grep -qxF).
BatchMode=yes disables every interactive prompt (notably the password prompt), so this NEVER blocks asking for a password: if the guest doesn't already trust a key hlab can use, ssh fails with "Permission denied" instead of prompting. That auth failure is translated into an actionable message (see authFailureHint) rather than a raw ssh dump — the usual cause is a keyless guest whose sshd refuses password auth (e.g. an LXC container's root, PermitRootLogin prohibit-password), which no amount of retrying over SSH can fix.
func AuthorizedKeyCommand ¶
AuthorizedKeyCommand returns the idempotent shell command that appends pubKey to the login user's ~/.ssh/authorized_keys. Shared by both install paths — SSH (AppendAuthorizedKey) and the Proxmox console (engine.InjectSSHKeyViaConsole, which logs in as root, where ~ is /root) — so the key is installed identically however hlab reaches the guest.
func AuthorizedKeyCommandFor ¶
AuthorizedKeyCommandFor returns the idempotent `/bin/sh -c` script that appends an SSH public key to a NAMED user's ~/.ssh/authorized_keys with correct ownership and permissions. Unlike AuthorizedKeyCommand (which targets the connection user's ~, i.e. root over the LXC console), this resolves the user's home via `getent passwd` and chowns everything to that user — it is used by the VM guest-agent path (engine.InjectSSHKeyViaAgent), where the agent runs as root but the key belongs to the unprivileged login user.
The key is NOT embedded in the script: it is read from stdin (`key=$(cat)`), so the caller feeds it via the guest agent's input-data. That avoids shell-quoting the key into the command entirely — the script is identical for every key.
func Forget ¶ added in v0.10.3
Forget removes every host key recorded for host from the known_hosts files ssh would actually consult for it.
hlab is what puts those entries there in the first place: AppendAuthorizedKey connects with StrictHostKeyChecking=accept-new, and the plain ssh behind `hlab {vm,ct} ssh` records the key on the usual TOFU prompt. Both write to the operator's real known_hosts. Nothing ever took them back out, so destroying a guest and creating another one at the same address (the normal homelab pattern — recycled test IDs and a small static-IP pool) left a stale entry behind and the next ssh died with "REMOTE HOST IDENTIFICATION HAS CHANGED".
Callers must hook this to a *mutation* (create/destroy), never to a connection. At create/destroy hlab knows first-hand that the guest at host is brand new or gone, so the recorded key is stale by construction and dropping it decides nothing about trust. Dropping entries at connect time instead would silently turn every genuine man-in-the-middle warning into an accept, which is the one thing known_hosts exists to prevent.
Removal goes through `ssh-keygen -R` rather than editing the file directly: known_hosts is often hashed (HashKnownHosts), where the hostname never appears in plain text and no grep/sed can find it. ssh-keygen also handles every key type at once and retains the previous contents as <file>.old.
func HostKeysMismatch ¶ added in v0.10.3
HostKeysMismatch reports whether anything recorded for a host contradicts what it actually presents — the precise definition of a stale entry, and the only thing `known-hosts clean --all` is allowed to remove.
Only algorithms present on BOTH sides are compared. An algorithm recorded but not offered (or offered but not recorded) is not a contradiction: sshd may have stopped offering a type, or the operator may only ever have accepted one. That asymmetry is normal and must not cost a correct entry.
func IsHostKeyMismatch ¶ added in v0.10.3
IsHostKeyMismatch reports whether ssh output is the "host identification has changed" refusal — a stale known_hosts entry — rather than any other failure. AppendAuthorizedKey connects with accept-new, which silently records an unknown host but hard-fails on a changed one, and that failure is not an auth failure, so authFailureHint doesn't catch it and the caller would otherwise print a raw ssh dump.
func KeylessAddKeyError ¶
KeylessAddKeyError is the error returned by the add-ssh-key / inject-key flows when a key cannot be installed at all. For a VM the recovery channel is the QEMU guest agent (engine.InjectSSHKeyViaAgent), so the VM branch is the fallback shown only when that channel can't be used — the agent isn't running or the token lacks VM.GuestAgent.Unrestricted (the real agent/privilege error is threaded up instead where possible). For a container hlab CAN inject the first key over the Proxmox console using the root password: it uses the stored password, or prompts for it (or takes `--password`), so a keyless container is recoverable as long as you know the root password. This error is for the case where even that is impossible (no password stored, none entered, and no prompt available — e.g. a non-interactive run). The fix is to supply the root password or seed a key out of band. Shared by the CLI handler, the TUI inject flow and the engine's console injection.
func KnownHostsFiles ¶ added in v0.10.3
KnownHostsFiles returns the user known_hosts files ssh would consult for host, resolved through `ssh -G` so that a UserKnownHostsFile override in the operator's ssh_config is honoured — a bare `ssh-keygen -R` would edit the default file, which may not be the one in use. Files that don't exist are dropped: ssh-keygen -R fails outright (exit 255, "Cannot stat") on a missing file, and a file with no entries is nothing to clean anyway.
Best-effort by design: any failure to interrogate ssh yields no files, and the caller simply skips the cleanup.
func LiveHostKeys ¶ added in v0.10.3
LiveHostKeys returns the host keys the guest at host currently presents, keyed by algorithm, read straight off the wire with ssh-keyscan. An unreachable guest yields an error, which callers treat as "don't touch this entry" — a host we cannot ask about is a host we cannot prove stale.
func RecordedHostKeys ¶ added in v0.10.3
RecordedHostKeys returns the keys known_hosts currently records for host, keyed by algorithm. ssh-keygen -F is used rather than reading the file, so hashed entries resolve correctly.
Types ¶
This section is empty.