router-for-me/CLIProxyAPI · error

service account missing private_key

Error message

service account missing private_key

What it means

Thrown by NormalizeServiceAccountMap when the service account JSON parses but its private_key field is missing, empty, or whitespace-only (keyutil.go:41-43). The type assertion sa["private_key"].(string) failing (field absent or non-string) also yields an empty pk. Vertex JWT signing requires this RSA key, so normalization aborts.

Source

Thrown at internal/auth/vertex/keyutil.go:43

	if err != nil {
		return raw, err
	}
	out, err := json.Marshal(normalized)
	if err != nil {
		return raw, err
	}
	return out, nil
}

// NormalizeServiceAccountMap returns a copy of the given service account map with
// a sanitized private_key field that is guaranteed to contain a valid RSA PRIVATE KEY PEM block.
func NormalizeServiceAccountMap(sa map[string]any) (map[string]any, error) {
	if sa == nil {
		return nil, fmt.Errorf("service account payload is empty")
	}
	pk, _ := sa["private_key"].(string)
	if strings.TrimSpace(pk) == "" {
		return nil, fmt.Errorf("service account missing private_key")
	}
	normalized, err := sanitizePrivateKey(pk)
	if err != nil {
		return nil, err
	}
	clone := make(map[string]any, len(sa))
	for k, v := range sa {
		clone[k] = v
	}
	clone["private_key"] = normalized
	return clone, nil
}

func sanitizePrivateKey(raw string) (string, error) {
	pk := strings.ReplaceAll(raw, "\r\n", "\n")
	pk = strings.ReplaceAll(pk, "\r", "\n")
	pk = stripANSIEscape(pk)
	pk = strings.ToValidUTF8(pk, "")

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Regenerate the key in GCP (IAM & Admin > Service Accounts > Keys > Add key > JSON) and use the fresh file
  2. Verify the file actually contains a private_key field: jq -r '.private_key | length' service-account.json (should be > 1600)
  3. Ensure you are using a service account key file, not an OAuth client credentials file
  4. If passing via env/store, confirm the value was not truncated or placeholder-replaced

Example fix

# before
jq -r '.private_key' sa.json   # null or empty
# after: re-download the key and check
jq -r '.private_key | startswith("-----BEGIN")' sa.json   # true
Defensive patterns

Strategy: validation

Validate before calling

if pk, ok := sa["private_key"].(string); !ok || strings.TrimSpace(pk) == "" {
    return fmt.Errorf("service account JSON lacks private_key; re-download the key from GCP")
}

Type guard

func hasPrivateKey(sa map[string]any) bool {
    pk, ok := sa["private_key"].(string)
    return ok && strings.TrimSpace(pk) != ""
}

Prevention

When it happens

Trigger: Uploading a service account JSON where private_key was redacted for security; copying the wrong JSON (e.g. a GCP 'client_secret' OAuth file or a key-info stub) as the Vertex credential; the field present but containing only newlines/whitespace.

Common situations: Teams commit sanitized service account templates with the key stripped, then deploy them accidentally; users confuse the OAuth client JSON with the service account key JSON; key truncated during copy-paste into an env var.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/9766641c1a45ba3d. Report an issue: GitHub.