grpc/grpc-go · error

xds: config parsing for certificate provider plugin

Error message

xds: config parsing for certificate provider plugin %q failed during bootstrap: %v

What it means

Returned by Config.UnmarshalJSON when a certificate provider plugin's ParseConfig fails. Each certificate_providers entry names a plugin and a config blob; if the plugin rejects the config, bootstrap fails with the instance name printed.

Solutions

  1. Read the nested %v to see which constraint the plugin rejected.
  2. For file_watcher, supply the required fields: at least one of certificate_file or ca_certificate_file, plus private_key_file when a cert is given, and a refresh_interval.
  3. Verify the plugin_name is spelled exactly (commonly 'file_watcher').
  4. Confirm the cert provider plugin is built into your grpc-go (pemfile is).

Example fix

// before (missing required file fields)
"certificate_providers":{"default":{"plugin_name":"file_watcher","config":{"refresh_interval":"1s"}}}

// after
"certificate_providers":{"default":{"plugin_name":"file_watcher","config":{"certificate_file":"/etc/certs/client.crt","private_key_file":"/etc/certs/client.key","ca_certificate_file":"/etc/certs/ca.crt","refresh_interval":"1s"}}}
Defensive patterns

Strategy: validation

Validate before calling

// Validate a file_watcher certificate provider config block.
func validateFileWatcher(cfg map[string]any) error {
    refresh, _ := cfg["refresh_interval"].(string)
    if refresh == "" {
        return fmt.Errorf("file_watcher: refresh_interval required")
    }
    cert, _ := cfg["certificate_file"].(string)
    ca, _ := cfg["ca_certificate_file"].(string)
    if cert == "" && ca == "" {
        return fmt.Errorf("file_watcher: need certificate_file or ca_certificate_file")
    }
    return nil
}

Try / catch

if _, err := bootstrap.NewConfigFromContents(data); err != nil {
    if strings.Contains(err.Error(), "certificate provider plugin") {
        // read nested cause, fix the named provider's config.
    }
}

Prevention

When it happens

Trigger: Triggered at bootstrap.go:620 when parser.ParseConfig(nameAndConfig.Config) errors. Most commonly the file_watcher (pemfile) plugin rejects a config missing required file fields or with invalid values.

Common situations: file_watcher config missing certificate_file/private_key_file when mTLS is required; bad refresh interval; unknown plugin-specific field; mismatched plugin_name spelling.

Understand the failure class

Related errors


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

Appendix: source

Thrown at internal/xds/bootstrap/bootstrap.go:620

	c.cpcs = config.CertificateProviders
	c.serverListenerResourceNameTemplate = config.ServerListenerResourceNameTemplate
	c.clientDefaultListenerResourceNameTemplate = config.ClientDefaultListenerResourceNameTemplate
	c.authorities = config.Authorities
	c.node = config.Node

	// Build the certificate providers configuration to ensure that it is valid.
	cpcCfgs := make(map[string]*certprovider.BuildableConfig)
	getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
	for instance, nameAndConfig := range c.cpcs {
		name := nameAndConfig.PluginName
		parser := getBuilder(nameAndConfig.PluginName)
		if parser == nil {
			// We ignore plugins that we do not know about.
			continue
		}
		bc, err := parser.ParseConfig(nameAndConfig.Config)
		if err != nil {
			return fmt.Errorf("xds: config parsing for certificate provider plugin %q failed during bootstrap: %v", name, err)
		}
		cpcCfgs[instance] = bc
	}
	c.certProviderConfigs = cpcCfgs

	// Default value of the default client listener name template is "%s".
	if c.clientDefaultListenerResourceNameTemplate == "" {
		c.clientDefaultListenerResourceNameTemplate = "%s"
	}
	if len(c.xDSServers) == 0 {
		return fmt.Errorf("xds: required field `xds_servers` not found in bootstrap configuration: %s", string(data))
	}

	// Post-process the authorities' client listener resource template field:
	// - if set, it must start with "xdstp://<authority_name>/"
	// - if not set, it defaults to "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s"
	for name, authority := range c.authorities {
		prefix := fmt.Sprintf("xdstp://%s", url.PathEscape(name))

View on GitHub (pinned to 0c51461d27)