Files
felhom-agent/internal/felhomsshd/config.go
T

68 lines
3.4 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"
// PrivApply is the root content checker that installs the config and felhom-op's key (R-861, agent v0.146.0).
PrivApply = "/usr/local/sbin/felhom-priv-apply"
// 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
}