{"record":{"id":"03201b539879d6f8","repo":"JuliusBrussee/caveman","slug":"githubapp-private-key-is-not-valid-pem","errorCode":null,"errorMessage":"githubapp: private key is not valid PEM","messagePattern":"githubapp: private key is not valid PEM","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/githubapp/githubapp.go","lineNumber":400,"sourceCode":"\tresp, err := a.httpClient.Do(req)\n\tif err != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\traw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))\n\tif err != nil {\n\t\treturn resp.StatusCode, nil, fmt.Errorf(\"githubapp: read response: %w\", err)\n\t}\n\treturn resp.StatusCode, raw, nil\n}\n\n// parseRSAPrivateKey accepts a PKCS#1 (\"RSA PRIVATE KEY\") or PKCS#8\n// (\"PRIVATE KEY\") PEM — GitHub Apps download PKCS#1, but Cloud KMS / openssl\n// conversions emit PKCS#8, so we accept both.\nfunc parseRSAPrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {\n\tblock, _ := pem.Decode(pemBytes)\n\tif block == nil {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is not valid PEM\")\n\t}\n\tif key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {\n\t\treturn key, nil\n\t}\n\tparsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is neither PKCS#1 nor PKCS#8 RSA: %w\", err)\n\t}\n\tkey, ok := parsed.(*rsa.PrivateKey)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is not RSA\")\n\t}\n\treturn key, nil\n}\n\n// snippet trims an error body so we never echo a large/secret-bearing response.\nfunc snippet(b []byte) string {\n\tconst max = 256","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/githubapp/githubapp.go#L382-L418","documentation":"Thrown by parseRSAPrivateKey in shared/platform/githubapp/githubapp.go:400 when pem.Decode finds no PEM block at all in the supplied private-key bytes. The function accepts PKCS#1 and PKCS#8 RSA keys (GitHub Apps download PKCS#1; KMS/openssl conversions often emit PKCS#8), but both require a valid PEM envelope first. This error means the input is not PEM in any form.","triggerScenarios":"The GitHub App private key env var / secret contains raw DER bytes, a JSON service-account key, base64 without PEM headers, a markdown-fenced key copied from docs, or an empty/LRM-character-polluted paste. pem.Decode returns a nil block and this error fires.","commonSituations":"Downloading the .pem from GitHub and processing it with xxd/base64 instead of using the file; storing the key in a secret manager field that stripped the BEGIN/END lines; copying the key through a chat/wiki that mangled dashes; picking the wrong file out of the app's key directory.","solutions":["Ensure the value starts with -----BEGIN RSA PRIVATE KEY----- or -----BEGIN PRIVATE KEY----- and ends with the matching END line.","Re-download the .pem from the GitHub App settings page and inject it verbatim.","If the secret manager mangles newlines, store base64 of the file and decode in the container: base64 -d.","Check for invisible characters (BOM, \\r\\n is fine, but smart quotes are not) by re-encoding and diffing."],"exampleFix":"# before: JSON key stored in GITHUBAPP_PRIVATE_KEY\n{\"type\":\"service_account\",...}\n\n# after: the app's .pem file contents\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----","handlingStrategy":"validation","validationCode":"block, _ := pem.Decode(keyBytes)\nif block == nil {\n    return errors.New(\"key is not PEM: expected BEGIN/END envelope\")\n}","typeGuard":"func isPEMKey(b []byte) bool {\n    block, _ := pem.Decode(b)\n    return block != nil && strings.Contains(block.Type, \"PRIVATE KEY\")\n}","tryCatchPattern":"if err := githubapp.WithPrivateKey(keyBytes); err != nil {\n    return fmt.Errorf(\"github app credentials: %w\", err) // surface at boot, not at first API call\n}","preventionTips":["Inject the .pem downloaded from GitHub App settings verbatim.","Store keys base64-encoded in secret managers and decode at mount time to survive newline mangling.","Add a startup credential check that PEM-decodes the key before serving traffic."],"tags":["go","github-app","pem","rsa","authentication"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}