feat(check): handlungsleitende Hinweise bei TLS- und Zertifikatsfehlern

Der bisherige Fehlertext gab den rohen x509-Fehler weiter, ohne zu sagen, was
zu tun ist. check unterscheidet jetzt unbekannte CA, unpassenden Hostnamen,
SHA-1-Signatur, abgelaufenes Zertifikat, zu alte TLS-Version und fehlende
gemeinsame Cipher-Suite und nennt jeweils die passende Stellschraube.

Der technische Originalfehler bleibt erhalten. Reine Netzwerkfehler bekommen
bewusst keinen Hinweis, damit kein falscher Verdacht entsteht.

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:29:47 +02:00
parent 7237aee744
commit fd88251e07
2 changed files with 116 additions and 0 deletions

View file

@ -226,6 +226,51 @@ func (c *Checker) checkFirewall(ctx context.Context) (time.Time, error) {
}
}
// directoryHint übersetzt typische TLS- und Zertifikatsfehler in einen
// handlungsleitenden Zusatz. Der technische Originalfehler bleibt erhalten;
// nur ein Hinweis kommt dazu. Passt nichts, bleibt der Zusatz leer — ein
// reiner Netzwerkfehler soll keinen falschen Verdacht wecken.
func directoryHint(err error) string {
msg := strings.ToLower(err.Error())
contains := func(needles ...string) bool {
for _, n := range needles {
if strings.Contains(msg, n) {
return true
}
}
return false
}
switch {
case contains("insecure algorithm sha1", "sha1-rsa"):
return "Das Zertifikat des Domain Controllers ist mit SHA-1 signiert. " +
"Go akzeptiert das seit Version 1.24 nicht mehr und bietet auch keinen " +
"Schalter dafür — das Zertifikat muss mit SHA-256 neu ausgestellt werden."
case contains("certificate signed by unknown authority", "unknown authority", "unable to verify"):
return "Die ausstellende CA ist auf diesem Host nicht bekannt. Die interne " +
"Stammzertifizierungsstelle als PEM ablegen und in ad.ca_file eintragen " +
"(die Datei muss die gesamte Kette bis zur Wurzel enthalten). " +
"Ohne ad.ca_file wird nur der Trust-Store des Betriebssystems benutzt."
case contains("certificate is valid for", "certificate is not valid for any names", "x509: certificate relies on legacy"):
return "Das Zertifikat passt nicht zum angefragten Namen. In ad.servers muss " +
"genau der Name stehen, der im Zertifikat als Subject Alternative Name " +
"eingetragen ist — IP-Adressen sind deshalb nicht zulässig."
case contains("certificate has expired", "not yet valid"):
return "Das Zertifikat des Domain Controllers ist abgelaufen oder noch nicht " +
"gültig. Zertifikat erneuern und die Systemzeit beider Seiten prüfen."
case contains("protocol version not supported", "unsupported protocol version"):
return "Der Domain Controller spricht kein TLS 1.2. Für Altsysteme wie " +
"Windows Server 2012 R2 lässt sich ad.min_tls_version auf \"1.0\" setzen; " +
"besser ist, TLS 1.2 auf dem Domain Controller zu aktivieren."
case contains("no cipher suite supported by both", "handshake failure"):
return "Client und Domain Controller haben keine gemeinsame Cipher-Suite. " +
"Zuerst ad.min_tls_version prüfen. Braucht der Domain Controller zwingend " +
"RSA-Schlüsselaustausch, hilft nur GODEBUG=tlsrsakex=1 in der systemd-Unit."
default:
return ""
}
}
// checkDirectory bindet den Service-Account und löst die Gruppe auf.
func (c *Checker) checkDirectory(ctx context.Context) (string, error) {
if c.Dir == nil {
@ -233,6 +278,9 @@ func (c *Checker) checkDirectory(ctx context.Context) (string, error) {
}
dn, err := c.Dir.ResolveGroupDN(ctx)
if err != nil {
if hint := directoryHint(err); hint != "" {
return "", fmt.Errorf("Verzeichnisdienst nicht nutzbar: %w\n\n%s", err, hint)
}
return "", fmt.Errorf("Verzeichnisdienst nicht nutzbar: %w", err)
}
if dn == "" {