hub: S1 wgsync (pinned-SSH push + declarative reconciler) + /admin/wg API + env wiring

internal/wgsync: x/crypto/ssh client with ssh.FixedHostKey pin (no insecure
fallback), forced-command exec, ok/applied response contract; Reconciler pushes
the FULL peer list on Trigger or 5-min tick (drift repair by construction).
internal/api/wg.go: PUT/GET /admin/wg/endpoint + POST/DELETE/GET /admin/wg/peers,
global-key-only, pubkey in body (base64 vs URL), sync ok|deferred|disabled.
main.go: WG_ENDPOINT_SSH_* env wiring, disabled-with-INFO when unconfigured.
Groups B/C/D tests incl. in-process SSH server; red-proofs b/c/d run + reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-03 23:40:22 +02:00
parent b18f6aee1b
commit fbeeacb124
8 changed files with 1190 additions and 0 deletions
+32
View File
@@ -20,6 +20,7 @@ import (
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
"gitea.dooplex.hu/admin/felhom-hub/internal/web"
"gitea.dooplex.hu/admin/felhom-hub/internal/wgsync"
"gopkg.in/yaml.v3"
)
@@ -316,6 +317,37 @@ func main() {
apiHandler.SetLatestVersionProvider(versionChecker)
}
// S1 offsite connectivity: the WG peer-sync reconciler (internal/wgsync). Config from env
// (mirror of the Resend/registry pattern): the SSH private key from the mounted Secret file,
// the (non-secret) pinned host key from plain env. Any piece missing → disabled with an INFO
// log; the /admin/wg mutations still work against the DB and report sync:"disabled".
{
wgAddr := os.Getenv("WG_ENDPOINT_SSH_ADDR")
wgUser := os.Getenv("WG_ENDPOINT_SSH_USER")
if wgUser == "" {
wgUser = "felhom-peersync"
}
wgKeyFile := os.Getenv("WG_ENDPOINT_SSH_KEY_FILE")
wgHostKey := os.Getenv("WG_ENDPOINT_SSH_HOSTKEY")
if wgAddr != "" && wgKeyFile != "" && wgHostKey != "" {
keyPEM, err := os.ReadFile(wgKeyFile)
if err != nil {
logger.Printf("[ERROR] WG peer-sync disabled: read key file %s: %v", wgKeyFile, err)
} else if wgClient, err := wgsync.New(wgsync.Config{
Addr: wgAddr, User: wgUser, PrivateKey: keyPEM, HostKeyLine: wgHostKey,
}, logger); err != nil {
logger.Printf("[ERROR] WG peer-sync disabled: %v", err)
} else {
wgReconciler := wgsync.NewReconciler(dataStore, wgClient, logger)
go wgReconciler.Run(ctx)
apiHandler.SetWGSyncer(wgReconciler)
logger.Printf("[INFO] WG peer-sync enabled (endpoint %s, user %s)", wgAddr, wgUser)
}
} else {
logger.Printf("[INFO] WG peer-sync disabled (endpoint not configured)")
}
}
// Session cleanup — removes expired sessions every hour
go webServer.CleanupSessions(ctx)