feat(cluster): Fix "joining" status + cross-node auth federation
1. preRegisterJoiner now runs SYNCHRONOUSLY before IssueCert responds,
so nftables @peer_ipv4 is updated before the joiner calls autoRegister.
Previously it was a goroutine → race → autoRegister failed → "joining"
forever.
2. Stable node ID for pre-registered placeholder (prenode-{fqdn}) instead
of time-based ID — re-joins are now idempotent.
3. AgentRegisterPeer sets status="online" immediately (peer proved it is
online by connecting via mTLS) and deletes the prenode-{fqdn} placeholder.
4. autoRegister retries 3× with 2s delay in case of transient nftables lag.
5. Auth federation: cluster nodes forward failed logins to the primary via
mTLS /agent/auth/check so users can log in on any node with primary
credentials (no PG replication needed).
- SystemHandler.AgentAuthCheck: new endpoint on :8443
- AuthHandler.checkWithPrimary: mTLS call to primary when local auth fails
- AuthHandler.WithClusterTLS: inject cluster TLS store
- startAgentListener now uses the wired systemHdl with Users repo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bufio"
|
||||
stdcontext "context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
aptsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/apt"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||
)
|
||||
|
||||
// SystemHandler covers /system/health, /system/package-versions,
|
||||
@@ -44,6 +46,9 @@ type SystemHandler struct {
|
||||
// ExtraReloaders: additional service renderers triggered by
|
||||
// RenderConfigs. Keyed by service name (nftables, wireguard, etc.).
|
||||
ExtraReloaders map[string]func(stdcontext.Context) error
|
||||
// Users: optional — wired after DB pool opens. Used by AgentAuthCheck
|
||||
// so cluster peers can verify credentials against this node's DB.
|
||||
Users *usersvc.Repo
|
||||
}
|
||||
|
||||
func NewSystemHandler(version string) *SystemHandler {
|
||||
@@ -87,6 +92,12 @@ func (h *SystemHandler) WithAllReloaders(extras map[string]func(stdcontext.Conte
|
||||
return h
|
||||
}
|
||||
|
||||
// WithUsers injectet das Users-Repo für AgentAuthCheck.
|
||||
func (h *SystemHandler) WithUsers(u *usersvc.Repo) *SystemHandler {
|
||||
h.Users = u
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *SystemHandler) Register(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/system")
|
||||
g.GET("/health", h.Health)
|
||||
@@ -124,8 +135,50 @@ func (h *SystemHandler) RegisterAgent(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/agent/system")
|
||||
g.GET("/health", h.Health)
|
||||
g.GET("/resources", h.Resources)
|
||||
// Auth-Federation: Cluster-Nodes verifizieren Credentials gegen diesen
|
||||
// Node via mTLS. Nur über den Agent-Listener (:8443) erreichbar.
|
||||
rg.POST("/agent/auth/check", h.AgentAuthCheck)
|
||||
}
|
||||
|
||||
// AgentAuthCheck verifies email+password against the local users table
|
||||
// and setup.json. Called by cluster nodes over mTLS when local auth fails
|
||||
// so users can log in on any cluster node with the primary's credentials.
|
||||
func (h *SystemHandler) AgentAuthCheck(c *gin.Context) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
if email == "" || req.Password == "" {
|
||||
response.Unauthorized(c, errInvalidCreds)
|
||||
return
|
||||
}
|
||||
|
||||
// Check local users table first.
|
||||
if h.Users != nil {
|
||||
u, hash, err := h.Users.FindByEmail(c.Request.Context(), email)
|
||||
if err == nil && u.Active && usersvc.VerifyPassword(hash, req.Password) {
|
||||
response.OK(c, gin.H{"actor": u.Email, "role": u.Role})
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fallback: setup.json admin.
|
||||
if h.Setup != nil {
|
||||
st, _ := h.Setup.Load()
|
||||
if st != nil && strings.EqualFold(st.AdminEmail, email) && st.VerifyAdminPassword(req.Password) {
|
||||
response.OK(c, gin.H{"actor": st.AdminEmail, "role": "admin"})
|
||||
return
|
||||
}
|
||||
}
|
||||
response.Unauthorized(c, errInvalidCreds)
|
||||
}
|
||||
|
||||
var errInvalidCreds = errors.New("invalid_credentials")
|
||||
|
||||
// servicesToCheck is the curated list shown on the dashboard
|
||||
// service-health-grid. Order matters (UI renders in this sequence).
|
||||
// Each entry is a (label, systemd-unit) pair — label is what the
|
||||
|
||||
Reference in New Issue
Block a user