feat(auth): ad.max_tls_version zum Deckeln der angebotenen TLS-Version

Alte Schannel-Stacks brechen an einem TLS-1.3-ClientHello kommentarlos ab:
sie nehmen die TCP-Verbindung an, lesen den ClientHello und setzen zurueck,
ohne ein Zertifikat zu schicken. Herunterhandeln hilft dann nicht — TLS 1.3
darf gar nicht erst angeboten werden.

max_tls_version (Default 1.3) deckelt die Hoechstversion; die Validierung
lehnt eine Hoechstversion unterhalb der Mindestversion ab.

check erkennt zusaetzlich zurueckgesetzte Verbindungen und nennt die beiden
plausiblen Ursachen: fehlendes LDAPS-Zertifikat auf dem Server oder
TLS-1.3-Inkompatibilitaet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 13:36:03 +02:00
parent fd88251e07
commit 11a3ec2e5f
10 changed files with 136 additions and 10 deletions

View file

@ -34,6 +34,8 @@ type Options struct {
// MinTLSVersion ist die niedrigste akzeptierte TLS-Version (Konstante aus
// crypto/tls). 0 bedeutet TLS 1.2.
MinTLSVersion uint16
// MaxTLSVersion deckelt die angebotene TLS-Version. 0 bedeutet TLS 1.3.
MaxTLSVersion uint16
Timeout time.Duration
// Dial ist injizierbar; nil = echte LDAP-Verbindung.
@ -76,7 +78,7 @@ func NewAD(opts Options) (*AD, error) {
opts.Port = 636
}
if opts.Dial == nil {
opts.Dial = realDialer(opts.Port, opts.TLSMode, opts.CAFile, opts.MinTLSVersion, opts.Timeout)
opts.Dial = realDialer(opts.Port, opts.TLSMode, opts.CAFile, opts.MinTLSVersion, opts.MaxTLSVersion, opts.Timeout)
}
a := &AD{opts: opts}
if opts.OnFailover != nil {

View file

@ -45,11 +45,17 @@ func ParseTLSVersion(s string) uint16 {
// aktiv — für LDAP gibt es bewusst keine Insecure-Option. Lediglich die
// Protokoll-Mindestversion ist konfigurierbar, damit Altsysteme wie ein
// Windows Server 2012 R2 ohne TLS 1.2 erreichbar bleiben.
func tlsConfigFor(server, caFile string, minVersion uint16) (*tls.Config, error) {
func tlsConfigFor(server, caFile string, minVersion, maxVersion uint16) (*tls.Config, error) {
if minVersion == 0 {
minVersion = tls.VersionTLS12
}
cfg := &tls.Config{ServerName: server, MinVersion: minVersion}
if maxVersion == 0 {
maxVersion = tls.VersionTLS13
}
if maxVersion < minVersion {
maxVersion = minVersion
}
cfg := &tls.Config{ServerName: server, MinVersion: minVersion, MaxVersion: maxVersion}
if minVersion < tls.VersionTLS12 {
// Go bietet die CBC-Suiten alter Schannel-Stacks nicht mehr von sich
// aus an. Ohne sie scheitert der Handshake mit einem 2012 R2, der auf
@ -94,9 +100,9 @@ func legacyCipherSuites() []uint16 {
}
// realDialer erzeugt die Dial-Funktion für den Produktivbetrieb.
func realDialer(port int, tlsMode, caFile string, minVersion uint16, timeout time.Duration) func(context.Context, string) (conn, error) {
func realDialer(port int, tlsMode, caFile string, minVersion, maxVersion uint16, timeout time.Duration) func(context.Context, string) (conn, error) {
return func(ctx context.Context, server string) (conn, error) {
tlsCfg, err := tlsConfigFor(server, caFile, minVersion)
tlsCfg, err := tlsConfigFor(server, caFile, minVersion, maxVersion)
if err != nil {
return nil, err
}

View file

@ -26,7 +26,7 @@ func TestParseTLSVersion(t *testing.T) {
}
func TestTLSConfigDefaultsToTLS12(t *testing.T) {
cfg, err := tlsConfigFor("dc01.firma.local", "", 0)
cfg, err := tlsConfigFor("dc01.firma.local", "", 0, 0)
if err != nil {
t.Fatal(err)
}
@ -42,10 +42,40 @@ func TestTLSConfigDefaultsToTLS12(t *testing.T) {
if cfg.CipherSuites != nil {
t.Error("bei TLS 1.2+ sollen Gos sichere Vorgaben gelten, keine eigene Liste")
}
if cfg.MaxVersion != tls.VersionTLS13 {
t.Errorf("MaxVersion = %#04x, want TLS 1.3", cfg.MaxVersion)
}
}
func TestTLSConfigCapsMaxVersion(t *testing.T) {
// Alte Schannel-Stacks brechen an einem TLS-1.3-ClientHello kommentarlos ab.
// Dann darf 1.3 gar nicht erst angeboten werden.
cfg, err := tlsConfigFor("dc01.firma.local", "", tls.VersionTLS12, tls.VersionTLS12)
if err != nil {
t.Fatal(err)
}
if cfg.MaxVersion != tls.VersionTLS12 {
t.Errorf("MaxVersion = %#04x, want TLS 1.2", cfg.MaxVersion)
}
if cfg.MinVersion != tls.VersionTLS12 {
t.Errorf("MinVersion = %#04x, want TLS 1.2", cfg.MinVersion)
}
}
func TestTLSConfigMaxNeverBelowMin(t *testing.T) {
// Eine widerspruechliche Kombination darf keine unmoegliche Konfiguration
// erzeugen; die Validierung faengt sie ohnehin vorher ab.
cfg, err := tlsConfigFor("dc01", "", tls.VersionTLS12, tls.VersionTLS10)
if err != nil {
t.Fatal(err)
}
if cfg.MaxVersion < cfg.MinVersion {
t.Errorf("MaxVersion %#04x < MinVersion %#04x", cfg.MaxVersion, cfg.MinVersion)
}
}
func TestTLSConfigLegacyAddsCBCSuites(t *testing.T) {
cfg, err := tlsConfigFor("dc01.firma.local", "", tls.VersionTLS10)
cfg, err := tlsConfigFor("dc01.firma.local", "", tls.VersionTLS10, 0)
if err != nil {
t.Fatal(err)
}
@ -92,7 +122,7 @@ func TestTLSConfigLegacyAddsCBCSuites(t *testing.T) {
}
func TestTLSConfigRejectsBadCAFile(t *testing.T) {
if _, err := tlsConfigFor("dc01", "/gibt/es/nicht.pem", tls.VersionTLS12); err == nil {
if _, err := tlsConfigFor("dc01", "/gibt/es/nicht.pem", tls.VersionTLS12, 0); err == nil {
t.Fatal("fehlende CA-Datei muss abgelehnt werden")
}
}