Feat v0.6.7 native mtls (#42)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #42.
This commit is contained in:
94
server/auth/mtls_proxy.go
Normal file
94
server/auth/mtls_proxy.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"switchboard-core/store"
|
||||
)
|
||||
|
||||
// MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider.
|
||||
type MTLSProxyConfig struct {
|
||||
HeaderDN string // header carrying cert DN (default "X-SSL-Client-DN")
|
||||
HeaderVerify string // header carrying verify status (default "X-SSL-Client-Verify")
|
||||
HeaderFingerprint string // header carrying cert fingerprint (default "X-SSL-Client-Fingerprint")
|
||||
AutoActivate bool // auto-activate new users (default true)
|
||||
DefaultTeam string // team ID for auto-provisioned users (optional)
|
||||
}
|
||||
|
||||
// MTLSProxyProvider authenticates via client certificate headers injected
|
||||
// by the TLS-terminating reverse proxy (nginx, Traefik, Istio).
|
||||
//
|
||||
// The backend never sees the actual TLS handshake — it trusts headers
|
||||
// injected by the proxy after cert validation. After reading and parsing
|
||||
// the DN, the provider resolves an existing user or auto-provisions a
|
||||
// new one, then returns a Result. The auth handler issues an internal JWT.
|
||||
//
|
||||
// Header flow:
|
||||
//
|
||||
// Client cert → nginx ssl_verify_client → injects X-SSL-Client-DN,
|
||||
// X-SSL-Client-Verify, X-SSL-Client-Fingerprint → backend reads headers.
|
||||
type MTLSProxyProvider struct {
|
||||
cfg MTLSProxyConfig
|
||||
}
|
||||
|
||||
func NewMTLSProxyProvider(cfg MTLSProxyConfig) *MTLSProxyProvider {
|
||||
if cfg.HeaderDN == "" {
|
||||
cfg.HeaderDN = "X-SSL-Client-DN"
|
||||
}
|
||||
if cfg.HeaderVerify == "" {
|
||||
cfg.HeaderVerify = "X-SSL-Client-Verify"
|
||||
}
|
||||
if cfg.HeaderFingerprint == "" {
|
||||
cfg.HeaderFingerprint = "X-SSL-Client-Fingerprint"
|
||||
}
|
||||
return &MTLSProxyProvider{cfg: cfg}
|
||||
}
|
||||
|
||||
func (p *MTLSProxyProvider) Mode() Mode { return ModeMTLS }
|
||||
|
||||
func (p *MTLSProxyProvider) SupportsRegistration() bool { return false }
|
||||
|
||||
func (p *MTLSProxyProvider) Register(_ *gin.Context, _ store.Stores) (*Result, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// Authenticate reads the cert DN and verify status from proxy headers,
|
||||
// parses the DN fields, and resolves or auto-provisions the user.
|
||||
//
|
||||
// Returns ErrInvalidCreds when headers are missing or verify fails.
|
||||
// Returns ErrInactive when user exists but is deactivated.
|
||||
func (p *MTLSProxyProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) {
|
||||
// ── Validate headers ───────────────────────────────────────────
|
||||
verify := c.GetHeader(p.cfg.HeaderVerify)
|
||||
if verify == "" {
|
||||
return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderVerify)
|
||||
}
|
||||
// nginx: "SUCCESS", Traefik: "0" (both mean valid cert)
|
||||
if verify != "SUCCESS" && verify != "0" {
|
||||
return nil, fmt.Errorf("%w: cert verify=%s", ErrInvalidCreds, verify)
|
||||
}
|
||||
|
||||
dn := c.GetHeader(p.cfg.HeaderDN)
|
||||
if dn == "" {
|
||||
return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderDN)
|
||||
}
|
||||
|
||||
fields := ParseDN(dn)
|
||||
cn := fields["CN"]
|
||||
if cn == "" {
|
||||
return nil, fmt.Errorf("%w: cert DN has no CN field", ErrInvalidCreds)
|
||||
}
|
||||
|
||||
// Stable external identity: fingerprint if available, else full DN
|
||||
fingerprint := c.GetHeader(p.cfg.HeaderFingerprint)
|
||||
if fingerprint == "" {
|
||||
fingerprint = dn
|
||||
}
|
||||
|
||||
return resolveOrProvision(
|
||||
c.Request.Context(), stores, cn, fields, fingerprint,
|
||||
p.cfg.AutoActivate, p.cfg.DefaultTeam,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user