Implement WireGuard peer authentication and enhance internal request validation

This commit is contained in:
anonpenguin23 2026-02-19 06:43:06 +02:00
parent 4f1709e136
commit b58e1d80ee
7 changed files with 88 additions and 42 deletions

22
pkg/auth/internal_auth.go Normal file
View File

@ -0,0 +1,22 @@
package auth
import "net"
// WireGuardSubnet is the internal WireGuard mesh CIDR.
const WireGuardSubnet = "10.0.0.0/24"
// IsWireGuardPeer checks whether remoteAddr (host:port format) originates
// from the WireGuard mesh subnet. This provides cryptographic peer
// authentication since WireGuard validates keys at the tunnel layer.
func IsWireGuardPeer(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
_, wgNet, _ := net.ParseCIDR(WireGuardSubnet)
return wgNet.Contains(ip)
}

View File

@ -18,6 +18,7 @@ import (
"sync" "sync"
"time" "time"
nodeauth "github.com/DeBrosOfficial/network/pkg/auth"
"github.com/DeBrosOfficial/network/pkg/client" "github.com/DeBrosOfficial/network/pkg/client"
"github.com/DeBrosOfficial/network/pkg/deployments" "github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/deployments/health" "github.com/DeBrosOfficial/network/pkg/deployments/health"
@ -806,8 +807,8 @@ func (g *Gateway) namespaceClusterRepairHandler(w http.ResponseWriter, r *http.R
return return
} }
// Internal auth check // Internal auth check: header + WireGuard subnet verification
if r.Header.Get("X-Orama-Internal-Auth") != "namespace-coordination" { if r.Header.Get("X-Orama-Internal-Auth") != "namespace-coordination" || !nodeauth.IsWireGuardPeer(r.RemoteAddr) {
writeError(w, http.StatusUnauthorized, "unauthorized") writeError(w, http.StatusUnauthorized, "unauthorized")
return return
} }

View File

