grpc/grpc-go · error

credentials: failed to read the service account key file

Error message

credentials: failed to read the service account key file: %v

What it means

Returned by NewJWTAccessFromFile when os.ReadFile cannot read the service-account JSON key file. The %v is the OS error. This happens at credential construction time (not per-RPC), so the failure surfaces immediately when building the gRPC channel. NewServiceAccountFromFile wraps the same root cause with an identical message.

Solutions

  1. Confirm the key file path exists and is readable: ls -l /path/to/key.json.
  2. Use an absolute path resolved from a single config source.
  3. Mount the Kubernetes Secret/ConfigMap containing the key before the process starts.
  4. Prefer Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or metadata server) over manual file paths when possible.

Example fix

// before
creds, err := oauth.NewJWTAccessFromFile("service-account.json")
// after
keyPath := "/etc/secrets/gcp/service-account.json"
if _, err := os.Stat(keyPath); err != nil {
    log.Fatalf("key file missing: %v", err)
}
creds, err := oauth.NewJWTAccessFromFile(keyPath)
Defensive patterns

Strategy: validation

Validate before calling

func loadKey(path string) ([]byte, error) {
    if _, err := os.Stat(path); err != nil {
        return nil, fmt.Errorf("service-account key missing at %q: %w", path, err)
    }
    return os.ReadFile(path)
}

jsonKey, err := loadKey(keyPath)
if err != nil {
    log.Fatal(err)
}
creds, err := oauth.NewJWTAccessFromKey(jsonKey)

Try / catch

creds, err := oauth.NewJWTAccessFromFile(keyPath)
if err != nil {
    return fmt.Errorf("cannot load service-account key %q: %w", keyPath, err)
}

Prevention

When it happens

Trigger: Passing a non-existent or unreadable key file path to oauth.NewJWTAccessFromFile or oauth.NewServiceAccountFromFile; relative path resolved against the wrong working directory; missing file permission.

Common situations: Deploying without mounting the service-account key Secret; path typo; running as a user without read permission; CI using a different key path than production.

Related errors


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

Appendix: source

Thrown at credentials/oauth/oauth.go:78

// removeServiceNameFromJWTURI removes RPC service name from URI.
func removeServiceNameFromJWTURI(uri string) (string, error) {
	parsed, err := url.Parse(uri)
	if err != nil {
		return "", err
	}
	parsed.Path = "/"
	return parsed.String(), nil
}

type jwtAccess struct {
	jsonKey []byte
}

// NewJWTAccessFromFile creates PerRPCCredentials from the given keyFile.
func NewJWTAccessFromFile(keyFile string) (credentials.PerRPCCredentials, error) {
	jsonKey, err := os.ReadFile(keyFile)
	if err != nil {
		return nil, fmt.Errorf("credentials: failed to read the service account key file: %v", err)
	}
	return NewJWTAccessFromKey(jsonKey)
}

// NewJWTAccessFromKey creates PerRPCCredentials from the given jsonKey.
func NewJWTAccessFromKey(jsonKey []byte) (credentials.PerRPCCredentials, error) {
	return jwtAccess{jsonKey}, nil
}

func (j jwtAccess) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	// Remove RPC service name from URI that will be used as audience
	// in a self-signed JWT token. It follows https://google.aip.dev/auth/4111.
	aud, err := removeServiceNameFromJWTURI(uri[0])
	if err != nil {
		return nil, err
	}
	// TODO: the returned TokenSource is reusable. Store it in a sync.Map, with
	// uri as the key, to avoid recreating for every RPC.

View on GitHub (pinned to 0c51461d27)