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
| Rule | Value |
|---|---|
| Entries per credential | 1 to 50 |
| Accepted format | Individual public IPv4 addresses (203.0.113.10) |
| Ranges (CIDR) | Not accepted |
| IPv6 | Not supported yet |
| Duplicates | Collapsed |
| Order | Canonical. The order you send does not change the credential |
| Scope | Per 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.
| Provider | Component | Address to reserve |
|---|---|---|
| AWS | NAT Gateway | Elastic IP |
| Azure | NAT Gateway | Static public IP (Standard SKU) |
| Google Cloud | Cloud NAT + Cloud Router | Reserved regional external IP |
| Oracle Cloud | NAT Gateway | Reserved 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.
- AWS
- Azure
- Google Cloud
- Oracle Cloud
# 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
# NAT Gateway requires a Standard SKU public IP.
# 1. reserve the address
az network public-ip create \
--resource-group <resource-group> \
--name lbpay-egress \
--location <location> \
--sku Standard \
--allocation-method Static
# 2. create the NAT Gateway with it
az network nat gateway create \
--resource-group <resource-group> \
--name lbpay-nat \
--location <location> \
--public-ip-addresses lbpay-egress
# 3. attach it to the subnet where the workload runs
az network vnet subnet update \
--resource-group <resource-group> \
--vnet-name <vnet> \
--name <subnet> \
--nat-gateway lbpay-nat
# 4. confirm the reserved address...
az network public-ip show --resource-group <resource-group> \
--name lbpay-egress --query ipAddress --output tsv
# 5. ...and that the workload actually egresses through it
curl --fail --silent https://ifconfig.me
# Instances must have no external IP, otherwise they bypass Cloud NAT
# and egress through their own address.
gcloud config set project <project-id>
# 1. reserve a regional external address
gcloud compute addresses create lbpay-egress --region=<region>
# 2. reuse an existing Cloud Router in this network and region, or create one
gcloud compute routers create lbpay-router \
--network=<vpc-network> --region=<region>
# 3. pin Cloud NAT to the reserved address.
# Use <subnet>:ALL to also cover secondary ranges — GKE pods need this.
gcloud compute routers nats create lbpay-nat \
--router=lbpay-router --region=<region> \
--nat-custom-subnet-ip-ranges=<subnet> \
--nat-external-ip-pool=lbpay-egress
# 4. confirm the reserved address...
gcloud compute addresses describe lbpay-egress \
--region=<region> --format='value(address)'
# 5. ...and that the workload actually egresses through it
curl --fail --silent https://ifconfig.me
Cloud NAT is regional: repeat for every region you run in, and register each address.
# 1. reserve a public IP so the address survives a gateway recreation
oci network public-ip create \
--compartment-id <compartment-ocid> \
--lifetime RESERVED \
--display-name lbpay-egress
# 2. create the NAT Gateway with it
oci network nat-gateway create \
--compartment-id <compartment-ocid> \
--vcn-id <vcn-ocid> \
--display-name lbpay-nat \
--public-ip-id <reserved-public-ip-ocid>
# 3. read the CURRENT route rules first — the update replaces the whole
# set, so anything you omit is deleted
oci network route-table get --rt-id <route-table-ocid> \
--query 'data."route-rules"'
# 4. resend the existing rules plus the new one
oci network route-table update --rt-id <route-table-ocid> \
--route-rules '[<existing-rules>,
{"cidrBlock":"0.0.0.0/0",
"networkEntityId":"<nat-gateway-ocid>"}]'
# 5. confirm the gateway address...
oci network nat-gateway get --nat-gateway-id <nat-gateway-ocid> \
--query 'data."nat-ip"'
# 6. ...and that the workload actually egresses through it
curl --fail --silent https://ifconfig.me
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:
- Keep signing in your application. The proxy forwards traffic; it must never hold your private key or re-sign anything.
- 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.
- 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.
- 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.
| Symptom | Likely cause | What to do |
|---|---|---|
IP_NOT_ALLOWED right after registering | You registered an inbound address: load balancer, CDN, hostname or your own browser | Run the outbound check from inside the workload and compare |
IP_NOT_ALLOWED on some requests only | The workload egresses through a rotating provider pool | Route egress through a NAT gateway in your own VPC, or a dedicated proxy |
| The app works, the batch job fails | The second workload runs in another subnet or account, with its own egress | Register the egress address of every caller on the same credential |
| Failures only during a regional failover | The failover region egresses from an address that was never registered | Register failover addresses up front, not during the incident |
| Signature errors after introducing a proxy | The proxy terminates TLS and alters the request | Forward the connection untouched; never re-sign at the proxy |
| The address changed on its own | It was ephemeral, not reserved | Reserve the address explicitly and pin the NAT component to it |
Before you go live
- Map every system that calls us, including batch jobs, internal tools and third parties acting on your behalf.
- Determine the real outbound address of each one, from inside the workload.
- Give a fixed egress path to anything that lacks one.
- Register every resulting address, failover included.
- Do it in sandbox first, confirm the integration still works, then repeat in production.