@ -217,14 +217,14 @@ func (h *DomainHandler) HandleVerifyDomain(w http.ResponseWriter, r *http.Reques
return return
} }
// Update status // Update status (scoped to deployment_id for defense-in-depth)
updateQuery := ` updateQuery := `
UPDATE deployment_domains UPDATE deployment_domains
SET verification_status = 'verified', verified_at = ? SET verification_status = 'verified', verified_at = ?
WHERE domain = ? WHERE domain = ? AND deployment_id = ?
` `
_, err = h.service.db.Exec(ctx, updateQuery, time.Now(), domain) _, err = h.service.db.Exec(ctx, updateQuery, time.Now(), domain, domainRecord.DeploymentID)
if err != nil { if err != nil {
h.logger.Error("Failed to update verification status", zap.Error(err)) h.logger.Error("Failed to update verification status", zap.Error(err))
http.Error(w, "Failed to update verification status", http.StatusInternalServerError) http.Error(w, "Failed to update verification status", http.StatusInternalServerError)
@ -358,9 +358,9 @@ func (h *DomainHandler) HandleRemoveDomain(w http.ResponseWriter, r *http.Reques
} }
deploymentID = rows[0].DeploymentID deploymentID = rows[0].DeploymentID
// Delete domain // Delete domain (scoped to deployment_id for defense-in-depth)
deleteQuery := `DELETE FROM deployment_domains WHERE domain = ?` deleteQuery := `DELETE FROM deployment_domains WHERE domain = ? AND deployment_id = ?`
_, err = h.service.db.Exec(ctx, deleteQuery, domain) _, err = h.service.db.Exec(ctx, deleteQuery, domain, deploymentID)
if err != nil { if err != nil {
h.logger.Error("Failed to delete domain", zap.Error(err)) h.logger.Error("Failed to delete domain", zap.Error(err))
http.Error(w, "Failed to delete domain", http.StatusInternalServerError) http.Error(w, "Failed to delete domain", http.StatusInternalServerError)

View File

@ -11,6 +11,7 @@ import (
"os/exec" "os/exec"
"github.com/DeBrosOfficial/network/pkg/auth"
"github.com/DeBrosOfficial/network/pkg/deployments" "github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/deployments/process" "github.com/DeBrosOfficial/network/pkg/deployments/process"
"github.com/DeBrosOfficial/network/pkg/ipfs" "github.com/DeBrosOfficial/network/pkg/ipfs"
@ -422,6 +423,11 @@ func (h *ReplicaHandler) extractFromIPFS(ctx context.Context, cid, destPath stri
} }
// isInternalRequest checks if the request is an internal node-to-node call. // isInternalRequest checks if the request is an internal node-to-node call.
// Requires both the static auth header AND that the request originates from
// the WireGuard mesh subnet (cryptographic peer authentication).
func (h *ReplicaHandler) isInternalRequest(r *http.Request) bool { func (h *ReplicaHandler) isInternalRequest(r *http.Request) bool {
return r.Header.Get("X-Orama-Internal-Auth") == "replica-coordination" if r.Header.Get("X-Orama-Internal-Auth") != "replica-coordination" {
return false
}
return auth.IsWireGuardPeer(r.RemoteAddr)
} }

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/DeBrosOfficial/network/pkg/auth"
"github.com/DeBrosOfficial/network/pkg/gateway" "github.com/DeBrosOfficial/network/pkg/gateway"
namespacepkg "github.com/DeBrosOfficial/network/pkg/namespace" namespacepkg "github.com/DeBrosOfficial/network/pkg/namespace"
"github.com/DeBrosOfficial/network/pkg/olric" "github.com/DeBrosOfficial/network/pkg/olric"
@ -80,8 +81,8 @@ func (h *SpawnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// Authenticate via internal auth header // Authenticate via internal auth header + WireGuard subnet check
if r.Header.Get("X-Orama-Internal-Auth") != "namespace-coordination" { if r.Header.Get("X-Orama-Internal-Auth") != "namespace-coordination" || !auth.IsWireGuardPeer(r.RemoteAddr) {
http.Error(w, "unauthorized", http.StatusUnauthorized) http.Error(w, "unauthorized", http.StatusUnauthorized)
return return
} }

View File

@ -940,33 +940,57 @@ func (g *Gateway) handleNamespaceGatewayRequest(w http.ResponseWriter, r *http.R
return targets[i].ip < targets[j].ip return targets[i].ip < targets[j].ip
}) })
// Prefer local gateway if this node is part of the namespace cluster. // Build ordered target list: local gateway first, then hash-selected, then remaining.
// This avoids a WireGuard network hop and eliminates single-point-of-failure // This ordering is used by the circuit breaker fallback loop below.
// when a remote gateway node is down. orderedTargets := make([]namespaceGatewayTarget, 0, len(targets))
var selected namespaceGatewayTarget localIdx := -1
if g.localWireGuardIP != "" { if g.localWireGuardIP != "" {
for _, t := range targets { for i, t := range targets {
if t.ip == g.localWireGuardIP { if t.ip == g.localWireGuardIP {
selected = t orderedTargets = append(orderedTargets, t)
localIdx = i
break break
} }
} }
} }
// Fall back to consistent hashing for nodes not in the namespace cluster // Consistent hashing for affinity (keeps WS subscribe/publish on same node)
if selected.ip == "" { affinityKey := namespaceName + "|" + validatedNamespace
affinityKey := namespaceName + "|" + validatedNamespace if apiKey := extractAPIKey(r); apiKey != "" {
if apiKey := extractAPIKey(r); apiKey != "" { affinityKey = namespaceName + "|" + apiKey
affinityKey = namespaceName + "|" + apiKey } else if authz := strings.TrimSpace(r.Header.Get("Authorization")); authz != "" {
} else if authz := strings.TrimSpace(r.Header.Get("Authorization")); authz != "" { affinityKey = namespaceName + "|" + authz
affinityKey = namespaceName + "|" + authz } else {
} else { affinityKey = namespaceName + "|" + getClientIP(r)
affinityKey = namespaceName + "|" + getClientIP(r) }
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(affinityKey))
hashIdx := int(hasher.Sum32()) % len(targets)
if hashIdx != localIdx {
orderedTargets = append(orderedTargets, targets[hashIdx])
}
for i, t := range targets {
if i != localIdx && i != hashIdx {
orderedTargets = append(orderedTargets, t)
} }
hasher := fnv.New32a() }
_, _ = hasher.Write([]byte(affinityKey))
targetIdx := int(hasher.Sum32()) % len(targets) // Select the first target whose circuit breaker allows a request through.
selected = targets[targetIdx] // This provides automatic failover when a namespace gateway node is down.
var selected namespaceGatewayTarget
var cb *CircuitBreaker
for _, candidate := range orderedTargets {
cbKey := "ns:" + candidate.ip
candidateCB := g.circuitBreakers.Get(cbKey)
if candidateCB.Allow() {
selected = candidate
cb = candidateCB
break
}
}
if selected.ip == "" {
http.Error(w, "Namespace gateway unavailable (all circuits open)", http.StatusServiceUnavailable)
return
} }
gatewayIP := selected.ip gatewayIP := selected.ip
gatewayPort := selected.port gatewayPort := selected.port
@ -1027,14 +1051,6 @@ func (g *Gateway) handleNamespaceGatewayRequest(w http.ResponseWriter, r *http.R
proxyReq.Header.Set(HeaderInternalAuthNamespace, validatedNamespace) proxyReq.Header.Set(HeaderInternalAuthNamespace, validatedNamespace)
} }
// Circuit breaker: check if target is healthy before sending request
cbKey := "ns:" + gatewayIP
cb := g.circuitBreakers.Get(cbKey)
if !cb.Allow() {
http.Error(w, "Namespace gateway unavailable (circuit open)", http.StatusServiceUnavailable)
return
}
// Execute proxy request using shared transport for connection pooling // Execute proxy request using shared transport for connection pooling
httpClient := &http.Client{Timeout: 30 * time.Second, Transport: g.proxyTransport} httpClient := &http.Client{Timeout: 30 * time.Second, Transport: g.proxyTransport}
resp, err := httpClient.Do(proxyReq) resp, err := httpClient.Do(proxyReq)

