slackhq/nebula · critical
invalid passphrase or corrupt private key
Error message
invalid passphrase or corrupt private key
What it means
aes256Decrypt derives the key from the passphrase with Argon2id and opens the AES-256-GCM ciphertext; gcm.Open fails when authentication fails. The library collapses any authentication failure into this message, meaning the passphrase is wrong or the ciphertext/metadata was altered or truncated. It deliberately does not distinguish wrong passphrase from corruption.
Source
Thrown at cert/crypto.go:105
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce, ciphertext, err := splitNonceCiphertext(data, gcm.NonceSize())
if err != nil {
return nil, err
}
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("invalid passphrase or corrupt private key")
}
return plaintext, nil
}
func aes256DeriveKey(passphrase []byte, params *Argon2Parameters) ([]byte, error) {
if params.salt == nil {
params.salt = make([]byte, 32)
if _, err := rand.Read(params.salt); err != nil {
return nil, err
}
}
// keySize of 32 bytes will result in AES-256 encryption
key, err := deriveKey(passphrase, 32, params)
if err != nil {
return nil, err
}View on GitHub (pinned to dd8f660c0a)
Solutions
- Re-enter the exact passphrase used when the key was encrypted ( EncryptAndMarshalSigningPrivateKey ).
- Re-encrypt the private key with the new passphrase if the original is lost — the old ciphertext cannot be recovered.
- Verify the encrypted key file is byte-identical to the original (no edits, CRLF conversion, or truncation).
- Ensure the Argon2 parameters stored in the file metadata were not altered.
Example fix
// before
key, _, err := cert.DecryptAndUnmarshalSigningPrivateKey([]byte(os.Getenv("NEBULA_PASS")), pemBytes) // wrong env var value
// after
pass := []byte(os.Getenv("NEBULA_KEY_PASSWORD"))
if len(pass) == 0 {
return fmt.Errorf("NEBULA_KEY_PASSWORD not set")
}
key, _, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, pemBytes) Defensive patterns
Strategy: try-catch
Validate before calling
// cannot be validated beforehand; verify passphrase is present and non-empty
pass := []byte(os.Getenv("NEBULA_KEY_PASSWORD"))
if len(pass) == 0 {
return fmt.Errorf("passphrase must be provided to decrypt host key")
} Try / catch
key, groups, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, pemBytes)
if err != nil {
if err.Error() == "invalid passphrase or corrupt private key" {
return fmt.Errorf("wrong passphrase for encrypted host key (or file corrupted); re-check NEBULA_KEY_PASSWORD")
}
return err
} Prevention
- Store the passphrase in a secret manager, not hand-typed config.
- Compare checksums of encrypted key files after transfer to rule out corruption.
- Keep a tested backup of the passphrase before rotating.
- Re-encrypt the key with EncryptAndMarshalSigningPrivateKey after passphrase changes.
When it happens
Trigger: Calling DecryptAndUnmarshalSigningPrivateKey with a passphrase that differs from the one used at encryption, or with ciphertext bytes that were modified/truncated (splitNonceCiphertext passed but GCM tag mismatched).
Common situations: Typo or changed passphrase in config (key.password / encrypted_host_key), encrypted key file edited or copied incorrectly, different Argon2 parameters than used at encryption time, environment variable interpolation altering the passphrase.
Related errors
- error while encrypting out-key: %s
- ca-key is encrypted and must be decrypted interactively
- no cipher state available to encrypt
- no cipher state available to encrypt
- invalid AES-256 key length (%d) - cowardly refusing to encry
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/7e1b55701a151359.
Report an issue: GitHub.