{"record":{"id":"ef39e49f1a8fd010","repo":"AdguardTeam/AdGuardHome","slug":"certificate-key-pair-w","errorCode":null,"errorMessage":"certificate-key pair: %w","messagePattern":"certificate-key pair: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/aghtls/defaultmanager.go","lineNumber":857,"sourceCode":"\t// Validate the private key by parsing it.\n\tif len(pkey) > 0 {\n\t\tvar keyErr error\n\t\tstatus.KeyType, keyErr = validatePKey(pkey)\n\t\tif keyErr != nil {\n\t\t\t// Don't wrap the error, since it's informative enough as is.\n\t\t\treturn keyErr\n\t\t}\n\n\t\t// Set status.ValidKey to true to signal the frontend that the\n\t\t// key is valid.\n\t\tstatus.ValidKey = true\n\t}\n\n\t// If both are set, validate together.\n\tif len(certChain) > 0 && len(pkey) > 0 {\n\t\t_, pairErr := tls.X509KeyPair(certChain, pkey)\n\t\tif pairErr != nil {\n\t\t\treturn fmt.Errorf(\"certificate-key pair: %w\", pairErr)\n\t\t}\n\n\t\tstatus.ValidPair = true\n\t}\n\n\treturn err\n}\n\n// validateCertificate processes certificate data.  status must not be nil, as\n// it is used to accumulate the validation results.  logger and rootCAs must not\n// be nil. Other parameters are optional.  If ok is true, the returned error, if\n// any, is not critical.\nfunc validateCertificate(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\trootCAs *x509.CertPool,\n\tstatus *TLSConfigStatus,\n\tcertChain []byte,","sourceCodeStart":839,"sourceCodeEnd":875,"githubUrl":"https://github.com/AdguardTeam/AdGuardHome/blob/b41aefbe51c8dde65e2c50f093996afa0502edf9/internal/aghtls/defaultmanager.go#L839-L875","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\ncertificate: /etc/letsencrypt/live/a.com/fullchain.pem\nkey: /etc/letsencrypt/live/b.com/privkey.pem\n# after\ncertificate: /etc/letsencrypt/live/a.com/fullchain.pem\nkey: /etc/letsencrypt/live/a.com/privkey.pem","handlingStrategy":"validation","validationCode":"certPub := pubKeyPEM(certPEM)\nkeyPub := privKeyPubPEM(keyPEM)\nif certPub != keyPub { return fmt.Errorf(\"certificate does not match key\") }","typeGuard":null,"tryCatchPattern":"if err := mgr.LoadTLSConfig(ctx, conf); err != nil {\n    if strings.Contains(err.Error(), \"certificate-key pair\") { /* regenerate pair together */ }\n}","preventionTips":["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"],"tags":["tls","x509","crypto","key-mismatch"],"backgroundTag":"certificate-key-mismatch","analyzedSha":"b41aefbe51c8dde65e2c50f093996afa0502edf9","analyzedAt":"2026-08-27T04:57:55.097Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}