Skip to content

IP Restriction (allowed_ips)

allowed_ips is a per-reseller comma-separated allowlist that restricts where the reseller can log in from and where their API calls can come from. If set, every login attempt and every authenticated request is checked: if the client IP isn’t in the list, the request is rejected with 403 “Access denied from this IP address”.

This is the simplest tool ProxPanel has for hardening a high-trust account like a partner / read-only role. It’s also the easiest to get wrong, because client IPs aren’t always what you’d expect.

Admin only. From the Resellers page:

  • Resellers → click reseller → edit form → Allowed IPs field.
  • Or directly: /resellers/{id}/edit.

The field is part of the admin-only field whitelist on the reseller update endpoint — a reseller cannot change their own allowed_ips, only an admin can.

The field accepts a comma-separated list. Each entry is either:

FormatExampleMatches
Single IPv4203.0.113.42Exactly this address.
IPv4 CIDR203.0.113.0/24Any address in the /24 range.
Single IPv62001:db8::1Exactly this address.
IPv6 CIDR2001:db8::/32Any address in the /32 range.

The list is OR’d — if any entry matches, the request is allowed. Whitespace around entries is trimmed.

Empty field = no restriction (the default). All IPs are allowed.

The check fires on every authenticated request, not just login. Specifically:

TriggerWhat happens
POST /auth/loginIf credentials are valid but IP isn’t allowed → 403, no token issued.
Any authenticated API callMiddleware re-checks every request → 403 if IP isn’t allowed. Existing JWT is not enough.
Customer portal (subscriber login)The check applies only to reseller-type users. Subscribers (user_type=1) have no IP restriction.

This means rotating a reseller’s IP allowlist takes effect immediately — there’s no need to wait for sessions to expire. A reseller whose IP just got removed from the list is logged out on their next API call.

Since v1.0.548 the check was widened from “resellers only” to all non-admin user types — so support staff, collectors, and read-only accounts can also be IP-restricted using their owning reseller’s allowed_ips. Admins are still exempt (locking out the only admin would be unrecoverable).

This is where most setups go wrong. The check looks at the HTTP client IP, which is the public IP your browser appears to come from — not the IP the panel sees on its private network and not the PPPoE IP your customers see.

SourceWhat gets sent to the APIWhat allowed_ips should contain
Browser at the officeThe office’s public NAT egress IP, e.g. 203.0.113.42203.0.113.42
Browser on home WiFiThe home ISP’s public IP (often dynamic)A wide CIDR, or the customer’s static IP
Browser through a VPNThe VPN exit node’s public IPThe VPN endpoint IP
Mobile app on cell networkThe carrier’s public NAT IP (often shared across thousands of users)A CIDR of the carrier’s range — or don’t restrict
Reseller logging in from inside a PPPoE subscriber sessionThe public IP of the NAS, not the PPPoE Framed-IP — because the request egresses via NATThe NAS’s public IP, not 10.180.96.x

The easiest test: from the network you want to allowlist, open https://api.ipify.org/ in a browser. Whatever IP it returns is what the panel will see in c.IP() from that network.

For internal testing on the panel host itself, set X-Real-IP or check the nginx access log:

Terminal window
docker logs proxpanel-api 2>&1 | grep "your-username" | tail

The IP in the log entry is what the middleware checks against.

The middleware calls IsIPInList(reseller.AllowedIPs, c.IP()). The function:

  1. Returns true immediately if the allowlist is empty.
  2. Splits the list by comma, trims each entry.
  3. For each entry: parses as a CIDR if it contains /, otherwise as a single IP.
  4. Returns true on first match. Returns false if no entry matches.

The check is O(n) in the number of entries. Lists with more than ~50 entries should use a CIDR instead.

  1. From the partner’s office, browse to https://api.ipify.org/. Note the IP, e.g. 203.0.113.42.
  2. Resellers → partner row → edit → Allowed IPs = 203.0.113.42. Save.
  3. From the partner’s office: log in. Success.
  4. From your phone hotspot: log in. 403 “Access denied from this IP address”. Restriction confirmed.
  1. Get the office’s public IP: 203.0.113.42.
  2. Get the home VPN’s public IP: 198.51.100.10.
  3. Allowed IPs = 203.0.113.42, 198.51.100.10. Save.
  4. Verify from both networks.

Wider net — allow any address from a known ISP

Section titled “Wider net — allow any address from a known ISP”
  1. Find the ISP’s IP range. For a small ISP, this might be 203.0.113.0/24.
  2. Allowed IPs = 203.0.113.0/24. Save.
  3. Anyone on that ISP’s network can log in. Anyone outside cannot.
  4. Note: this is less secure than pinning to one IP, but useful for resellers who roam within a known network.

A reseller who locks themselves out cannot fix it themselves — they can’t log in to edit the field. The recovery path is admin-only:

  1. Admin logs in (admins are exempt from allowed_ips).
  2. Resellers → the locked-out reseller → edit → clear or widen the Allowed IPs field. Save.
  3. The reseller can log in immediately.
  • Cloudflare / load balancers in front of the panel rewrite the source IP. The panel reads X-Real-IP (set by the bundled nginx) — make sure your upstream proxy sets that header correctly, or the check will see the proxy’s IP instead of the real client.
  • IPv6. If the panel is reachable on IPv6 and a reseller’s browser prefers IPv6, the client IP will be an IPv6 address. The allowlist must include the IPv6 form too.
  • Dynamic IPs. Many home ISPs rotate IPs every 24h. Locking down by IP only works if the network you’re allowlisting has a static IP — usually office, datacenter, or a VPN endpoint.
  • WireGuard / custom VPN. A nice pattern is to issue the reseller a WireGuard config — their traffic always egresses from your VPN endpoint, so you allowlist that one IP.
PermissionEffect
Admin-only fieldOnly admins can edit allowed_ips. Resellers cannot edit their own (or anyone else’s) value.
resellers.editRequired to save changes (admin already has it).