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
- Move (or symlink) both the certificate and key files into the same directory.
- If the files are managed by cert-manager or a secret rotation system, configure it to write both outputs to one directory.
- 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
- Place cert and key files in the same directory.
- If separate directories are required, use credentials.NewTLS with tls.LoadX509KeyPair instead.
- Configure cert-manager or secret rotators to write both files to one directory.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- cannot have a leading slash
- cannot have exclude and a '*' wildcard
- ClientConn's authority from transport creds
- empty string is not a valid method binary logging config
- empty token_exchange_service_uri in options
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 = defaultCertRefreshDurationView on GitHub (pinned to 0c51461d27)