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
- If the TPM was reset or re-provisioned, recover from the old backend or regenerate, then re-seal with the current TPM
- Restore the state from backup if the blob is corrupted
- 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
- Keep a backup of the plaintext or a re-seal path before any TPM maintenance
- Never clone or move TPM-sealed state directories between machines
- Store a checksum next to the sealed blob to distinguish corruption from key mismatch
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
- TPM not supported on this platform
- --encrypt-state is not supported on this device or a TPM is
- --encrypt-state can only be used with --state set to a local
- err
- errorMessageFromBody(all)
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/490dcd56f286f8bd.
Report an issue: GitHub.