// Package felhomsshd manages the dedicated OOB sshd instance (TASK H1). It is a SECOND sshd — // separate port, config, host keys, AuthorizedKeysFile, and systemd unit — that COEXISTS with the // customer's/stock sshd on :22 (never touched). Design + safety earned by // SPIKE-felhom-sshd-2026-07-05 (§2 claim, §3 SAFE unit, §5 reload-not-restart, §7 AuthorizedKeysFile // isolation) and SPIKE-oob-wg-operator-peer-2026-07-05 (the tunnel-only belt). // // The agent RENDERS the config (Port from the claim) and reloads on change — the wg-felhom pattern. // It NEVER declares RuntimeDirectory= (G1 [SF-1]) and NEVER restarts on a config change [SF-2]. package felhomsshd import ( "fmt" "strings" ) const ( // ConfDir is the dedicated config tree (host-install creates it; the agent renders the config). ConfDir = "/etc/felhom-sshd" // ConfPath is the rendered sshd config (referenced by the static unit's ExecStart/ExecReload). ConfPath = ConfDir + "/sshd_config" // HostKeyPath is the dedicated ed25519 host key (host-install generates it; stable across reloads). HostKeyPath = ConfDir + "/ssh_host_ed25519_key" // AuthKeysDir holds per-user authorized_keys OUTSIDE ~/.ssh, so the customer's sshd (which reads // ~/.ssh/authorized_keys) structurally cannot honour the operator key [SF-3/§7]. AuthKeysDir = ConfDir + "/authorized_keys" // PortFile persists the claimed port (idempotent re-pick). PortFile = ConfDir + "/port" // PidFile is the instance pidfile (NOT a RuntimeDirectory — that is the G1 incident cause). PidFile = "/run/felhom-sshd.pid" // Unit is the systemd unit name. Unit = "felhom-sshd" // OperatorUser is the default operator login (scoped sudo; key in AuthKeysDir only). OperatorUser = "felhom-op" ) // renderConfig builds the felhom-sshd config for a claimed port. Pure + deterministic (byte-stable // for a given port → a stable conf-hash, no reload churn). The security posture is the SAFE template // from the spike §3: key-only, dedicated host key + AuthorizedKeysFile, AllowUsers scoped to // root+felhom-op, binds 0.0.0.0 (+ ::) so it never waits on a late interface, no RuntimeDirectory. func renderConfig(port int) (string, error) { if port < 1 || port > 65535 { return "", fmt.Errorf("felhomsshd: port %d out of range", port) } if port == 22 { // The whole point is coexistence — the dedicated instance must NEVER claim :22 [SF-4/trap 5]. return "", fmt.Errorf("felhomsshd: refusing to render on :22 (the stock/customer sshd port)") } var b strings.Builder b.WriteString("# felhom OOB sshd — agent-managed (H1); DO NOT EDIT\n") fmt.Fprintf(&b, "Port %d\n", port) b.WriteString("ListenAddress 0.0.0.0\n") b.WriteString("ListenAddress ::\n") fmt.Fprintf(&b, "HostKey %s\n", HostKeyPath) fmt.Fprintf(&b, "PidFile %s\n", PidFile) fmt.Fprintf(&b, "AuthorizedKeysFile %s/%%u\n", AuthKeysDir) b.WriteString("PasswordAuthentication no\n") b.WriteString("PermitRootLogin prohibit-password\n") b.WriteString("PubkeyAuthentication yes\n") b.WriteString("KbdInteractiveAuthentication no\n") b.WriteString("UsePAM yes\n") fmt.Fprintf(&b, "AllowUsers root %s\n", OperatorUser) b.WriteString("X11Forwarding no\n") b.WriteString("Subsystem sftp internal-sftp\n") return b.String(), nil }