{"record":{"id":"3257e8564fa6e708","repo":"github/github-mcp-server","slug":"invalid-github-app-private-key-w","errorCode":null,"errorMessage":"invalid GitHub App private key: %w","messagePattern":"invalid GitHub App private key: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/githubapp/githubapp.go","lineNumber":196,"sourceCode":"\t}, nil\n}\n\n// Provider caches and refreshes GitHub App installation access tokens.\ntype Provider struct {\n\tsource oauth2.TokenSource\n\tlogger *slog.Logger\n\n\tmu        sync.Mutex\n\terrLogged bool\n}\n\nfunc NewProvider(cfg Config, logger *slog.Logger) (*Provider, error) {\n\tif err := cfg.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tprivateKey, err := parsePrivateKey(cfg.PrivateKeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid GitHub App private key: %w\", err)\n\t}\n\tif logger == nil {\n\t\tlogger = slog.Default()\n\t}\n\tsource := oauth2.ReuseTokenSource(nil, newInstallationTokenSource(cfg, privateKey, nil))\n\treturn &Provider{source: source, logger: logger}, nil\n}\n\n// AccessToken returns a cached token or refreshes it before expiry.\nfunc (p *Provider) AccessToken() string {\n\ttok, err := p.source.Token()\n\tif err != nil {\n\t\tp.mu.Lock()\n\t\tif !p.errLogged {\n\t\t\tp.errLogged = true\n\t\t\tp.logger.Error(\"failed to obtain GitHub App installation token\", \"error\", err)\n\t\t}\n\t\tp.mu.Unlock()","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/internal/githubapp/githubapp.go#L178-L214","documentation":"NewProvider failed to load the GitHub App private key; the %w wrap carries one of the specific parse errors — 'no PEM block found', 'parsing private key (want PKCS#1 or PKCS#8 RSA)', or 'private key is %T, want an RSA key'. It fires once at construction (internal/githubapp/githubapp.go:194-197), so it is a startup-time configuration failure with an actionable chain describing exactly how the PEM is malformed.","triggerScenarios":"Config.PrivateKeyPEM is: base64-encoded instead of raw PEM; a file path string rather than file contents; mangled by env-var transport (\\n literals not converted to newlines, shell quoting stripped the header); a PKCS#8 EC/Ed25519 key; an empty/truncated file after a failed mount.","commonSituations":"Passing GITHUB_APP_PRIVATE_KEY via docker -e with literal '\\n' escapes never expanded; Kubernetes secret mounted as the path instead of the value; copying the .pem out of a browser window truncating the final line; CI secret scanner stripping the BEGIN/END lines; Windows line endings or a BOM prefixing the block.","solutions":["Match the error tail: 'no PEM block found' means the bytes are not PEM at all (check for base64, path-instead-of-contents, stripped headers)","Load the exact file downloaded from the GitHub App settings page: os.ReadFile(path) and pass those bytes — do not paste through env vars","If it must go through an env var, base64-encode it and decode in-process: pemBytes, _ := base64.StdEncoding.DecodeString(v)","Verify with: openssl rsa -in app.pem -check -noout"],"exampleFix":"// before\ncfg := githubapp.Config{PrivateKeyPEM: []byte(os.Getenv(\"GITHUB_APP_PRIVATE_KEY\"))}\n// env contains \"-----BEGIN RSA PRIVATE KEY-----\\\\n...\" with literal backslash-n\n// -> invalid GitHub App private key: no PEM block found in private key\n\n// after\ncfg := githubapp.Config{PrivateKeyPEM: mustReadPEM()}\n\nfunc mustReadPEM() []byte {\n    if p := os.Getenv(\"GITHUB_APP_PRIVATE_KEY_PATH\"); p != \"\" {\n        b, err := os.ReadFile(p) // raw file bytes, headers intact\n        if err != nil { log.Fatal(err) }\n        return b\n    }\n    b, err := base64.StdEncoding.DecodeString(os.Getenv(\"GITHUB_APP_PRIVATE_KEY\"))\n    if err != nil { log.Fatal(err) }\n    return b\n}","handlingStrategy":"validation","validationCode":"func loadPEM() ([]byte, error) {\n    if p := os.Getenv(\"GITHUB_APP_PRIVATE_KEY_PATH\"); p != \"\" {\n        return os.ReadFile(p)\n    }\n    if v := os.Getenv(\"GITHUB_APP_PRIVATE_KEY\"); v != \"\" {\n        return base64.StdEncoding.DecodeString(v) // transport-safe, decode in-process\n    }\n    return nil, errors.New(\"no private key configured\")\n}\n\n// guard: pem.Decode(block) != nil && strings.HasPrefix(block.Type, \"PRIVATE KEY\")","typeGuard":"func looksLikePEM(b []byte) bool {\n    block, _ := pem.Decode(b)\n    return block != nil && strings.Contains(block.Type, \"PRIVATE KEY\")\n}","tryCatchPattern":"if _, err := githubapp.NewProvider(cfg, logger); err != nil {\n    return fmt.Errorf(\"boot: GitHub App credentials rejected: %w\", err) // fail the deploy early\n}","preventionTips":["Prefer the file path (GITHUB_APP_PRIVATE_KEY_PATH) over env-transported PEM bodies","If env transport is required, base64-encode the whole file — never paste raw PEM with \\n escapes","Validate lookLikePEM at config-load time with a clear error naming the variable at fault"],"tags":["configuration","pem","github-app","startup","secrets"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}