kubernetes/kops · error

failed to verify claim signature for node

Error message

failed to verify claim signature for node

What it means

The ECDSA signature on the token's Data payload failed verification against the public key registered for the claimed node. The claims parsed, the audience/timestamp/hash passed, and a Host object with a public key was found — but ecdsa.VerifyASN1 (the only supported key type) returned false. This means either the token was not signed by the private key corresponding to the Host's spec.publicKey, or the signed payload differs from token.Data.

Source

Thrown at pkg/bootstrap/pkibootstrap/pkiverifier/verifier.go:126

// Note that golang doesn't support secp256k1: https://groups.google.com/g/golang-nuts/c/Mbkug5t3ZYA

func (v *verifier) VerifyToken(ctx context.Context, rawRequest *http.Request, authToken string, body []byte) (*bootstrap.VerifyResult, error) {
	// Reminder: we shouldn't trust any data we get from the client until we've checked the signature (and even then...)
	// Thankfully the GCE SDK does seem to escape the parameters correctly, for example.

	token, tokenData, err := v.parseTokenData(pkibootstrap.AuthenticationTokenPrefix, authToken, body)
	if err != nil {
		return nil, err
	}

	// Verify the token has a valid signature.
	result, signingKey, err := v.getSigningKey(ctx, tokenData)
	if err != nil {
		return nil, err
	}

	if !verifySignature(signingKey, token.Data, token.Signature) {
		return nil, fmt.Errorf("failed to verify claim signature for node")
	}

	return result, nil
}

func (v *verifier) getSigningKey(ctx context.Context, tokenData *pkibootstrap.AuthTokenData) (*bootstrap.VerifyResult, crypto.PublicKey, error) {
	nodeName := tokenData.Instance
	id := types.NamespacedName{
		Namespace: "kops-system",
		Name:      nodeName,
	}
	var host kops.Host
	if err := v.client.Get(ctx, id, &host); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, nil, fmt.Errorf("host not found for %v", id)
		}
		return nil, nil, fmt.Errorf("error getting host %v: %w", id, err)
	}

View on GitHub (pinned to 4c8573c808)

Solutions

  1. Confirm the node signs with the private key matching the Host's spec.publicKey: regenerate the Host's publicKey from the node's current public key (`openssl ecparam -name prime256v1 -genkey ...; openssl ec -in ec-priv-key.pem -pubout`).
  2. Check kops-controller logs for the "key type %T not supported" warning — if present, switch the node key to an ECDSA key (Go supports P-256/P-384/P-521, not secp256k1).
  3. Ensure the Host object name (tokenData.Instance) maps to the node actually sending the request; fix Instance or the Host CR if they diverge.
  4. Rule out body/token corruption in transit (a proxy rewriting the Authorization header) and retry with a freshly minted token.
  5. If the key was intentionally rotated, update the kops Host spec.publicKey (or delete and recreate the Host object) so the controller trusts the new key.

Example fix

// before: RSA key on node, unsupported by verifier
signer, _ := rsa.GenerateKey(rand.Reader, 2048)
auth, _ := pkibootstrap.NewAuthenticator(hostname, signer)
// after: ECDSA P-256 key, matching the Host's registered public key
signer, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
auth, _ := pkibootstrap.NewAuthenticator(hostname, signer)
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight on the node: confirm the registered Host public key matches the signer's public key
pubPEM, _ := x509.MarshalPKIXPublicKey(signer.Public())
var buf bytes.Buffer
pem.Encode(&buf, &pem.Block{Type: "PUBLIC KEY", Bytes: pubPEM})
// fetch kops Host kops-system/<hostname> and compare:
// strings.TrimSpace(host.Spec.PublicKey) == buf.String()  -> safe to send token
// if mismatched, update host.Spec.PublicKey before bootstrapping

Type guard

func isSupportedSigningKey(key crypto.PublicKey) bool {
	_, ok := key.(*ecdsa.PublicKey)
	return ok
}

Try / catch

result, err := verifier.VerifyToken(ctx, req, authToken, body)
if err != nil {
	if strings.Contains(err.Error(), "failed to verify claim signature for node") {
		// key mismatch: node key rotated or Host CR stale; rotate Host spec.publicKey then re-mint token and retry once
		if rotErr := rotateHostPublicKey(ctx, tokenInstanceName); rotErr != nil {
			return nil, rotErr
		}
		return retryWithFreshToken(ctx, req)
	}
	return nil, err
}

Prevention

When it happens

Trigger: VerifyToken (verifier.go:125) raises this when verifySignature returns false: the node's private key was rotated/regenerated but the kops Host object (kops-system/<nodeName>) still holds the old public key; the Host's spec.publicKey is for a different node; the signing key is an unsupported type (verifySignature warns "key type %T not supported" and returns false, e.g. RSA key); or the token bytes were corrupted after signing.

Common situations: Key rotation on the node (re-running bootstrap with a new key) without updating the Host CR; the wrong private key file passed to NewAuthenticatorFromFile; Host CR created with a placeholder or mismatched PEM public key; using an RSA/Ed25519 key where only ECDSA (e.g. prime256v1 per the comments at verifier.go:106) is supported; replayed or tampered tokens caught as designed.

Related errors


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