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

  1. Compare public keys: openssl x509 -in cert.pem -noout -pubkey vs openssl pkey -in key.pem -pubout — they must match
  2. Regenerate the pair together (new CSR from the current key, reissue the cert)
  3. Check that certificate_path and private_key_path are not swapped or pointing at sibling domains
  4. 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

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

Related errors


AI-assisted analysis of AdguardTeam/AdGuardHome@b41aefbe51 (2026-08-27). Data as JSON: /api/errors/ef39e49f1a8fd010. Report an issue: GitHub.