k3s-io/k3s · error

invalid cipher text, not : delimited

Error message

invalid cipher text, not : delimited

What it means

decrypt() requires ciphertext in the exact form produced by encrypt(): '<salt>:<base64 AES-GCM sealed data>'. SplitN on ':' yields fewer than 2 parts when the value contains no colon, so the stored bytes are not ciphertext from this scheme and cannot be decrypted with any passphrase.

Source

Thrown at pkg/cluster/encrypt.go:57

		return nil, err
	}

	nonce := make([]byte, gcm.NonceSize())
	_, err = io.ReadFull(rand.Reader, nonce)
	if err != nil {
		return nil, err
	}

	sealed := gcm.Seal(nonce, nonce, plaintext, nil)
	return []byte(salt + ":" + base64.StdEncoding.EncodeToString(sealed)), nil
}

// decrypt attempts to decrypt the byte slice using the supplied passphrase.
// The input byte slice should be the ciphertext output from the encrypt function.
func decrypt(passphrase string, ciphertext []byte) ([]byte, error) {
	parts := strings.SplitN(string(ciphertext), ":", 2)
	if len(parts) != 2 {
		return nil, errors.New("invalid cipher text, not : delimited")
	}

	clearKey := pbkdf2.Key([]byte(passphrase), []byte(parts[0]), 4096, 32, sha1.New)
	key, err := aes.NewCipher(clearKey)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(key)
	if err != nil {
		return nil, err
	}

	data, err := base64.StdEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to 6ba341e396)

Solutions

  1. Confirm the value stored under the token-derived bootstrap key still contains a colon: etcdctl get <bootstrap-key> and inspect; if not, it is corrupt.
  2. Restore the datastore from a known-good etcd snapshot taken while the cluster was healthy.
  3. If no good snapshot exists, back up and delete the db directory and reinitialize the cluster (--cluster-init), then rejoin peers.
  4. Verify you are decrypting with the matching token only after the format is confirmed; a wrong token fails later in gcm.Open, not here.
Defensive patterns

Strategy: validation

Validate before calling

// Mirror the format check decrypt() performs, before calling it:
func isEncryptedBootstrap(v []byte) bool {
	parts := strings.SplitN(string(v), ":", 2)
	return len(parts) == 2 && parts[0] != "" && parts[1] != ""
}
if !isEncryptedBootstrap(kv.Value) { /* corrupt: restore from snapshot */ }

Try / catch

if _, err := decrypt(token, kv.Value); err != nil {
	if strings.Contains(err.Error(), "not : delimited") {
		// value corrupt/foreign: restore datastore from snapshot, do not retry with other tokens
	} else {
		// wrong token (gcm.Open auth failure) or transient error
	}
}

Prevention

When it happens

Trigger: The '/bootstrap' key value was corrupted, truncated, base64-decoded, or overwritten with plaintext/manual bytes; a datastore restore wrote the wrong bytes; code passed an arbitrary buffer (e.g., an unencrypted value from a foreign tool) into decrypt.

Common situations: Datastore restored from a partially corrupt backup; someone hand-edited the etcd key; version mix where an older format wrote values without the salt prefix.

Related errors


AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15). Data as JSON: /api/errors/dbaa42222fda2bc1. Report an issue: GitHub.