mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 10:26:57 +00:00
Implement WireGuard peer authentication and enhance internal request validation
This commit is contained in:
parent
4f1709e136
commit
b58e1d80ee
22
pkg/auth/internal_auth.go
Normal file
22
pkg/auth/internal_auth.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user