mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 07:39:07 +02:00
Merge branch 'main' into reverse-proxy-crowdsec-appsec
This commit is contained in:
@@ -1,36 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/auth"
|
||||
"github.com/netbirdio/netbird/proxy/internal/types"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/hash/argon2id"
|
||||
)
|
||||
|
||||
// ErrHeaderAuthFailed indicates that the header was present but the
|
||||
// credential did not validate. Callers should return 401 instead of
|
||||
// falling through to other auth schemes.
|
||||
var ErrHeaderAuthFailed = errors.New("header authentication failed")
|
||||
|
||||
// Header implements header-based authentication. The proxy checks for the
|
||||
// configured header in each request and validates its value via gRPC.
|
||||
// Header implements header-based authentication. The service mapping carries
|
||||
// the argon2id hash of every value accepted for the header, so the proxy
|
||||
// verifies the credential locally rather than round-tripping to management.
|
||||
type Header struct {
|
||||
id types.ServiceID
|
||||
accountId types.AccountID
|
||||
headerName string
|
||||
client authenticator
|
||||
hashes []string
|
||||
verified *verifiedValues
|
||||
}
|
||||
|
||||
// NewHeader creates a Header authentication scheme for the given header name.
|
||||
func NewHeader(client authenticator, id types.ServiceID, accountId types.AccountID, headerName string) Header {
|
||||
// NewHeader creates a Header authentication scheme accepting any value whose
|
||||
// argon2id hash appears in hashes. An empty hashes slice rejects every request
|
||||
// carrying the header, so a mapping that arrived without its hashes fails
|
||||
// closed instead of leaving the service unprotected.
|
||||
func NewHeader(headerName string, hashes []string) Header {
|
||||
return Header{
|
||||
id: id,
|
||||
accountId: accountId,
|
||||
headerName: headerName,
|
||||
client: client,
|
||||
headerName: http.CanonicalHeaderKey(headerName),
|
||||
hashes: hashes,
|
||||
verified: &verifiedValues{seen: make(map[[32]byte]struct{}, len(hashes))},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,31 +41,64 @@ func (h Header) HeaderName() string {
|
||||
return h.headerName
|
||||
}
|
||||
|
||||
// Authenticate checks for the configured header in the request. If absent,
|
||||
// returns empty (unauthenticated). If present, validates via gRPC.
|
||||
func (h Header) Authenticate(r *http.Request) (string, string, error) {
|
||||
// Authenticate satisfies Scheme. Header credentials are resolved by Verify
|
||||
// before the scheme loop runs, so a request that reaches here never carries
|
||||
// the header and there is no credential to prompt for.
|
||||
func (Header) Authenticate(*http.Request) (string, string, error) {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// Verify reports whether the request carries the configured header and, when
|
||||
// it does, whether the value matches one of the service's hashes.
|
||||
//
|
||||
// A non-nil unusable is a diagnostic rather than a request error: a stored hash
|
||||
// could not be decoded, so no credential can ever match it and the header stays
|
||||
// unauthenticatable until the service is saved again. Folding that into an
|
||||
// ordinary mismatch would hide the misconfiguration behind a permanent 401.
|
||||
func (h Header) Verify(r *http.Request) (present, matched bool, unusable error) {
|
||||
value := r.Header.Get(h.headerName)
|
||||
if value == "" {
|
||||
return "", "", nil
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
res, err := h.client.Authenticate(r.Context(), &proto.AuthenticateRequest{
|
||||
Id: string(h.id),
|
||||
AccountId: string(h.accountId),
|
||||
Request: &proto.AuthenticateRequest_HeaderAuth{
|
||||
HeaderAuth: &proto.HeaderAuthRequest{
|
||||
HeaderValue: value,
|
||||
HeaderName: h.headerName,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("authenticate header: %w", err)
|
||||
digest := sha256.Sum256([]byte(value))
|
||||
if h.verified.has(digest) {
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
if res.GetSuccess() {
|
||||
return res.GetSessionToken(), "", nil
|
||||
for _, hash := range h.hashes {
|
||||
err := argon2id.Verify(value, hash)
|
||||
if err == nil {
|
||||
h.verified.add(digest)
|
||||
return true, true, nil
|
||||
}
|
||||
if !errors.Is(err, argon2id.ErrMismatchedHashAndPassword) {
|
||||
unusable = err
|
||||
}
|
||||
}
|
||||
return true, false, unusable
|
||||
}
|
||||
|
||||
return "", "", ErrHeaderAuthFailed
|
||||
// verifiedValues remembers which header values already passed argon2id
|
||||
// verification. argon2id is deliberately expensive (19 MiB, two passes) and
|
||||
// header credentials repeat on every request, so re-deriving per request would
|
||||
// dominate the hot path. The set cannot outgrow the number of configured
|
||||
// hashes, and a mapping update builds a fresh scheme with an empty set.
|
||||
// Values are keyed by digest so the plaintext credential is not retained.
|
||||
type verifiedValues struct {
|
||||
mu sync.Mutex
|
||||
seen map[[32]byte]struct{}
|
||||
}
|
||||
|
||||
func (v *verifiedValues) has(digest [32]byte) bool {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
_, ok := v.seen[digest]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (v *verifiedValues) add(digest [32]byte) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
v.seen[digest] = struct{}{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user