kubernetes/kops · error

failed to delete ssh key %s (ID %s): %s

Error message

failed to delete ssh key %s (ID %s): %s

What it means

deleteSSHKey wraps a non-404 error from the DigitalOcean SSH Keys DeleteByID API. 404 is treated as success (key already gone); anything else — auth failure, forbidden scope, rate limit, network — produces this error while deleting an SSH key during cluster teardown. The key name, ID, and underlying error are all included.

Source

Thrown at pkg/resources/digitalocean/resources.go:447

	return resourceTrackers
}

func deleteSSHKey(cloud fi.Cloud, t *resources.Resource) error {
	c := cloud.(do.DOCloud)

	id, err := strconv.Atoi(t.ID)
	if err != nil {
		return fmt.Errorf("failed to convert ssh key ID %q to int: %s", t.ID, err)
	}

	klog.V(2).Infof("deleting DO SSH key %q (ID %d)", t.Name, id)
	response, err := c.KeysService().DeleteByID(context.TODO(), id)
	if err != nil {
		if response != nil && response.StatusCode == http.StatusNotFound {
			return nil
		}
		return fmt.Errorf("failed to delete ssh key %s (ID %s): %s", t.Name, t.ID, err)
	}

	return nil
}

func listVPCs(cloud fi.Cloud, clusterName string) ([]*resources.Resource, error) {
	c := cloud.(do.DOCloud)
	var resourceTrackers []*resources.Resource

	clusterName = do.SafeClusterName(clusterName)
	vpcName := "vpc-" + clusterName

	vpcs, err := c.GetAllVPCs()
	if err != nil {
		return nil, fmt.Errorf("failed to list vpcs: %v", err)
	}

	for _, vpc := range vpcs {

View on GitHub (pinned to 4c8573c808)

Solutions

  1. Check the wrapped error's HTTP status; verify the token has write scope (doctl compute ssh-key delete <id> as a test)
  2. Retry after backoff on 429/5xx; teardown is idempotent thanks to the 404 pass-through
  3. If the token is read-only, regenerate/reuse a token with full(write) scope and rerun kOps delete cluster
  4. Check DO status page for ongoing incidents before further retries
Defensive patterns

Strategy: try-catch

Validate before calling

// Check key existence first; skip if already gone
_, resp, err := c.KeysService().GetByID(context.TODO(), id)
if err != nil {
	if resp != nil && resp.StatusCode == http.StatusNotFound {
		return nil
	}
	return fmt.Errorf("cannot fetch ssh key %d: %v", id, err)
}

Try / catch

response, err := c.KeysService().DeleteByID(context.TODO(), id)
if err != nil {
	if response != nil && response.StatusCode == http.StatusNotFound {
		return nil
	}
	if response != nil && (response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500) {
		// retry with exponential backoff
	}
	return fmt.Errorf("failed to delete ssh key %s (ID %s): %w", t.Name, t.ID, err)
}

Prevention

When it happens

Trigger: KeysService().DeleteByID returns an error that is not HTTP 404: 401 invalid token, 403 token lacks write scope, 429 rate limited, 5xx DO server error, or network failure.

Common situations: Read-only API token used for cluster deletion; DO API incident; rate limiting when deleting many clusters concurrently; key already removed by another process (that path returns nil, so this error implies something other than missing key).

Related errors


AI-assisted analysis of kubernetes/kops@4c8573c808 (2026-09-05). Data as JSON: /api/errors/076c94494f4de6e5. Report an issue: GitHub.