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:
@@ -53,10 +53,19 @@ type Config struct {
|
||||
LogFormat string
|
||||
LogLevel string
|
||||
|
||||
// TLS termination
|
||||
// TLS_MODE: "none" (default) | "server" (TLS, no client cert) | "mtls" (mutual TLS)
|
||||
// When server or mtls: binary calls ListenAndServeTLS directly.
|
||||
// TLS_MODE and AUTH_MODE are independent knobs.
|
||||
TLSMode string
|
||||
TLSCert string // path to server certificate PEM
|
||||
TLSKey string // path to server private key PEM
|
||||
TLSCA string // path to CA certificate PEM (required for mtls)
|
||||
|
||||
// Auth mode: "builtin" (default) | "mtls" | "oidc"
|
||||
AuthMode string
|
||||
|
||||
// mTLS
|
||||
// mTLS (proxy mode)
|
||||
// Headers injected by TLS-terminating reverse proxy.
|
||||
MTLSHeaderDN string // default "X-SSL-Client-DN"
|
||||
MTLSHeaderVerify string // default "X-SSL-Client-Verify"
|
||||
@@ -141,6 +150,12 @@ func Load() *Config {
|
||||
LogFormat: getEnv("LOG_FORMAT", "text"),
|
||||
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||
|
||||
// TLS
|
||||
TLSMode: getEnv("TLS_MODE", "none"),
|
||||
TLSCert: getEnv("TLS_CERT", ""),
|
||||
TLSKey: getEnv("TLS_KEY", ""),
|
||||
TLSCA: getEnv("TLS_CA", ""),
|
||||
|
||||
AuthMode: getEnv("AUTH_MODE", "builtin"),
|
||||
|
||||
// mTLS
|
||||
|
||||
98
server/config/tls.go
Normal file
98
server/config/tls.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// TLS mode values for TLS_MODE env var.
|
||||
const (
|
||||
TLSModeNone = "none" // Plain HTTP (default). Use behind a proxy.
|
||||
TLSModeServer = "server" // Server-side TLS only. No client cert required.
|
||||
TLSModeMTLS = "mtls" // Mutual TLS. Client cert required and verified.
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTLSCertRequired = errors.New("TLS_CERT and TLS_KEY required when TLS_MODE is server or mtls")
|
||||
ErrTLSCARequired = errors.New("TLS_CA required when TLS_MODE is mtls")
|
||||
ErrTLSCALoad = errors.New("failed to load TLS CA certificate")
|
||||
ErrTLSModeInvalid = errors.New("TLS_MODE must be none, server, or mtls")
|
||||
)
|
||||
|
||||
// ValidateTLSMode returns an error if the TLS_MODE value is not recognized.
|
||||
func ValidateTLSMode(mode string) error {
|
||||
switch mode {
|
||||
case TLSModeNone, TLSModeServer, TLSModeMTLS, "":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%w: %q", ErrTLSModeInvalid, mode)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildTLSConfig constructs a *tls.Config from the application config.
|
||||
// Returns nil when TLS_MODE is "none" (or empty).
|
||||
// Returns an error if required cert/key/CA files are missing or unreadable.
|
||||
func BuildTLSConfig(cfg *Config) (*tls.Config, error) {
|
||||
mode := cfg.TLSMode
|
||||
if mode == "" || mode == TLSModeNone {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if cfg.TLSCert == "" || cfg.TLSKey == "" {
|
||||
return nil, ErrTLSCertRequired
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if mode == TLSModeMTLS {
|
||||
if cfg.TLSCA == "" {
|
||||
return nil, ErrTLSCARequired
|
||||
}
|
||||
caCert, err := os.ReadFile(cfg.TLSCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrTLSCALoad, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("%w: no valid certificates in %s", ErrTLSCALoad, cfg.TLSCA)
|
||||
}
|
||||
tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
tlsCfg.ClientCAs = pool
|
||||
}
|
||||
|
||||
return tlsCfg, nil
|
||||
}
|
||||
|
||||
// BuildPeerTLSConfig constructs a *tls.Config for outbound node-to-node
|
||||
// connections. The node presents its own cert and verifies peers against
|
||||
// the cluster CA. Returns nil when TLS_MODE is not "mtls".
|
||||
func BuildPeerTLSConfig(cfg *Config) (*tls.Config, error) {
|
||||
if cfg.TLSMode != TLSModeMTLS {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(cfg.TLSCert, cfg.TLSKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load node keypair: %w", err)
|
||||
}
|
||||
|
||||
caCert, err := os.ReadFile(cfg.TLSCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrTLSCALoad, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("%w: no valid certificates in %s", ErrTLSCALoad, cfg.TLSCA)
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: pool,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user