grpc/grpc-go · error

pemfile: at least one credential file needs to be specified

Error message

pemfile: at least one credential file needs to be specified

What it means

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.

Source

Thrown at credentials/tls/certprovider/pemfile/watcher.go:82

	RootFile string
	// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
	// If a given provider configures both the RootFile and the
	// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
	// Optional.
	SPIFFEBundleMapFile string
	// RefreshDuration is the amount of time the plugin waits before checking
	// for updates in the specified files.
	// Optional. If not set, a default value (1 hour) will be used.
	RefreshDuration time.Duration
}

func (o Options) canonical() []byte {
	return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
}

func (o Options) validate() error {
	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
	}
	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
	}
	// C-core has a limitation that they cannot verify that a certificate file
	// matches a key file. So, the only way to get around this is to make sure
	// that both files are in the same directory and that they do an atomic
	// read. Even though Java/Go do not have this limitation, we want the
	// overall plugin behavior to be consistent across languages.
	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
		return errors.New("pemfile: certificate and key file must be in the same directory")
	}
	return nil
}

// NewProvider returns a new certificate provider plugin that is configured to
// watch the PEM files specified in the passed in options.
func NewProvider(o Options) (certprovider.Provider, error) {

View on GitHub (pinned to 03255a9237)

Solutions

  1. Provide at least one of ca_certificate_file, certificate_file+private_key_file (both required together), or spiffe_trust_bundle_map_file.
  2. If you only need server validation, set ca_certificate_file to your root CA bundle path.
  3. Audit the config template for unfilled ${VAR} placeholders that resolve to empty strings.

Example fix

// before
pemfile.NewProvider(pemfile.Options{})

// after
pemfile.NewProvider(pemfile.Options{RootFile: "/etc/ssl/certs/ca.crt"})
Defensive patterns

Strategy: validation

Validate before calling

func validatePemfileOptions(o pemfile.Options) error {
    if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
        return errors.New("pemfile: at least one credential file is required")
    }
    return nil
}

Try / catch

p, err := pemfile.NewProvider(opts)
if err != nil { return fmt.Errorf("cert provider: %w", err) }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/cdc34dc62ffca699. Report an issue: GitHub.