113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
// Keywarden - Centralized SSH Key Management and Deployment
|
|
// Copyright (C) 2026 Patrick Asmus (scriptos)
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package security
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// trustedNets holds the parsed trusted proxy CIDR ranges.
|
|
// Set once at startup via Init().
|
|
var trustedNets []*net.IPNet
|
|
|
|
// Init parses the trusted proxy configuration and prepares the package
|
|
// for use. Must be called once at startup before any middleware runs.
|
|
//
|
|
// trustedProxies is a comma-separated list of CIDRs or IPs, e.g.
|
|
// "10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16" or "10.0.1.92".
|
|
// An empty string means no trusted proxies are configured (legacy mode:
|
|
// proxy headers are trusted unconditionally for backward compatibility).
|
|
func Init(trustedProxies string) {
|
|
trustedNets = nil
|
|
if trustedProxies == "" {
|
|
return
|
|
}
|
|
for _, entry := range strings.Split(trustedProxies, ",") {
|
|
cidr := strings.TrimSpace(entry)
|
|
if cidr == "" {
|
|
continue
|
|
}
|
|
// Plain IP → convert to single-host CIDR
|
|
if !strings.Contains(cidr, "/") {
|
|
if strings.Contains(cidr, ":") {
|
|
cidr += "/128"
|
|
} else {
|
|
cidr += "/32"
|
|
}
|
|
}
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
continue // skip invalid entries silently
|
|
}
|
|
trustedNets = append(trustedNets, ipNet)
|
|
}
|
|
}
|
|
|
|
// ClientIP extracts the real client IP address from the request.
|
|
//
|
|
// When trusted proxies are configured, X-Forwarded-For is walked from
|
|
// right to left and the first non-trusted IP is returned (secure approach).
|
|
// When no trusted proxies are configured, the legacy behavior is used
|
|
// (leftmost X-Forwarded-For entry, i.e. the value the first proxy saw).
|
|
func ClientIP(r *http.Request) string {
|
|
remoteIP := extractRemoteIP(r.RemoteAddr)
|
|
|
|
if len(trustedNets) > 0 {
|
|
// Strict mode: only honour proxy headers when the direct peer is trusted
|
|
if !isTrustedIP(remoteIP) {
|
|
return remoteIP
|
|
}
|
|
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
|
parts := strings.Split(fwd, ",")
|
|
// Walk right-to-left: rightmost untrusted IP is the real client
|
|
for i := len(parts) - 1; i >= 0; i-- {
|
|
ip := strings.TrimSpace(parts[i])
|
|
if ip != "" && !isTrustedIP(ip) {
|
|
return ip
|
|
}
|
|
}
|
|
}
|
|
if real := r.Header.Get("X-Real-Ip"); real != "" {
|
|
return real
|
|
}
|
|
return remoteIP
|
|
}
|
|
|
|
// Legacy mode (no trusted proxies configured): trust headers as before
|
|
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
|
parts := strings.SplitN(fwd, ",", 2)
|
|
return strings.TrimSpace(parts[0])
|
|
}
|
|
if real := r.Header.Get("X-Real-Ip"); real != "" {
|
|
return real
|
|
}
|
|
return remoteIP
|
|
}
|
|
|
|
// extractRemoteIP strips the port from r.RemoteAddr.
|
|
func extractRemoteIP(addr string) string {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err == nil {
|
|
return host
|
|
}
|
|
return addr
|
|
}
|
|
|
|
// isTrustedIP checks if an IP is within any of the configured trusted networks.
|
|
func isTrustedIP(ip string) bool {
|
|
parsed := net.ParseIP(ip)
|
|
if parsed == nil {
|
|
return false
|
|
}
|
|
for _, n := range trustedNets {
|
|
if n.Contains(parsed) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|