JuliusBrussee/caveman · error

listen address is not loopback; standalone proxy has no…

Error message

listen address %q is not loopback; standalone proxy has no inbound authentication; set CAVEMAN_AUTH_TOKEN to expose the proxy beyond loopback

What it means

The listen host parses as a non-loopback IP (or fails to parse as an IP at all) while CAVEMAN_AUTH_TOKEN is unset, so the standalone proxy would be exposed with no inbound authentication. validateListen blocks this unless an auth token is configured, turning a wide bind into a deliberate operator choice. Note: when authenticated is true, a non-loopback bind is allowed and the host is also not required to be a parseable IP.

Solutions

  1. Set CAVEMAN_AUTH_TOKEN to a strong token (≥ minAuthTokenBytes, no spaces/control chars) to deliberately allow the wide bind.
  2. Otherwise change listen to a loopback address: "127.0.0.1:8080", "localhost:8080", or "[::1]:8080".
  3. If remote access is needed but not authentication, front the proxy with a reverse proxy that enforces auth and keep this proxy loopback-only.

Example fix

// before
listen = "0.0.0.0:8080"   # no CAVEMAN_AUTH_TOKEN
// after
listen = "127.0.0.1:8080"
// or, deliberately exposed:
// listen = "0.0.0.0:8080" + CAVEMAN_AUTH_TOKEN=<strong secret>
Defensive patterns

Strategy: validation

Validate before calling

host, _, err := net.SplitHostPort(strings.TrimSpace(listen))
if err == nil && host != "localhost" {
    ip := net.ParseIP(host)
    if (ip == nil || !ip.IsLoopback()) && os.Getenv("CAVEMAN_AUTH_TOKEN") == "" {
        return fmt.Errorf("non-loopback listen %q requires CAVEMAN_AUTH_TOKEN", listen)
    }
}

Prevention

When it happens

Trigger: Setting listen to "0.0.0.0:8080", "192.168.1.10:8080", or a hostname that is not "localhost" and not a parseable loopback IP, without CAVEMAN_AUTH_TOKEN set, then calling Load.

Common situations: Trying to reach the proxy from another machine or container on the LAN; Docker setups binding a wide interface; hostnames like "proxy.local" that net.ParseIP rejects.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/8da6d7ecd13400ec. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/config/config.go:327

// inbound credential gates it. Binding an empty, wildcard, or non-loopback host
// would expose every configured provider credential to the network with no
// inbound authentication; authenticated says CAVEMAN_AUTH_TOKEN is set, so
// standalone.Auth rejects every request that does not present it and the wider
// bind becomes a deliberate operator choice instead of an accident.
func validateListen(listen string, authenticated bool) error {
	host, port, err := net.SplitHostPort(strings.TrimSpace(listen))
	if err != nil || port == "" {
		return fmt.Errorf("listen address %q must be loopback host:port", listen)
	}
	if strings.EqualFold(host, "localhost") {
		return nil
	}
	ip := net.ParseIP(host)
	if ip == nil || !ip.IsLoopback() {
		if authenticated {
			return nil
		}
		return fmt.Errorf("listen address %q is not loopback; standalone proxy has no inbound authentication; set CAVEMAN_AUTH_TOKEN to expose the proxy beyond loopback", listen)
	}
	return nil
}

func (c Config) withDefaults() Config {
	if label := env.String("CAVEMAN_LABEL", ""); label != "" {
		c.Label = label
	}
	if c.Label == "" {
		c.Label = "local"
	}
	if mode := env.String("CAVEMAN_MODE", ""); mode != "" {
		c.Mode = mode
	}
	if listen := env.String("CAVEMAN_LISTEN", ""); listen != "" {
		c.Listen = listen
	}
	// Assigned unconditionally: the environment is the ONLY source for this

View on GitHub (pinned to 3ee70a1026)