c983a25609
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
66 lines
3.2 KiB
Go
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
|
|
}
|