gastownhall/beads · error
identity: reply authentication failed
Error message
identity: reply authentication failed
What it means
VerifyIdentReply computes the expected MAC (SignIdentReply over the canonical reply, secret, and nonce) and compares it to the reply's MAC using hmac.Equal. This error means both MACs decoded as hex but differ — the reply was tampered with, or was signed with a different secret or nonce than the request used.
Source
Thrown at internal/storage/dbproxy/identity/control.go:116
return reply, nil
}
// VerifyIdentReply verifies the authenticated reply for the request nonce.
func VerifyIdentReply(reply IdentReply, secret, nonce string) error {
got, err := hex.DecodeString(reply.MAC)
if err != nil {
return errors.New("identity: invalid reply MAC")
}
signed, err := SignIdentReply(reply, secret, nonce)
if err != nil {
return fmt.Errorf("identity: authenticate reply: %w", err)
}
want, err := hex.DecodeString(signed.MAC)
if err != nil {
return fmt.Errorf("identity: decode expected reply MAC: %w", err)
}
if !hmac.Equal(got, want) {
return errors.New("identity: reply authentication failed")
}
return nil
}
func decodeIdentNonce(nonce string) ([]byte, error) {
raw, err := hex.DecodeString(nonce)
if err != nil || len(raw) != identNonceBytes {
return nil, errors.New("identity: invalid request nonce")
}
return raw, nil
}
func canonicalIdentReply(reply IdentReply) ([]byte, error) {
payload := struct {
Schema int `json:"schema"`
Role string `json:"role"`
RootID string `json:"root_id"`
UpstreamID string `json:"upstream_id"`View on GitHub (pinned to 71377f2769)
Solutions
- Confirm both sides read the same proxy secret (re-run secret rotation atomically and restart the proxy)
- Always generate a fresh nonce per Identify call and verify the reply for that exact nonce
- Treat as an authentication failure: reject the peer and do not retry with the same connection
- Check for stale proxy processes still running with an old secret after rotation
Example fix
// before
if err := identity.VerifyIdentReply(reply, secret, nonce); err != nil { return err }
// after
if err := identity.VerifyIdentReply(reply, freshNonce(), secret); err != nil {
if strings.Contains(err.Error(), "authentication failed") { rotateSecret(); return ErrUntrustedPeer }
return err
} Defensive patterns
Strategy: type-guard
Type guard
func isAuthFailed(err error) bool { return strings.Contains(err.Error(), "reply authentication failed") } Try / catch
if err := identity.VerifyIdentReply(reply, secret, nonce); err != nil {
if isAuthFailed(err) { rotateSecretAndRedial(); return ErrUntrustedPeer }
return err
} Prevention
- Rotate the proxy secret atomically and restart both sides together
- Generate a fresh nonce for every Identify call
- Never replay captured replies across handshakes
- Reject peers immediately on MAC mismatch — do not retry the same connection
When it happens
Trigger: Calling VerifyIdentReply on a reply whose MAC decodes cleanly but does not match: wrong shared secret on the proxy side, a stale/reused nonce, mutated reply fields (role/root_id changed in transit), or a replay from a previous handshake.
Common situations: Secret rotation where the proxy still holds the old secret; a man-in-the-middle or rogue socket peer altering reply contents; reusing a captured reply for a new nonce; config drift between beads and its managed proxy.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- identity: invalid reply MAC
- identity: oversized reply
- ErrMissingBirth
- authenticated proxy identity does not match its pidfile or w
- --allowed-host is empty; pass the Host header value clients
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0be5d215d4d8ae2c.
Report an issue: GitHub.