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.

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)

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.