gastownhall/beads · error

identity: invalid proxy secret

Error message

identity: invalid proxy secret

What it means

ReadSecret loads the proxy secret file and requires the trimmed content to be exactly 64 characters — a 32-byte secret hex-encoded. This error means the file exists but its length is wrong: it was truncated, empty, or written in a different format (raw bytes, base64, extra content).

Source

Thrown at internal/storage/dbproxy/identity/identity.go:62

	if _, err := rand.Read(raw); err != nil {
		return "", fmt.Errorf("identity: generate proxy secret: %w", err)
	}
	secret := hex.EncodeToString(raw)
	if err := atomicfile.WriteFile(filepath.Join(rootDir, SecretFileName), []byte(secret+"\n"), 0o600); err != nil {
		return "", fmt.Errorf("identity: write proxy secret: %w", err)
	}
	return secret, nil
}

// ReadSecret reads and validates the control-listener secret.
func ReadSecret(rootDir string) (string, error) {
	data, err := os.ReadFile(filepath.Join(rootDir, SecretFileName)) // #nosec G304 - rootDir is the workspace proxy root, not user input
	if err != nil {
		return "", fmt.Errorf("identity: read proxy secret: %w", err)
	}
	secret := strings.TrimSpace(string(data))
	if len(secret) != 64 {
		return "", errors.New("identity: invalid proxy secret")
	}
	if _, err := hex.DecodeString(secret); err != nil {
		return "", errors.New("identity: invalid proxy secret")
	}
	return secret, nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Regenerate the secret with the package's WriteSecret/Rotate helper and restart the proxy
  2. Check file size: expect 64 bytes (+optional newline); restore from backup if truncated
  3. Ensure no two processes rotate the secret file concurrently (atomic rename, not in-place write)
  4. Verify no provisioning script writes the secret in base64 or raw-byte form

Example fix

// before
// hand-written secret
os.WriteFile(secretPath, []byte(base64.StdEncoding.EncodeToString(key)), 0600)
// after
// hex-encode to exactly 64 chars, write atomically
hexSecret := hex.EncodeToString(key) // 64 chars
os.WriteFile(secretPath+".tmp", []byte(hexSecret+"\n"), 0600)
os.Rename(secretPath+".tmp", secretPath)
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(secretPath); if err != nil || info.Size() < 64 { return ErrSecretMissing }

Type guard

func isInvalidSecretLen(err error) bool { return strings.Contains(err.Error(), "invalid proxy secret") }

Try / catch

secret, err := identity.ReadSecret(root)
if err != nil {
    if isInvalidSecretLen(err) { return regenerateAndRotateSecret(root) }
    return err
}

Prevention

When it happens

Trigger: Calling ReadSecret when SecretFileName under rootDir contains content whose trimmed length is not 64 — empty file, partial write during creation/rotation, or a secret written by a different tool/version in raw or base64 form.

Common situations: Crash or concurrent rotation leaving a partially written secret file; manually editing the secret file; provisioning tooling writing base64 instead of hex; disk-full truncation.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/cdef28bc4ccd843c. Report an issue: GitHub.