package auth import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "os" "strconv" "time" "github.com/go-ldap/ldap/v3" ) // conn ist der Ausschnitt der LDAP-Verbindung, den der Authenticator braucht. // Das Interface existiert, damit Tests ohne echten Verzeichnisdienst laufen. type conn interface { Bind(username, password string) error Search(req *ldap.SearchRequest) (*ldap.SearchResult, error) Close() error } // ldapConn adaptiert *ldap.Conn an conn (Close hat dort keine Fehlerrückgabe). type ldapConn struct{ *ldap.Conn } func (c ldapConn) Close() error { c.Conn.Close(); return nil } // ParseTLSVersion übersetzt "1.0".."1.3" in die Konstante von crypto/tls. // Ein leerer oder unbekannter Wert ergibt TLS 1.2 — im Zweifel das Sichere. func ParseTLSVersion(s string) uint16 { switch s { case "1.0": return tls.VersionTLS10 case "1.1": return tls.VersionTLS11 case "1.3": return tls.VersionTLS13 default: return tls.VersionTLS12 } } // tlsConfigFor baut die TLS-Konfiguration. Die Zertifikatsprüfung ist immer // 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, maxVersion uint16) (*tls.Config, error) { if minVersion == 0 { minVersion = tls.VersionTLS12 } 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 // TLS 1.0/1.1 festgenagelt ist, bereits an der Cipher-Auswahl. cfg.CipherSuites = legacyCipherSuites() } if caFile == "" { return cfg, nil } pem, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("ad.ca_file %s: %w", caFile, err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pem) { return nil, fmt.Errorf("ad.ca_file %s enthält kein gültiges PEM-Zertifikat", caFile) } cfg.RootCAs = pool return cfg, nil } // legacyCipherSuites ergänzt Gos sichere Vorgaben um die CBC-SHA256-Suiten, // die Go als unsicher einstuft und deshalb nicht von sich aus anbietet, die // aber auf alten Schannel-Stacks vorkommen. // // Die Suiten mit reinem RSA-Schlüsselaustausch (TLS_RSA_*) sind bewusst NICHT // dabei: Go schaltet sie seit 1.22 unabhängig von dieser Liste ab. Braucht ein // Domain Controller sie zwingend, hilft nur GODEBUG=tlsrsakex=1 in der // systemd-Unit — siehe README. // // Wird nur benutzt, wenn der Betreiber die Mindestversion ausdrücklich unter // TLS 1.2 gesetzt hat. func legacyCipherSuites() []uint16 { out := make([]uint16, 0, len(tls.CipherSuites())+2) for _, cs := range tls.CipherSuites() { out = append(out, cs.ID) } return append(out, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, ) } // realDialer erzeugt die Dial-Funktion für den Produktivbetrieb. 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, maxVersion) if err != nil { return nil, err } addr := net.JoinHostPort(server, strconv.Itoa(port)) dialer := &net.Dialer{Timeout: timeout} var c *ldap.Conn if tlsMode == "starttls" { c, err = ldap.DialURL("ldap://"+addr, ldap.DialWithDialer(dialer)) if err != nil { return nil, fmt.Errorf("Verbindung zu %s: %w", addr, err) } if err := c.StartTLS(tlsCfg); err != nil { c.Close() return nil, fmt.Errorf("StartTLS zu %s: %w", addr, err) } } else { c, err = ldap.DialURL("ldaps://"+addr, ldap.DialWithDialer(dialer), ldap.DialWithTLSConfig(tlsCfg)) if err != nil { return nil, fmt.Errorf("LDAPS-Verbindung zu %s: %w", addr, err) } } c.SetTimeout(timeout) return ldapConn{c}, nil } }