View File

@ -3,9 +3,9 @@ package gateway
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@ -89,7 +89,7 @@ func (g *Gateway) probeLocalNamespaces(ctx context.Context) {
} }
query := ` query := `
SELECT nc.namespace_name, npa.rqlite_http_port, npa.olric_memberlist_port, npa.gateway_http_port SELECT nc.namespace_name, npa.rqlite_http_port, npa.olric_http_port, npa.gateway_http_port
FROM namespace_port_allocations npa FROM namespace_port_allocations npa
JOIN namespace_clusters nc ON npa.namespace_cluster_id = nc.id JOIN namespace_clusters nc ON npa.namespace_cluster_id = nc.id
WHERE npa.node_id = ? AND nc.status = 'ready' WHERE npa.node_id = ? AND nc.status = 'ready'
@ -117,7 +117,7 @@ func (g *Gateway) probeLocalNamespaces(ctx context.Context) {
// Probe RQLite (HTTP on localhost) // Probe RQLite (HTTP on localhost)
nsHealth.Services["rqlite"] = probeTCP("127.0.0.1", rqlitePort) nsHealth.Services["rqlite"] = probeTCP("127.0.0.1", rqlitePort)
// Probe Olric memberlist (binds to WireGuard IP) // Probe Olric HTTP API (binds to WireGuard IP)
olricHost := g.localWireGuardIP olricHost := g.localWireGuardIP
if olricHost == "" { if olricHost == "" {
olricHost = "127.0.0.1" olricHost = "127.0.0.1"
@ -238,7 +238,7 @@ func (g *Gateway) isRQLiteLeader(ctx context.Context) bool {
// probeTCP checks if a port is listening by attempting a TCP connection. // probeTCP checks if a port is listening by attempting a TCP connection.
func probeTCP(host string, port int) NamespaceServiceHealth { func probeTCP(host string, port int) NamespaceServiceHealth {
start := time.Now() start := time.Now()
addr := fmt.Sprintf("%s:%d", host, port) addr := net.JoinHostPort(host, strconv.Itoa(port))
conn, err := net.DialTimeout("tcp", addr, 2*time.Second) conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
latency := time.Since(start) latency := time.Since(start)