ory/hydra · error
ip %s is not a permitted destination
Error message
ip %s is not a permitted destination
What it means
IsAssociatedIPAllowed returns this error when the input parses as a bare IP address (netip.ParseAddr) and the allowed() predicate rejects it — i.e. the IP is not a permitted destination (typically because private/loopback/link-local ranges are disallowed as SSRF protection). Raised at ip_validator.go:55.
Source
Thrown at oryx/ipx/ip_validator.go:55
return g.Wait()
}
// IsAssociatedIPAllowed returns nil for a domain (with NS lookup), IP, or IPv6 address if it
// does not resolve to a private IP subnet. This is a first level of defense against
// SSRF attacks by disallowing any domain or IP to resolve to a private network range.
//
// Please keep in mind that validations for domains is valid only when looking up.
// A malicious actor could easily update the DSN record post validation to point
// to an internal IP
func IsAssociatedIPAllowed(ctx context.Context, ipOrHostnameOrURL string) error {
ipOrHostname := ipOrHostnameOrURL
if parsed, err := url.ParseRequestURI(ipOrHostnameOrURL); err == nil {
ipOrHostname = parsed.Hostname()
}
if ip, err := netip.ParseAddr(ipOrHostname); err == nil {
if !allowed(ip) {
return errors.Errorf("ip %s is not a permitted destination", ip)
}
return nil
}
if addr, err := netip.ParseAddrPort(ipOrHostnameOrURL); err == nil {
if !allowed(addr.Addr()) {
return errors.Errorf("ip %s is not a permitted destination", addr.Addr())
}
return nil
}
ctx, cancel := context.WithTimeoutCause(ctx, 2*time.Second, errors.New("DNS lookup timed out"))
defer cancel()
ips, err := resolver.LookupNetIP(ctx, "ip", ipOrHostname)
if err != nil {
if dnsErr, ok := stderrors.AsType[*net.DNSError](err); ok {
// Copy the `*net.DNSError` before masking `Server` to avoid a data
// race: the DNS resolver uses `singleflight` to deduplicateView on GitHub (pinned to 4174065ffb)
Solutions
- Use a public, internet-routable IP or hostname instead of the rejected address
- If the internal address is intentional, adjust the allowed() predicate / disallowed-range configuration to permit it
- In dev, expose the internal service via a public tunnel or the host's external address
Example fix
// before err := ipx.IsAssociatedIPAllowed(ctx, "127.0.0.1:9000") // loopback rejected // after err := ipx.IsAssociatedIPAllowed(ctx, "93.184.216.34:9000") // public IP accepted
Defensive patterns
Strategy: validation
Validate before calling
func isPublicIP(s string) bool {
ip, err := netip.ParseAddr(s)
if err != nil { return false }
return ip.IsGlobalUnicast() && !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
} Try / catch
if err := ipx.IsAssociatedIPAllowed(ctx, input); err != nil {
if strings.Contains(err.Error(), "not a permitted destination") {
return fmt.Errorf("endpoint %s is internal/blocked; use a public address", input)
}
return err
} Prevention
- Check addresses with netip.ParseAddr and IsPrivate/IsLoopback before submitting
- Never configure localhost or cluster-internal IPs for external callbacks
- Understand the SSRF rationale — internal destinations are blocked by design
- For dev environments, use tunnels or the host's external address
When it happens
Trigger: Calling IsAssociatedIPAllowed (directly, via IsAssociatedIPAllowedWhenSet, or via AreAllAssociatedIPsAllowed) with a string like "127.0.0.1", "10.0.0.5", "::1", or any IPv4/IPv6 address that the configured allowed() check rejects.
Common situations: Configuring callbacks, webhooks, or endpoints pointing at localhost/internal cluster IPs; Docker/K8s environments where services resolve to private ranges; SSRF guardrails rejecting internal addresses in submitted URLs.
Related errors
- no route to host
- DNS lookup timed out
- key %s validation is failing
- global secret is too short
- file loader disabled
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/b67ceff26968cc77.
Report an issue: GitHub.