fix(cluster): Joiner-IP sofort in @peer_ipv4 — Henne-Ei-Problem gelöst

IssueCert registriert den beitretenden Node jetzt direkt in ha_nodes
(public_ip = ClientIP, status='joining') und triggert den Firewall-Reload,
bevor die Response zurückgeht. Damit ist Port 8443 schon offen wenn der
Node im nächsten Schritt auto-register via mTLS versucht.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 17:25:38 +02:00
parent 6a5aff69d9
commit fd69081336
5 changed files with 66 additions and 6 deletions

View File

@@ -1 +1 @@
1.1.152
1.1.153

View File

@@ -60,7 +60,7 @@ import (
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
)
var version = "1.1.152"
var version = "1.1.153"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

@@ -11,7 +11,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
)
var version = "1.1.152"
var version = "1.1.153"
const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -41,7 +41,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
var version = "1.1.152"
var version = "1.1.153"
const (
// renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -2,7 +2,10 @@ package handlers
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"log/slog"
"time"
@@ -337,8 +340,8 @@ func (h *ClusterHandler) IssueCert(c *gin.Context) {
}
// consumedBy → Remote-IP. Audit-Trail wenn jemand Tokens stiehlt
// und vom falschen Host einlöst.
consumedBy := c.ClientIP()
if _, err := h.Tokens.Consume(c.Request.Context(), req.Token, consumedBy); err != nil {
clientIP := c.ClientIP()
if _, err := h.Tokens.Consume(c.Request.Context(), req.Token, clientIP); err != nil {
response.BadRequest(c, err)
return
}
@@ -353,12 +356,69 @@ func (h *ClusterHandler) IssueCert(c *gin.Context) {
response.Internal(c, err)
return
}
// Pre-register the joining node so its IP lands in @peer_ipv4
// immediately — otherwise port 8443 stays blocked and auto-register
// via mTLS can never succeed (chicken-and-egg).
if h.Store != nil && h.PeerReloader != nil {
go h.preRegisterJoiner(clientIP, req.CSR)
}
response.OK(c, issueCertResponse{
CACert: caPEM,
PeerCert: peerCert,
})
}
// preRegisterJoiner inserts a minimal ha_nodes row for the joining peer
// (using the CSR CN as FQDN and the HTTP client IP as public_ip), then
// triggers a firewall reload so @peer_ipv4 contains the new IP before
// the peer tries to call /agent/cluster/peers on port 8443.
func (h *ClusterHandler) preRegisterJoiner(clientIP, csrPEM string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
fqdn := cnFromCSR(csrPEM)
if fqdn == "" {
fqdn = "joining-" + clientIP
}
nodeID := fmt.Sprintf("pre-%x", time.Now().UnixNano())
n := models.HANode{
ID: nodeID,
Name: fqdn,
FQDN: fqdn,
APIURL: "https://" + fqdn + ":3443",
Role: "peer",
Status: "joining",
}
n.PublicIP = &clientIP
if _, err := h.Store.UpsertSelf(ctx, n); err != nil {
slog.Warn("cluster: pre-register joiner failed", "fqdn", fqdn, "ip", clientIP, "error", err)
return
}
if err := h.PeerReloader(ctx); err != nil {
slog.Warn("cluster: PeerReloader failed after pre-register", "error", err)
return
}
slog.Info("cluster: joiner pre-registered, firewall updated", "fqdn", fqdn, "ip", clientIP)
}
// cnFromCSR extracts the Subject Common Name from a PEM-encoded CSR.
// Returns empty string on any parse error.
func cnFromCSR(csrPEM string) string {
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
return ""
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return ""
}
return csr.Subject.CommonName
}
var errInvalidJoinRequest = simpleError("missing token or csr")
type simpleError string