This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/auth/mtls_native.go
Jeffrey Smith 1b095d50be
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Successful in 2m44s
CI/CD / test-sqlite (pull_request) Successful in 2m50s
CI/CD / build-and-deploy (pull_request) Successful in 1m4s
Feat v0.6.7 native mTLS (#42)
End-to-end mutual TLS without a reverse proxy. Go binary terminates
TLS itself via ListenAndServeTLS. TLS_MODE config (none/server/mtls)
is independent of AUTH_MODE.

- MTLSNativeProvider reads PeerCertificates directly (no header trust)
- Shared helpers extracted to mtls_helpers.go (ParseDN, FingerprintCert)
- MTLSProvider renamed to MTLSProxyProvider for clarity
- BuildPeerTLSConfig for future node-to-node mTLS
- switchboard-ca.sh: CA init, issue-node, issue-user (ECDSA P-256)
- 12 new tests (unit + TLS integration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:18:24 +00:00

73 lines
2.3 KiB
Go

package auth
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"switchboard-core/store"
)
// ErrNoCert is returned when no client certificate is presented on a
// connection that requires mTLS authentication.
var ErrNoCert = errors.New("no client certificate presented")
// MTLSNativeConfig holds configuration for the native (non-proxy) mTLS provider.
type MTLSNativeConfig struct {
AutoActivate bool // auto-activate new users (default true)
DefaultTeam string // team ID for auto-provisioned users (optional)
}
// MTLSNativeProvider authenticates by reading the peer certificate
// directly from the TLS connection state. Unlike MTLSProxyProvider,
// it does not trust headers — identity is cryptographically verified
// by the Go TLS stack before the HTTP layer runs.
//
// Requires TLS_MODE=mtls so the binary terminates TLS itself.
type MTLSNativeProvider struct {
cfg MTLSNativeConfig
}
// NewMTLSNativeProvider creates a native mTLS auth provider.
func NewMTLSNativeProvider(cfg MTLSNativeConfig) *MTLSNativeProvider {
return &MTLSNativeProvider{cfg: cfg}
}
func (p *MTLSNativeProvider) Mode() Mode { return ModeMTLS }
func (p *MTLSNativeProvider) SupportsRegistration() bool { return false }
func (p *MTLSNativeProvider) Register(_ *gin.Context, _ store.Stores) (*Result, error) {
return nil, ErrNotSupported
}
// Authenticate reads the verified peer certificate from the TLS connection
// state. The CN becomes the username, and sha256(cert.Raw) is the stable
// external_id. Returns ErrNoCert when no TLS or no peer certificates.
func (p *MTLSNativeProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) {
if c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 {
return nil, fmt.Errorf("%w", ErrNoCert)
}
peer := c.Request.TLS.PeerCertificates[0]
cn := peer.Subject.CommonName
if cn == "" {
return nil, fmt.Errorf("%w: certificate has no CommonName", ErrInvalidCreds)
}
// Build DN fields from the certificate subject for resolveOrProvision
dnFields := map[string]string{"CN": cn}
if len(peer.EmailAddresses) > 0 {
dnFields["emailAddress"] = peer.EmailAddresses[0]
}
fingerprint := FingerprintCert(peer)
return resolveOrProvision(
c.Request.Context(), stores, cn, dnFields, fingerprint,
p.cfg.AutoActivate, p.cfg.DefaultTeam,
)
}