OpenNHP/opennhp · error
peer not found in peer pool
Error message
peer not found in peer pool (type=%s, pubkey=%s)
What it means
validatePeer successfully decrypted the initiator's static public key from the header, but a lookup of that key in the device's peer pool found no registered peer (and no PeerLookupFallback accepted it). The device refuses to process messages from unknown public keys — this is the core Zero Trust allowlist behavior. The error carries the expected peer device type (derived from the message type, e.g. agent for KNK, AC for ART) and the base64 public key.
Solutions
- Add the sender's public key to the receiver's peer table (server.toml/resource.toml peer entries) and redeploy configs from deploy/config-templates.
- If keys were rotated (--regenerate), redeploy peer tables to ALL nodes in lockstep.
- For agents on the AC/DB, wire option.PeerLookupFallback so dynamically registered (NHP_REG) agents are accepted.
- Confirm the agent actually completed registration (NHP_REG/NHP_OTP) before sending operational messages.
- Check the logged pubkey against the sender's actual key file — a stale agent key file is the usual mismatch.
Example fix
// before (ac etc/server.toml) — peer missing [[peer]] # only ac and relay listed, agent key absent // after [[peer]] deviceType = 1 # NHP_AGENT pubKey = "<base64 of nhp_agent_public_key>"
Defensive patterns
Strategy: fallback
Validate before calling
pk, _ := base64.StdEncoding.DecodeString(agentPubKeyB64)
if dev.LookupPeer(pk) == nil && dev.Option.PeerLookupFallback == nil {
return fmt.Errorf("peer %s is not registered on this node; register or add to peer table first", agentPubKeyB64)
} Type guard
func peerKnown(dev *core.Device, pk []byte) bool {
return dev.LookupPeer(pk) != nil
} Try / catch
err := client.SendKnock(server)
if err != nil && strings.Contains(err.Error(), "peer not found in peer pool") {
// re-register then retry once
if rerr := client.Register(); rerr == nil {
err = client.SendKnock(server)
}
} Prevention
- Deploy peer tables to every node from the same key secret after any rotation.
- Run generate-nhp-keys.sh without --regenerate unless a full lockstep redeploy is planned.
- On AC/DB, wire PeerLookupFallback so NHP_REG-registered agents are accepted.
- Verify agents complete registration before sending operational messages.
- Log and alert on 'peer not found' spikes — they usually mean config drift.
When it happens
Trigger: An agent sends KNK/ACC without ever having been registered (no NHP_REG/OTP) or after its entry was removed; a server/AC/relay sends a message but its public key is absent from the receiver's peer table (config.toml/server.toml peers not deployed); keys rotated with --regenerate so the pool has old keys; DisableXPeerValidation is false but no PeerLookupFallback is configured on the AC/DB for dynamically registered agents.
Common situations: Deploying only some hosts' peer tables after generate-nhp-keys.sh; registering an agent against server A but knocking server B; SQLite-registered agent missing because PeerLookupFallback wasn't wired; peer deleted by expiry/cleanup on the receiving node.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/f743bc9e7402aa46.
Report an issue: GitHub.
Appendix: source
Thrown at nhp/core/responder.go:504
peerDeviceTypeName := DeviceTypeToString(peerDeviceType)
log.Debug("validatePeer: looking up %s peer pubkey=%s in peer pool", peerDeviceTypeName, peerPkBase64)
peer = ppd.device.LookupPeer(peerPk)
if peer == nil {
// Fallback: check dynamically-registered peers (e.g., agents
// registered via NHP-REG stored in SQLite).
ppd.device.optionMutex.Lock()
fallback := ppd.device.option.PeerLookupFallback
ppd.device.optionMutex.Unlock()
if fallback != nil && fallback(peerPk, ppd.HeaderType) {
log.Info("validatePeer: %s peer accepted via fallback, pubkey=%s",
peerDeviceTypeName, peerPkBase64)
// Skip expiry/address checks for fallback peers.
goto peerAccepted
}
log.Error("validatePeer: %s peer not found in peer pool, pubkey=%s",
peerDeviceTypeName, peerPkBase64)
err = fmt.Errorf("peer not found in peer pool (type=%s, pubkey=%s)", peerDeviceTypeName, peerPkBase64)
return err
}
if peer.IsExpired() {
log.Error("validatePeer: %s peer expired, pubkey=%s", peerDeviceTypeName, peerPkBase64)
err = fmt.Errorf("peer expired (type=%s, pubkey=%s)", peerDeviceTypeName, peerPkBase64)
return err
}
if !ppd.ConnData.CheckRecvAddress(ppd.LocalInitTime, ppd.ConnData.RemoteAddr) {
log.Error("validatePeer: %s peer address mismatch on connection, pubkey=%s, remoteAddr=%s",
peerDeviceTypeName, peerPkBase64, ppd.ConnData.RemoteAddr)
err = fmt.Errorf("peer does not match its previous address on this connection (type=%s, pubkey=%s)", peerDeviceTypeName, peerPkBase64)
return err
}
ppd.ConnData.UpdateRecvAddress(ppd.LocalInitTime, ppd.ConnData.RemoteAddr)
peer.UpdateRecv(ppd.LocalInitTime)
peerAccepted:View on GitHub (pinned to 6e04ca5ff0)