AdguardTeam/AdGuardHome · critical
certificate-key pair: %w
Error message
certificate-key pair: %w
What it means
tls.X509KeyPair rejected the certificate chain and private key together: they are a valid parse individually but do not match (public key in the certificate differs from the private key). Returned during combined validation in validateCertificates.
Source
Thrown at internal/aghtls/defaultmanager.go:857
// Validate the private key by parsing it.
if len(pkey) > 0 {
var keyErr error
status.KeyType, keyErr = validatePKey(pkey)
if keyErr != nil {
// Don't wrap the error, since it's informative enough as is.
return keyErr
}
// Set status.ValidKey to true to signal the frontend that the
// key is valid.
status.ValidKey = true
}
// If both are set, validate together.
if len(certChain) > 0 && len(pkey) > 0 {
_, pairErr := tls.X509KeyPair(certChain, pkey)
if pairErr != nil {
return fmt.Errorf("certificate-key pair: %w", pairErr)
}
status.ValidPair = true
}
return err
}
// validateCertificate processes certificate data. status must not be nil, as
// it is used to accumulate the validation results. logger and rootCAs must not
// be nil. Other parameters are optional. If ok is true, the returned error, if
// any, is not critical.
func validateCertificate(
ctx context.Context,
logger *slog.Logger,
rootCAs *x509.CertPool,
status *TLSConfigStatus,
certChain []byte,View on GitHub (pinned to b41aefbe51)
Solutions
- Compare public keys: openssl x509 -in cert.pem -noout -pubkey vs openssl pkey -in key.pem -pubout — they must match
- Regenerate the pair together (new CSR from the current key, reissue the cert)
- Check that certificate_path and private_key_path are not swapped or pointing at sibling domains
- If using acme clients, re-run the issuance so cert and key are written in the same transaction
Example fix
# before certificate: /etc/letsencrypt/live/a.com/fullchain.pem key: /etc/letsencrypt/live/b.com/privkey.pem # after certificate: /etc/letsencrypt/live/a.com/fullchain.pem key: /etc/letsencrypt/live/a.com/privkey.pem
Defensive patterns
Strategy: validation
Validate before calling
certPub := pubKeyPEM(certPEM)
keyPub := privKeyPubPEM(keyPEM)
if certPub != keyPub { return fmt.Errorf("certificate does not match key") } Try / catch
if err := mgr.LoadTLSConfig(ctx, conf); err != nil {
if strings.Contains(err.Error(), "certificate-key pair") { /* regenerate pair together */ }
} Prevention
- Always issue a new CSR from the exact key that will serve traffic
- Automate renewal so cert+key are written atomically as a set
- Name files per-domain and double-check path pairing in config
When it happens
Trigger: LoadTLSConfig with both a cert chain and a key where the key is not the one the certificate was issued for: renewed cert paired with an old key, swapped files, or wrong chain ordering in some failure modes.
Common situations: Cert renewed via a new CSR with a fresh key, but config still points at the old key; key regenerated manually after issuance; multi-domain setups where cert/key filenames got crossed.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- loading tls certificate: %w
- unknown cipher %q
- parsing tls certificate: %w
- certificate does not verify: %w
- parsing certificate at index %d: %w
AI-assisted analysis of AdguardTeam/AdGuardHome@b41aefbe51 (2026-08-27).
Data as JSON: /api/errors/ef39e49f1a8fd010.
Report an issue: GitHub.