hashicorp/terraform · error

failed to create cert signer

Error message

failed to create cert signer %q: %s

What it means

ssh.NewCertSigner combines the parsed certificate and signer. This fails if the certificate was not signed by the private key provided (key/cert mismatch), if the certificate is expired or structurally invalid for signer construction, or if the type assertion pcert.(*ssh.Certificate) panics because ParseAuthorizedKey returned a non-certificate key type. SECURITY NOTE: the error interpolates usigner via %q.

Solutions

  1. Ensure the private_key and certificate are a matched pair — the certificate must be signed by the corresponding CA, and the private key must be the one the certificate identifies.
  2. Regenerate both the key pair and certificate together: ssh-keygen -t rsa -f id_rsa then ssh-keygen -s ca_key -I identity id_rsa.pub.
  3. Verify the certificate's serial and key ID match the private key: ssh-keygen -L -f id_rsa-cert.pub.
  4. If hitting a panic (not an error) from the type assertion, ensure the certificate value is actually a certificate, not a plain public key.

Example fix

# before — mismatched key and cert from different sources
connection {
  private_key = file("~/.ssh/key_a")
  certificate  = file("~/.ssh/key_b-cert.pub")  # signed for a different key
}

# after — matched pair
cd ~/.ssh
ssh-keygen -t rsa -f tf_key
ssh-keygen -s ca_key -I tf-identity tf_key.pub
connection {
  private_key = file("~/.ssh/tf_key")
  certificate  = file("~/.ssh/tf_key-cert.pub")
}
Defensive patterns

Strategy: validation

Validate before calling

// Verify the key/cert pair is consistent before calling NewCertSigner
func validateKeyCertPair(pk, cert string) error {
    rawPk, err := ssh.ParseRawPrivateKey([]byte(pk))
    if err != nil {
        return err
    }
    signer, err := ssh.NewSignerFromKey(rawPk)
    if err != nil {
        return err
    }
    parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(cert))
    if err != nil {
        return err
    }
    sshCert, ok := parsedKey.(*ssh.Certificate)
    if !ok {
        return errors.New("not a certificate")
    }
    // Check the certificate's signature key matches the public key of the signer
    certPubKey := sshCert.SignatureKey.Marshal()
    signerPubKey := signer.PublicKey().Marshal()
    if !bytes.Equal(certPubKey, signerPubKey) {
        return errors.New("certificate was signed for a different key than the private key provided")
    }
    return nil
}

Type guard

// Safe type assertion guard to prevent the panic in the original code
func asCertificate(key ssh.PublicKey) (*ssh.Certificate, bool) {
    c, ok := key.(*ssh.Certificate)
    return c, ok
}

Try / catch

// Replace the unsafe assertion pcert.(*ssh.Certificate) with:
sshCert, ok := pcert.(*ssh.Certificate)
if !ok {
    return nil, errors.New("parsed key is not an SSH certificate")
}
ucertSigner, err := ssh.NewCertSigner(sshCert, usigner)
if err != nil {
    return nil, fmt.Errorf("cert signer creation failed (key/cert mismatch?): %w", err)
}

Prevention

When it happens

Trigger: The private key and certificate do not correspond — the certificate was signed by a different CA key. Also triggered when the certificate's principal or critical options are incompatible. There is also a latent PANIC risk: pcert.(*ssh.Certificate) is an unchecked type assertion that will panic (not error) if ParseAuthorizedKey returns a non-*Certificate type.

Common situations: User has multiple key pairs and mismatches a private key with a certificate signed under a different key. Certificate was regenerated by the CA but the user still references the old private key. Certificate and key from different environments (staging vs prod CA).

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/00e6842f5de6e4d7. Report an issue: GitHub.

Appendix: source

Thrown at internal/communicator/ssh/provisioner.go:415

func signCertWithPrivateKey(pk string, certificate string) (ssh.AuthMethod, error) {
	rawPk, err := ssh.ParseRawPrivateKey([]byte(pk))
	if err != nil {
		return nil, fmt.Errorf("failed to parse private key %q: %s", pk, err)
	}

	pcert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate))
	if err != nil {
		return nil, fmt.Errorf("failed to parse certificate %q: %s", certificate, err)
	}

	usigner, err := ssh.NewSignerFromKey(rawPk)
	if err != nil {
		return nil, fmt.Errorf("failed to create signer from raw private key %q: %s", rawPk, err)
	}

	ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner)
	if err != nil {
		return nil, fmt.Errorf("failed to create cert signer %q: %s", usigner, err)
	}

	return ssh.PublicKeys(ucertSigner), nil
}

func readPrivateKey(pk string) (ssh.AuthMethod, error) {
	// We parse the private key on our own first so that we can
	// show a nicer error if the private key has a password.
	block, _ := pem.Decode([]byte(pk))
	if block == nil {
		return nil, errors.New("Failed to read ssh private key: no key found")
	}
	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
		return nil, errors.New(
			"Failed to read ssh private key: password protected keys are\n" +
				"not supported. Please decrypt the key prior to use.")
	}

View on GitHub (pinned to d32a084675)