tailscale/tailscale · critical

failed to unseal data

Error message

failed to unseal data

What it means

During decryption the TPM successfully unsealed the 32-byte key, but NaCl secretbox.Open rejected the payload: MAC verification failed. The ciphertext or nonce does not match this key, meaning the data is corrupted, was encrypted under a different key, or came from another machine/TPM. The plaintext is unrecoverable from this path.

Source

Thrown at feature/tpm/tpm.go:355

		Data:  sealedData,
	}, nil
}

func unseal(logf logger.Logf, data encryptedData) (*decryptedData, error) {
	if len(data.Nonce) != 24 {
		return nil, fmt.Errorf("nonce should be 24 bytes long, got %d", len(data.Nonce))
	}

	unsealedKey, err := tpmUnseal(logf, data.Key)
	if err != nil {
		return nil, fmt.Errorf("failed to unseal encryption key with TPM: %w", err)
	}
	if len(unsealedKey) != 32 {
		return nil, fmt.Errorf("unsealed key should be 32 bytes long, got %d", len(unsealedKey))
	}
	unsealedData, ok := secretbox.Open(nil, data.Data, (*[24]byte)(data.Nonce), (*[32]byte)(unsealedKey))
	if !ok {
		return nil, errors.New("failed to unseal data")
	}

	return &decryptedData{
		Key:  *(*[32]byte)(unsealedKey),
		Data: unsealedData,
	}, nil
}

type tpmSealedData struct {
	Private []byte
	Public  []byte
}

// withSRK runs fn with the loaded Storage Root Key (SRK) handle. The SRK is
// flushed after fn returns.
func withSRK(logf logger.Logf, tpm transport.TPM, fn func(srk tpm2.AuthHandle) error) error {
	srkCmd := tpm2.CreatePrimary{
		PrimaryHandle: tpm2.TPMRHOwner,

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. If the TPM was reset or re-provisioned, recover from the old backend or regenerate, then re-seal with the current TPM
  2. Restore the state from backup if the blob is corrupted
  3. Verify the state directory belongs to this machine and was never cloned from another node
Defensive patterns

Strategy: try-catch

Validate before calling

// cheap integrity pre-check before decrypting
got := sha256.Sum256(blob)
if !bytes.Equal(got[:], expectedSum) {
	return errors.New("state blob checksum mismatch; refusing to decrypt")
}

Try / catch

dd, err := decryptData(logf, data)
if err != nil {
	if strings.Contains(err.Error(), "failed to unseal data") {
		// key/ciphertext mismatch: TPM reset, cloned state, or corruption.
		// Enter recovery flow; do not retry the same input.
		return recoveryMode()
	}
	return err
}

Prevention

When it happens

Trigger: Calling decryptData on a sealed payload where data.Data or data.Nonce was altered or truncated, or where the payload was encrypted under a different 32-byte key (TPM re-provisioned/cleared, state blob copied from another node).

Common situations: Cloning or moving a state directory between machines; TPM firmware update or clear that invalidated the sealing key; disk corruption or partial writes to the state file.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/490dcd56f286f8bd. Report an issue: GitHub.