Add reverse proxy header security and forwarding

- Rewrite Host header to backend target (configurable via pass_host_header per mapping)
- Strip and set X-Forwarded-For/X-Real-IP from direct connection (trust boundary)
- Set X-Forwarded-Host and X-Forwarded-Proto headers
- Strip nb_session cookie and session_token query param before forwarding
- Add --forwarded-proto flag (auto/http/https) for proto detection
- Fix OIDC redirect hardcoded https scheme
- Add pass_host_header to proto, API, and management model
This commit is contained in:
Viktor Liu
2026-02-08 14:16:52 +08:00
parent 0a3a9f977d
commit 07e59b2708
13 changed files with 700 additions and 228 deletions

View File

@@ -86,11 +86,9 @@ func (m *managerImpl) CreateReverseProxy(ctx context.Context, accountID, userID
}
}
authConfig := reverseProxy.Auth
reverseProxy = reverseproxy.NewReverseProxy(accountID, reverseProxy.Name, reverseProxy.Domain, proxyCluster, reverseProxy.Targets, reverseProxy.Enabled)
reverseProxy.Auth = authConfig
reverseProxy.AccountID = accountID
reverseProxy.ProxyCluster = proxyCluster
reverseProxy.InitNewRecord()
// Generate session JWT signing keys
keyPair, err := sessionkey.GenerateKeyPair()

View File

@@ -7,10 +7,11 @@ import (
"strconv"
"time"
"github.com/netbirdio/netbird/util/crypt"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util/crypt"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/proto"
)
@@ -89,6 +90,7 @@ type ReverseProxy struct {
ProxyCluster string `gorm:"index"`
Targets []Target `gorm:"serializer:json"`
Enabled bool
PassHostHeader bool
Auth AuthConfig `gorm:"serializer:json"`
Meta ReverseProxyMeta `gorm:"embedded;embeddedPrefix:meta_"`
SessionPrivateKey string `gorm:"column:session_private_key"`
@@ -96,18 +98,26 @@ type ReverseProxy struct {
}
func NewReverseProxy(accountID, name, domain, proxyCluster string, targets []Target, enabled bool) *ReverseProxy {
return &ReverseProxy{
ID: xid.New().String(),
rp := &ReverseProxy{
AccountID: accountID,
Name: name,
Domain: domain,
ProxyCluster: proxyCluster,
Targets: targets,
Enabled: enabled,
Meta: ReverseProxyMeta{
CreatedAt: time.Now(),
Status: string(StatusPending),
},
}
rp.InitNewRecord()
return rp
}
// InitNewRecord generates a new unique ID and resets metadata for a newly created
// ReverseProxy record. This overwrites any existing ID and Meta fields and should
// only be called during initial creation, not for updates.
func (r *ReverseProxy) InitNewRecord() {
r.ID = xid.New().String()
r.Meta = ReverseProxyMeta{
CreatedAt: time.Now(),
Status: string(StatusPending),
}
}
@@ -159,13 +169,14 @@ func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy {
}
resp := &api.ReverseProxy{
Id: r.ID,
Name: r.Name,
Domain: r.Domain,
Targets: apiTargets,
Enabled: r.Enabled,
Auth: authConfig,
Meta: meta,
Id: r.ID,
Name: r.Name,
Domain: r.Domain,
Targets: apiTargets,
Enabled: r.Enabled,
PassHostHeader: &r.PassHostHeader,
Auth: authConfig,
Meta: meta,
}
if r.ProxyCluster != "" {
@@ -220,13 +231,14 @@ func (r *ReverseProxy) ToProtoMapping(operation Operation, authToken string, oid
}
return &proto.ProxyMapping{
Type: operationToProtoType(operation),
Id: r.ID,
Domain: r.Domain,
Path: pathMappings,
AuthToken: authToken,
Auth: auth,
AccountId: r.AccountID,
Type: operationToProtoType(operation),
Id: r.ID,
Domain: r.Domain,
Path: pathMappings,
AuthToken: authToken,
Auth: auth,
AccountId: r.AccountID,
PassHostHeader: r.PassHostHeader,
}
}
@@ -265,6 +277,10 @@ func (r *ReverseProxy) FromAPIRequest(req *api.ReverseProxyRequest, accountID st
r.Enabled = req.Enabled
if req.PassHostHeader != nil {
r.PassHostHeader = *req.PassHostHeader
}
if req.Auth.PasswordAuth != nil {
r.Auth.PasswordAuth = &PasswordAuthConfig{
Enabled: req.Auth.PasswordAuth.Enabled,