Files
admin c983a25609 feat(felhomsshd): dedicated OOB sshd instance + port-adaptive belt (H1 Parts 2-4 agent)
internal/felhomsshd: agent-managed felhom-sshd (claim port [8822,2222,8022,62222]
loud-fail-on-exhaustion; render config→sshd -t→reload never-restart-on-change
[SF-2]; operator authorized_keys from the hub block outside ~/.ssh [SF-3]); the
static-table nft belt mutating ONLY @operator_ips + @ssh_port [trap 4]; health/heal
(reset-failed-then-restart with 10min cooldown, NEVER restart onto an invalid
config) + the oob heartbeat stanza. configs/felhom-sshd.service (SAFE, no
RuntimeDirectory [SF-1]). FELHOM_SSHD + FELHOM_OOB sudoers (set-elements only).
oob.enabled config DEFAULT FALSE. Wired into main like wgtunnel.

Non-hollow tests: claim clean/contention/idempotent/exhaustion; config
safe+byte-stable+refuses-:22; belt mutate-then-idempotent + never-touches-rules;
heal no-restart-on-invalid-config + cooldown; status reflects block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 22:27:02 +02:00

66 lines
3.2 KiB
Go

// 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
}