Skip to main content

Fixing your IPs

Every credential carries a list of addresses its requests may come from. A request is accepted only if the signature is valid and it arrives from one of those addresses.

accept = valid_signature(credential) AND source_ip ∈ allowed_ips

The list is required: a credential with no source restriction is not issued.

Why the list exists even with signed requests​

Most incidents do not involve a broken signature. They involve a private key that left where it was supposed to stay: a committed repository, a shared file, a developer machine, a third-party tool. Once a key is out, a valid signature can be produced from anywhere, and the traffic looks exactly like your real integration.

The list removes that. A key copied out of your environment stops working, because whoever holds it cannot also reproduce your network origin. It does not replace the signature: the signature proves who produced the request, the list constrains where it may come from.

It is a second layer, not a reason to relax key handling. Keep the private key in an HSM or a secret manager, use a separate credential per environment and per workload, and remember that anything running inside your allowed perimeter inherits its trust.

The rules​

RuleValue
Entries per credential1 to 50
Accepted formatIndividual public IPv4 addresses (203.0.113.10)
Ranges (CIDR)Not accepted
IPv6Not supported yet
DuplicatesCollapsed
OrderCanonical. The order you send does not change the credential
ScopePer credential and per environment; sandbox and production are independent

Ranges are refused on purpose: with a free CIDR, 0.0.0.0/0 would be a valid entry and a required list would restrict nothing. Register each egress address explicitly. The limit of 50 covers multiple regions, failover paths and separate workloads comfortably.

What the list is compared against​

The address of the peer that reaches us, which is where your requests leave from. Not your load balancer, not your public hostname, not the browser you are configuring from. Inbound and outbound addresses are different things and are almost never the same value.

This section is about traffic from you to us. Addresses we use when calling your webhooks are the opposite direction and are configured in your own firewall.

Find your real outbound address​

Run this from inside the workload that will call the API, through the same network path it uses in production, rather than from your laptop:

curl --fail --silent https://ifconfig.me

Repeat it on every instance, task or function that calls us. An address that changes between runs is not fixed, even if the first two runs agree.

Changing the list later​

Editing the allowed IPs replaces the credential with a new signed version that keeps the same key and the same client_id, so your application does not change. The previous version stops being accepted at that moment, including tokens already minted from it, so a client mid-request may see one failure and recover on the next token.

Register the new address before routing traffic through it, and remove the old one only after the new path is confirmed. Overlapping entries cost nothing and avoid a self-inflicted outage.

A rejected request​

A correctly signed request from an address outside the list is refused with 403 and IP_NOT_ALLOWED, before it reaches the resource.

The list is only evaluated after the signature checks out. A request with an invalid signature is rejected as an authentication failure and never reveals whether its address would have been allowed.

Fixed outbound IP by cloud provider​

The pattern is the same everywhere: put the workload in a private subnet with no public address of its own, and route its outbound traffic through a managed NAT component that owns one reserved public address. That address is what you register.

ProviderComponentAddress to reserve
AWSNAT GatewayElastic IP
AzureNAT GatewayStatic public IP (Standard SKU)
Google CloudCloud NAT + Cloud RouterReserved regional external IP
Oracle CloudNAT GatewayReserved public IP

Serverless and managed container runtimes egress through provider-owned pools by default, so their outbound address is not stable. Most can be attached to your own VPC or VNet, and then follow the NAT path above.

# Prerequisites: a VPC with a public subnet routed to an Internet Gateway,
# your workload in a private subnet with no public IP of its own.

export AWS_REGION=<region>

# 1. reserve the address — note AllocationId and PublicIp
aws ec2 allocate-address --domain vpc

# 2. create the NAT Gateway in the PUBLIC subnet, using that address
aws ec2 create-nat-gateway \
--subnet-id <public-subnet-id> \
--allocation-id <allocation-id>

aws ec2 wait nat-gateway-available --nat-gateway-ids <nat-gateway-id>

# 3. send the private subnet's outbound traffic through it
# (use replace-route if a 0.0.0.0/0 route already exists)
aws ec2 create-route \
--route-table-id <private-route-table-id> \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id <nat-gateway-id>

# 4. confirm the reserved address...
aws ec2 describe-addresses --allocation-ids <allocation-id> \
--query 'Addresses[0].PublicIp' --output text

# 5. ...and that the workload actually egresses through it
curl --fail --silent https://ifconfig.me
Two checks, not one

Reading the address from your provider only proves it is reserved. Only the curl from inside the workload proves your traffic actually leaves through it. Both must return the same IPv4.

When you have no fixed address​

Platforms with no VPC attachment, third-party runtimes whose networking you do not control, or links with dynamic addressing: funnel only the traffic destined to our API through a single fixed point you own: a small instance with a reserved public address, in a network you control.

your workload (rotating address)
│
▼
egress proxy (fixed address — the one you register)
│
▼
LB Pay API

Four requirements:

  1. Keep signing in your application. The proxy forwards traffic; it must never hold your private key or re-sign anything.
  2. Do not terminate TLS without a specific reason. Forwarding the encrypted connection keeps the proxy out of scope for request contents, and a proxy that alters the request breaks the signature.
  3. Restrict who can use it. From our side the proxy is a trusted origin, so it must accept connections only from your workloads and forward only to our API.
  4. Make it redundant. One proxy is a single point of failure for every integration behind it. Two instances, each with its own reserved address, both registered.
# TCP passthrough — forwards the encrypted stream untouched.
stream {
resolver 1.1.1.1;

server {
listen 443;
ssl_preread on;
proxy_pass api.sdb.lbpay.com.br:443;
}
}

Not every runtime honors HTTPS_PROXY automatically. Some need the proxy set explicitly in the HTTP client. Confirm with the outbound check, run through the same client your integration uses.

Troubleshooting​

Almost every failure has the same root cause: the address you believe you egress from is not the address we see.

SymptomLikely causeWhat to do
IP_NOT_ALLOWED right after registeringYou registered an inbound address: load balancer, CDN, hostname or your own browserRun the outbound check from inside the workload and compare
IP_NOT_ALLOWED on some requests onlyThe workload egresses through a rotating provider poolRoute egress through a NAT gateway in your own VPC, or a dedicated proxy
The app works, the batch job failsThe second workload runs in another subnet or account, with its own egressRegister the egress address of every caller on the same credential
Failures only during a regional failoverThe failover region egresses from an address that was never registeredRegister failover addresses up front, not during the incident
Signature errors after introducing a proxyThe proxy terminates TLS and alters the requestForward the connection untouched; never re-sign at the proxy
The address changed on its ownIt was ephemeral, not reservedReserve the address explicitly and pin the NAT component to it

Before you go live​

  1. Map every system that calls us, including batch jobs, internal tools and third parties acting on your behalf.
  2. Determine the real outbound address of each one, from inside the workload.
  3. Give a fixed egress path to anything that lacks one.
  4. Register every resulting address, failover included.
  5. Do it in sandbox first, confirm the integration still works, then repeat in production.