getsops/sops · error

transit backend is empty

Error message

transit backend is empty

What it means

Returned by encryptedKeyFromSecret when the Vault transit encrypt call returns a nil secret or a secret with nil Data — i.e. the transit backend returned an empty response. It is wrapped by the EncryptContext 'failed to encrypt...' error, so developers usually see it nested under that message.

Solutions

  1. Verify the engine type at the path: `vault secrets list -detailed` and confirm it is `transit`.
  2. Test encryption manually with the Vault CLI and confirm a `ciphertext` field is returned.
  3. Check Vault server version and any proxy in front of it for responses with empty bodies.
  4. Recreate the key/mount (`vault secrets enable transit; vault write -f transit/keys/<name>`) and retry.

Example fix

// before: engine path wrong — hits an empty/KV mount
hc_vault: https://vault.example.com/v1/myengine/keys/sops
// after: real transit mount
hc_vault: https://vault.example.com/v1/transit/keys/sops
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: confirm a transit engine with the key exists before encrypting
mounts, err := client.Sys().ListMounts()
if err != nil { return err }
m, ok := mounts[enginePath+"/"]
if !ok || m.Type != "transit" {
	return fmt.Errorf("engine %s is not mounted as transit", enginePath)
}
if _, err := client.Logical().Read(enginePath + "/keys/" + keyName); err != nil {
	return fmt.Errorf("transit key %s not found: %w", keyName, err)
}

Type guard

// Go: narrow the secret before use
func secretHasData(s *api.Secret) bool {
	return s != nil && s.Data != nil && len(s.Data) > 0
}
if !secretHasData(secret) {
	return fmt.Errorf("vault returned an empty secret for %s", fullPath)
}

Try / catch

if err := key.EncryptContext(ctx, dataKey); err != nil {
	if strings.Contains(err.Error(), "transit backend is empty") {
		return fmt.Errorf("vault returned an empty response; check %s is a transit mount and the server version is supported", fullPath)
	}
	return err
}

Prevention

When it happens

Trigger: EncryptContext calls encryptedKeyFromSecret(secret) after a successful write, but secret == nil or secret.Data == nil, producing 'transit backend is empty'. Callers: EncryptContext, TestMasterKey_Decrypt, and an anonymous test function.

Common situations: The configured path is not a transit mount (writes succeed silently elsewhere); an intermediary (proxy, older Vault version, bug) returns a 200 with an empty body; misconfigured namespace causing an empty response.

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/1c55dcb43a49b6f0. Report an issue: GitHub.

Appendix: source

Thrown at hcvault/keysource.go:353

// decryptPath returns the path for Decrypt requests.
func (key *MasterKey) decryptPath() string {
	return path.Join(key.EnginePath, "decrypt", key.KeyName)
}

// encryptPayload returns the payload for an encrypt request of the dataKey.
func encryptPayload(dataKey []byte) map[string]interface{} {
	encoded := base64.StdEncoding.EncodeToString(dataKey)
	return map[string]interface{}{
		"plaintext": encoded,
	}
}

// encryptedKeyFromSecret attempts to extract the encrypted key from the data
// of the provided secret.
func encryptedKeyFromSecret(secret *api.Secret) (string, error) {
	if secret == nil || secret.Data == nil {
		return "", fmt.Errorf("transit backend is empty")
	}
	encrypted, ok := secret.Data["ciphertext"]
	if !ok {
		return "", fmt.Errorf("no encrypted data")
	}
	encryptedKey, ok := encrypted.(string)
	if !ok {
		return "", fmt.Errorf("encrypted ciphertext cannot be cast to string")
	}
	return encryptedKey, nil
}

// decryptPayload returns the payload for a decrypt request of the
// encryptedKey.
func decryptPayload(encryptedKey string) map[string]interface{} {
	return map[string]interface{}{
		"ciphertext": encryptedKey,
	}

View on GitHub (pinned to 13442bb981)