grpc/grpc-go · error

pemfile: certificate and key file must be in the same…

Error message

pemfile: certificate and key file must be in the same directory

What it means

Returned by pemfile.Options.validate when filepath.Dir(o.CertFile) does not equal filepath.Dir(o.KeyFile). The pemfile certificate provider requires the identity certificate and private key to reside in the same directory so that an atomic read (symlink swap in the same dir) can be performed consistently. This constraint is enforced for cross-language consistency even though the Go implementation could technically handle separate directories.

Solutions

  1. Move (or symlink) both the certificate and key files into the same directory.
  2. If the files are managed by cert-manager or a secret rotation system, configure it to write both outputs to one directory.
  3. If using separate directories is unavoidable, use credentials.NewTLS with a tls.Config that uses tls.LoadX509KeyPair instead of the pemfile provider.

Example fix

// before
provider, err := pemfile.NewProvider(pemfile.Options{
    CertFile: "/etc/certs/server.crt",
    KeyFile:  "/etc/keys/server.key",
    RootFile: "/etc/certs/ca.crt",
}) // error
// after
provider, err := pemfile.NewProvider(pemfile.Options{
    CertFile: "/etc/certs/server.crt",
    KeyFile:  "/etc/certs/server.key",  // same directory
    RootFile: "/etc/certs/ca.crt",
})
Defensive patterns

Strategy: validation

Validate before calling

if filepath.Dir(o.CertFile) != filepath.Dir(o.KeyFile) {
    return fmt.Errorf("cert and key must be in the same directory; got %s and %s", filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile))
}

Prevention

When it happens

Trigger: Creating a pemfile certificate provider with pemfile.Options{CertFile: "/etc/certs/server.crt", KeyFile: "/etc/keys/server.key"} where the two files are in different directories.

Common situations: Operators place certificates and keys in separate directories following standard Unix conventions (/etc/ssl/certs vs /etc/ssl/private). This works with crypto/tls directly but fails with the pemfile provider. Also seen when upgrading from manual TLS config to the cert-provider abstraction.

Understand the failure class

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/d2d9e8391da1b61f. Report an issue: GitHub.

Appendix: source

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

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) {
	if err := o.validate(); err != nil {
		return nil, err
	}
	return newProvider(o), nil
}

// newProvider is used to create a new certificate provider plugin after
// validating the options, and hence does not return an error.
func newProvider(o Options) certprovider.Provider {
	if o.RefreshDuration == 0 {
		o.RefreshDuration = defaultCertRefreshDuration

View on GitHub (pinned to 0c51461d27)