IP whitelisting vs username password proxy auth: what each checks, where whitelists break in Docker, Kubernetes, CI and serverless, and what 407 means.
· 16 min read · Authentication
A proxy has to answer one question before it forwards a byte: is this client allowed. There are two ways to answer it. IP whitelisting vs username password proxy authentication is not a security debate — both models are fine — it is an operational one, and the operational answer depends entirely on whether your egress address is a fact about your infrastructure or an accident of it. Most comparison articles stop at "whitelisting is convenient, credentials are portable." That is true and useless. The interesting part is the list of environments where a whitelist quietly stops being a stable identifier, and what you give up when you drop the username.
| IP whitelisting | Username and password | |
|---|---|---|
| What the gateway checks | Source address of the TCP connection | Credentials presented in the connection setup |
| Where the secret lives | Nowhere — the network path is the secret | Config, env var, secret manager, or the proxy URL |
| Per-request overhead | None | One Proxy-Authorization header, or the SOCKS5 auth sub-negotiation |
| Breaks when | Your egress IP changes | Credentials rotate, leak, or are revoked |
| Revocation | Remove the address from the list | Change the password |
| Works from a laptop on hotel Wi-Fi | No | Yes |
| Works from an autoscaling node pool | Only with pinned egress | Yes |
| Carries targeting parameters | No | Yes — encoded in the username |
| Blast radius if compromised | Anyone sharing your egress IP | Anyone holding the string |
The row that decides most real deployments is the last-but-one, and almost nobody writes about it. Hold that thought until the targeting section.
The mechanics differ by protocol. Over HTTP, credentials ride in a
Proxy-Authorization: Basic header on the request or on the CONNECT, base64
of user:pass, defined in
RFC 7235. Over SOCKS5 they go
in the username/password sub-negotiation from
RFC 1929, a separate exchange
before the connect request. Same credentials, different frame. On the AnonEdge
gateway that is gw.anonedge.com port 823 for HTTP and HTTPS, port 824 for
SOCKS5:
curl -x "http://login:[email protected]:823" https://api.ipify.org/
curl -x "socks5h://login:[email protected]:824" https://api.ipify.org/
Under a whitelist, the same commands lose the login:password@ and nothing
else changes. AnonEdge ships both. Its getting-started documentation lists two
authentication methods — "Username/Password: Use the credentials provided in
your dashboard" and "IP Allowlisting: Add your server's IP to allowlist and
connect without credentials" — and the SimpleProxy walkthrough repeats the same
rule: if your IP is allowlisted, credentials are not required. See your first
authenticated request.
One note on vocabulary before going further. AnonEdge's documentation calls this allowlisting. The search term, and the key name in most tooling, is whitelist. Same mechanism, two words. This page says "whitelisting" for the general model and "allowlist" where it refers to what AnonEdge actually ships.
No credentials in code, config, or logs. There is no string to leak, because there is no string. Every leakage vector in the next-but-one section simply does not exist. For a fixed-egress production box that is a real reduction in attack surface, not a theoretical one.
Clients that cannot send proxy auth at all. This is the underrated case.
Plenty of tooling speaks SOCKS5 but has no way to pass credentials, and plenty
more speaks HTTP but drops Proxy-Authorization on redirects or on the
CONNECT. Playwright documents optional username and password "for HTTP(S)
proxy" specifically, and SOCKS5 proxy authentication has been an open feature
request against the project since 2021
(playwright#10567).
On that stack, a whitelist is not a preference — it is the only way to use
SOCKS5 at all; Playwright and authenticated
SOCKS5 covers what that costs you
per browser context.
Zero per-connection auth work. Nothing to parse, nothing to compare, no credential cache to invalidate. At high connection churn this is a small win that compounds.
Nothing to rotate. No 90-day password policy, no secret-manager entry, no rollout window. The whitelist changes when your infrastructure changes, which is the correct coupling for infrastructure that does not change.
This is the section the search results name and then never write. Every environment below shares one property: the address the gateway sees is produced by something other than the machine running your code.
A consumer or small-office connection gets its address from the ISP, on the ISP's schedule. A router reboot, a lease expiry, a line fault, a maintenance window — any of them can hand you a new address with no notification. Your job starts failing at 03:00 for a reason that has nothing to do with your code.
Carrier-grade NAT makes it worse in the other direction. Under CGNAT your "public" address is shared with a large number of other subscribers, so whitelisting it authorizes all of them to use your proxy account. The address is simultaneously unstable and over-broad. Mobile networks are almost universally CGNAT, so a whitelist plus a phone tether is the worst combination available.
On the default bridge network, a container does not have a public address. Its packets are source-NATed to the host's address by an iptables masquerade rule on the way out. The gateway sees the Docker host, not the container. That is usually what people expect.
What people do not expect is that everything around it also changes. Switch to
--network host and the container shares the host's network namespace
directly — same visible address, different behavior for binds and for
loopback. Put the container behind a Swarm overlay network and egress leaves via
whichever node the task is scheduled onto. Run the same image on a developer
laptop, in a build agent, and on a production host and you have three different
egress addresses for one artifact.
There is a second, sharper Docker trap that costs people hours: the Docker
client's proxy settings and the Docker daemon's are different things. The
Docker documentation is explicit that the proxies block in
~/.docker/config.json configures "proxy environment variables for containers
only, and not used as proxy settings for the Docker CLI or the Docker Engine
itself"
(docs.docker.com). Pulling an
image and running one are two separate network paths, and only one of them is
your application. The
full treatment of where proxy settings land in
containers is a topic in its own right.
By default in most clusters a pod's outbound traffic is source-NATed to the node's address. So the gateway sees the node, and your whitelist has to contain every node your workload might land on. Then the cluster autoscaler adds a node at peak, that node gets a new address, and the pods scheduled onto it fail authentication while the identical pods on the old nodes keep working. The symptom is a partial outage that correlates with load, which is exactly the symptom people spend the longest misdiagnosing.
The fix is to stop letting the node decide. Pin egress through a managed NAT with a fixed address — a cloud NAT gateway with a reserved address, or a dedicated egress gateway in the cluster — and whitelist that one address. Now node churn is invisible to the proxy. This works, it is the right answer for fixed-egress production, and it is a piece of infrastructure you now own and have to keep alive.
Hosted CI runners are shared infrastructure. GitHub publishes the address
ranges its Actions runners use under the actions key of the
GET /meta endpoint, and the
documentation is clear that you must query the API for current values rather
than copying them. Those ranges are large, they are shared with every other
customer on the platform, and they change.
Whitelisting them has two problems at once. It is operationally fragile, because the list moves. And it is a weak control, because "any address in GitHub's runner range" authorizes a very large population that is not you. Self-hosted runners behind a fixed NAT solve both, at the cost of running runners.
AWS states plainly that "by default, Lambda functions have access to the public internet," and that attaching a function to a VPC removes that access until you configure the VPC for it — "connecting a function to a public subnet doesn't give it internet access or a public IP address" (Lambda VPC documentation). An unattached function egresses from a provider-managed pool you neither choose nor can enumerate. A VPC-attached function egresses from whatever NAT you put in front of it.
So on serverless, a whitelist is not something you configure on the function. It is something you buy: a NAT gateway with a reserved address, running permanently, for a workload whose whole appeal was not running permanently. That trade is sometimes correct. It should be made deliberately.
Portability. The same string works from a laptop, a container, a runner and a Lambda. Nothing about the network path has to be true.
Per-job credentials. Issue one set per pipeline, per customer, per environment. Now your proxy bill and your proxy logs are attributable, and a compromise is scoped to one job instead of one company.
Instant revocation. Change the password and every holder of the old one stops working immediately, wherever they are. A whitelist can only revoke by address, which is useless when the address is shared.
Targeting. Covered next, because it deserves its own section.
The cost is secret sprawl. Every place that string lives is a place it can escape from, and proxy credentials are unusually prone to escaping because the most convenient way to express them is inside a URL.
http://login:[email protected]:823 is a password in a place designed
for logging. Here is where it actually surfaces.
| Vector | What exposes it |
|---|---|
| Shell history | ~/.bash_history, ~/.zsh_history — the whole curl line, in plaintext, forever |
| Process list | ps auxww shows every argument of every running process to every user on the box |
/proc/<pid>/environ |
Readable by the process owner and by root; survives for the life of the process |
docker inspect |
Prints the container's full Env array, credentials included, to anyone who can reach the socket |
| Image layers | A ENV HTTP_PROXY=http://user:pass@… in a Dockerfile is baked into a layer and travels with the image |
| CI logs | Failure paths that echo the effective config, plus any tool that prints its resolved proxy URL on error |
| Crash reports and APM | Exception payloads routinely include the request URL and the client configuration |
| Redirect handling | Some clients re-send Proxy-Authorization on cross-host redirects; check yours |
Three mitigations, in order of value. Keep the password out of the URL and pass
it through the client's dedicated auth mechanism where one exists — curl -U user:pass -x http://gw.anonedge.com:823, for example, which keeps it out of
the URL but still puts it in ps. Read it from a file or a secret manager at
startup rather than from the environment. And prefix a space before interactive
shell commands that carry it, if your shell honors HISTCONTROL=ignorespace.
None of that helps if the credential is long-lived and widely distributed. Short lifetime and narrow scope beat careful handling.
Here is the argument no comparison article makes, and it is the one that decides the model for most operators.
Gateway proxies encode per-request behavior in the username. Country, state, city, ZIP, ASN, session identity, sometimes rotation policy — all of it travels as structured parameters appended to the login string, because the username is the only free-form field the proxy protocol gives a client to send per connection. That is the mechanism by which one hostname and one port can serve 195 countries.
Remove the username and you remove the channel.
A whitelist-only setup authenticates you, and that is all it can do. There is nowhere to put "give me a Brazilian mobile exit in ASN 28573 and hold it for this session." You are left with whatever the account default is, changed globally in a dashboard rather than per request. For a fleet of workers each collecting a different geography, that is not a workaround away from being usable — it is a different product.
AnonEdge's default targeting — country selection and exclusion, ASN exclusion —
is included in the base rate. The advanced
state, city, ZIP and ASN filters are
billed at 2x the standard rate, and a filter with no matching exits returns
400 NO_RAY rather than silently falling back. Both of those only reach the
gateway if there is a username to carry them. Targeting parameters in the
username works through what that 2x actually
buys, and when it does not.
If your workload is single-geography and static, this costs you nothing. If it is not, the targeting requirement settles the auth question before the security discussion starts.
The correct unit of decision is the job, not the company. Run both.
Whitelist the fixed egress. Production workers behind a pinned NAT gateway, a self-hosted runner, a dedicated collection box. These have one address that changes at the speed of infrastructure change, which is to say rarely and deliberately. No credentials to manage, no rotation calendar.
Use credentials for everything ephemeral or portable. Developer machines, hosted CI, serverless, autoscaling pools, anything that runs somewhere new each time. Issue per-job credentials so the bill and the logs stay attributable.
Use credentials wherever you need targeting, regardless of how stable the egress is. This overrides the rule above. Pool type, rotation and country selection all live alongside the credential — see configuring credentials and pool settings.
The rules that follow from that are short. If the address is produced by infrastructure you control and pay to keep alive, whitelist. If the address is produced by somebody else's scheduler, authenticate. If the request needs per-request geography, authenticate. If the client cannot send credentials, whitelist and accept the loss of targeting.
Where both are available, requiring both is stricter than either — the connection has to come from a known address and present a valid credential. Reach for that on the jobs where a compromise would be expensive, not on everything, because it doubles the ways a deployment can fail.
Credential rotation is the recurring cost of the user:pass model, and it is the step people skip until an incident forces it. Four phases, no downtime.
The overlap window has to be longer than your longest-lived session and longer than your least frequent scheduled job. If a monthly report job holds the only remaining reference, a two-week window guarantees an incident on the 30th.
Sticky sessions extend the window in a way that is easy to underestimate. A session held on a port in the 10000–20000 range occupies that binding for as long as you hold it, and it holds a thread with it. Read thread limits and concurrency before you size the overlap.
407 means under each modelSame status code. Opposite fix. This is the single most common support ticket in proxy operations, and the auth model tells you which half of the problem space to search.
Under user:pass, 407 is a credential problem. The gateway got a
credential and rejected it. Check, in order: the password was actually
substituted and you are not sending the literal string password; special
characters in the credential are percent-encoded in the URL — @ becomes %40
and a raw @ truncates the host; the client is sending
Proxy-Authorization and not Authorization, which are different headers for
different hops; and the credential has not been rotated out from under you by
step 4 above.
Under whitelisting, 407 is an egress-IP problem. The gateway saw an
address it does not recognize. Your code and your credentials are irrelevant.
Find out what address the gateway actually saw before you change anything:
# What does the outside world think you are?
curl -s https://api.ipify.org/
Run that from inside the exact context that is failing — inside the container, inside the CI step, inside the function — not from your terminal. The answer is frequently a node, a NAT gateway, or a provider range you did not know was in the path.
There is a third case that is neither. AnonEdge documents
407 THREADS_EXHAUSTED, which means the concurrent connection limit on your
plan is reached. Nothing is wrong with your credentials or your address; you are
simply asking for more simultaneous connections than the plan allows. Thread
limits are per-plan and increases go through support, which may require KYC.
Sticky sessions make this easier to hit than it looks, because a held session
occupies a thread for its whole life.
Read the response body, not just the status line. The three cases are distinguishable, and treating them as one is how a capacity problem gets diagnosed as a password problem. Start at troubleshooting when the body is ambiguous.
Neither is inherently stronger. A whitelist removes the credential as a leakable object but authorizes everyone who shares your egress address, which under CGNAT or a hosted CI range can be thousands of unrelated parties. Credentials scope access to whoever holds the string, which is precise but leakable. The higher-security answer is both together, applied to the small set of jobs where a compromise would actually be expensive.
Because the gateway never saw the container. On the default bridge network the
container's packets are source-NATed to the Docker host, so the whitelisted
address must be the host's — and on a build agent, an overlay network, or a
different host, that address is different. Run curl -s https://api.ipify.org/
from inside the failing container to see what the gateway is actually
authenticating.
Not per request. Targeting parameters — country, state, city, ZIP, ASN, session identity — are encoded in the proxy username, so removing the username removes the only per-connection channel for them. A whitelist-only setup gets the account default, changed globally rather than per request. If different workers need different geographies simultaneously, you need credentials.
407 Proxy Authentication Required actually mean?It means the proxy refused the connection at the authentication step, and the
cause depends on your model. Under credentials it is a bad, missing or rotated
password, or a special character that was not percent-encoded. Under a whitelist
it means the source address is not registered. On AnonEdge there is a third
case, 407 THREADS_EXHAUSTED, which is a concurrency limit rather than an
authentication failure at all.
Rotate on a schedule you can actually complete without downtime, and immediately on any suspected exposure — a credential in a build log, a public repository, or a shared terminal recording. The mechanics matter more than the interval: issue the new credential first, roll it out, confirm the old one is idle across a full duty cycle including monthly jobs, and revoke last.
Where a provider supports it, yes, and it is the strictest configuration available: the connection must originate from a registered address and present a valid credential. It also doubles the number of things that can break a deployment, so it belongs on high-value jobs rather than as a default. Ask support what your account supports before designing around it.
Start with a username and password, because it works everywhere and it is the only model that carries targeting. Move a job to a whitelist when its egress address becomes something you own rather than something you inherit.
Create credentials at signup, or talk to support about whitelisting if your production egress is already pinned to a fixed address.