Release: v0.1.0-alpha
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user