{"record":{"id":"cdc34dc62ffca699","repo":"grpc/grpc-go","slug":"pemfile-at-least-one-credential-file-needs-to-be","errorCode":null,"errorMessage":"pemfile: at least one credential file needs to be specified","messagePattern":"pemfile: at least one credential file needs to be specified","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"credentials/tls/certprovider/pemfile/watcher.go","lineNumber":82,"sourceCode":"\tRootFile string\n\t// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.\n\t// If a given provider configures both the RootFile and the\n\t// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.\n\t// Optional.\n\tSPIFFEBundleMapFile string\n\t// RefreshDuration is the amount of time the plugin waits before checking\n\t// for updates in the specified files.\n\t// Optional. If not set, a default value (1 hour) will be used.\n\tRefreshDuration time.Duration\n}\n\nfunc (o Options) canonical() []byte {\n\treturn []byte(fmt.Sprintf(\"%s:%s:%s:%s:%s\", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))\n}\n\nfunc (o Options) validate() error {\n\tif o.CertFile == \"\" && o.KeyFile == \"\" && o.RootFile == \"\" && o.SPIFFEBundleMapFile == \"\" {\n\t\treturn fmt.Errorf(\"pemfile: at least one credential file needs to be specified\")\n\t}\n\tif keySpecified, certSpecified := o.KeyFile != \"\", o.CertFile != \"\"; keySpecified != certSpecified {\n\t\treturn fmt.Errorf(\"pemfile: private key file and identity cert file should be both specified or not specified\")\n\t}\n\t// C-core has a limitation that they cannot verify that a certificate file\n\t// matches a key file. So, the only way to get around this is to make sure\n\t// that both files are in the same directory and that they do an atomic\n\t// read. Even though Java/Go do not have this limitation, we want the\n\t// overall plugin behavior to be consistent across languages.\n\tif certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {\n\t\treturn errors.New(\"pemfile: certificate and key file must be in the same directory\")\n\t}\n\treturn nil\n}\n\n// NewProvider returns a new certificate provider plugin that is configured to\n// watch the PEM files specified in the passed in options.\nfunc NewProvider(o Options) (certprovider.Provider, error) {","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/credentials/tls/certprovider/pemfile/watcher.go#L64-L100","documentation":"Options.validate() rejects a file_watcher configuration where all four watched-file fields are empty: CertFile, KeyFile, RootFile, and SPIFFEBundleMapFile. The provider must watch at least one file to be useful, so this is a hard configuration error returned from both NewProvider and ParseConfig.","triggerScenarios":"Calling pemfile.NewProvider(Options{}) with no fields set, or passing a JSON config like {} that has no file paths. Also triggered when every path is the empty string, e.g. config generated from a template whose variables were never substituted.","commonSituations":"Empty/placeholder config object left in bootstrap; misconfigured CI that renders an empty file_watcher block; dev forgot to populate the cert paths for an environment.","solutions":["Provide at least one of ca_certificate_file, certificate_file+private_key_file (both required together), or spiffe_trust_bundle_map_file.","If you only need server validation, set ca_certificate_file to your root CA bundle path.","Audit the config template for unfilled ${VAR} placeholders that resolve to empty strings."],"exampleFix":"// before\npemfile.NewProvider(pemfile.Options{})\n\n// after\npemfile.NewProvider(pemfile.Options{RootFile: \"/etc/ssl/certs/ca.crt\"})","handlingStrategy":"validation","validationCode":"func validatePemfileOptions(o pemfile.Options) error {\n    if o.CertFile == \"\" && o.KeyFile == \"\" && o.RootFile == \"\" && o.SPIFFEBundleMapFile == \"\" {\n        return errors.New(\"pemfile: at least one credential file is required\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"p, err := pemfile.NewProvider(opts)\nif err != nil { return fmt.Errorf(\"cert provider: %w\", err) }","preventionTips":["Default to setting RootFile for server-validation-only setups.","Add a config-loader unit test asserting at least one path is set.","Fail builds if templates leave path variables unsubstituted."],"tags":["grpc","config","pemfile","validation","certprovider","tls"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}