Back to posts
AINews

The Claude API IP allowlist checks only your own address when you save. Find the monthly job, the IPv6 address and the shared IP first

Since 5 November a Claude Platform organization can refuse API requests from addresses outside a list of up to 1,000 IP ranges, so a leaked key stops working elsewhere. The settings page confirms only the address you save from. A script against the Usage and Cost Analytics API lists every key that called in the last 31 days and how many days it was used, which surfaces the weekly and monthly jobs before they hit the wall. Three curl commands show the address each machine presents, including IPv6. GitHub-hosted runners use 8,350 ranges and cannot fit, a Workload Identity Federation token is refused the same way, and the static IPs on Vercel and Railway, like Render's default outbound ranges, can carry other customers' traffic. When a call is refused, the SDKs give up after one request, and claude -p prints 'Failed to authenticate', which points at the wrong fix.

On 5 November Anthropic added an IP allowlist to Claude Platform organizations, in beta. An admin lists the public addresses the organization's traffic comes from, and from then on a request from anywhere else is refused, even with a valid key. A key copied out of a log or a public repository stops working on the attacker's machine.

The weak spot is the moment you switch it on. When you save, the settings page checks the address you are connected from, and nothing else. In Anthropic's words, "it does not check your servers, your teammates, or the address you might connect from tomorrow." Every other caller finds out the next time it runs, which for a weekly report is next week. The setup guide asks you to watch for refusals "including jobs that run only daily or weekly". The steps below find those jobs before the switch, along with the addresses that are easy to miss.

What the list covers

The allowlist applies to the Claude Console itself and to these API endpoints: Messages, Message Batches, Token Counting, Models, Files and Skills, the Claude Managed Agents endpoints, and the Admin and Compliance APIs. It applies whether the request carries an API key or a Workload Identity Federation (WIF) token.

It does not apply to Claude in Amazon Bedrock, Claude Platform on AWS, Claude models on Google Cloud or Claude in Microsoft Foundry, and it does not limit network traffic from inside the Managed Agents runtime. Entries are single addresses or CIDR ranges, IPv4 or IPv6, no broader than /8 for IPv4 or /32 for IPv6, and public addresses only: list where traffic leaves your network, not a private range such as 10.0.0.0/8. The list holds 1,000 entries, and a change can take up to 15 minutes to apply.

A refused request gets a 403 permission_error with the message Your IP address isn't in this organization's allowed range.

List every key that called in the last 31 days

Start from the usage data, not from memory. The Usage and Cost Analytics API (beta) can group requests by API key in daily buckets, and the number of days a key appears in tells you how often its job runs. A key that shows up on 31 of 31 days is your production service, and you already know where it runs. A key that appears on four or five days is a weekly job. A key that appears once is the monthly one somebody set up last spring.

The script needs an Admin API key (sk-ant-admin01-...). Individual accounts have no Admin API, so set the organization up first if you work alone. Run it before you switch the allowlist on, because the Admin API is covered too. It uses the official Python SDK, which sends the beta header and fetches every page for you.

# who_calls_claude.py: every API key that called the Claude API in the last 31 days,
# with the number of days it was used. Run it with an Admin API key:
#   ANTHROPIC_API_KEY=sk-ant-admin01-... python who_calls_claude.py
from collections import defaultdict
from datetime import datetime, timedelta, timezone

import anthropic

client = anthropic.Anthropic()
end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = end - timedelta(days=31)  # the longest range one query allows

days = defaultdict(set)
requests = defaultdict(int)
for row in client.beta.organization.analytics.usage.list(
    starting_at=start,
    ending_at=end,
    time_bucket_width="1d",
    group_by=["api_key_id"],
    limit=1000,
):
    key = row.group.api_key_id or "no API key (WIF token, Console or Managed Agents)"
    days[key].add(row.starting_at.date())
    requests[key] += row.usage.requests or 0

names = {k.id: f"{k.name} ({k.status})" for k in client.organization.api_keys.list(limit=1000)}

print(f"{'days':>4}  {'requests':>8}  last used   key")
for key in sorted(days, key=lambda k: len(days[k])):
    print(f"{len(days[key]):>4}  {requests[key]:>8}  {max(days[key])}  {names.get(key, key)}")

The rarest callers come out on top. The key names below are examples:

days  requests  last used   key
   1         3  2026-10-23  invoice-summariser (active)
   2        80  2026-11-06  no API key (WIF token, Console or Managed Agents)
   5        60  2026-11-07  weekly-report-cron (active)
  31     27900  2026-11-08  prod-api (active)

Work down the list and write the host next to each key. Three kinds of caller are easy to leave out:

  • Jobs that run less than once a month. One query covers at most 31 days. For a quarterly job, run the script again with end moved back 31 days at a time. The API goes back 365 days, and no earlier than 1 January 2026.
  • Developers. Team members who sign in to Claude Code with their Console account may have an API key created at sign-in, kept in the Claude Code workspace. Their laptops are callers, and a laptop at home is outside the office range.
  • Self-hosted sandbox workers for Managed Agents. Anthropic's guide lists them separately.

Ask each machine which address Anthropic will see

Run these on every host that holds one of those keys: the server, the CI runner, the cron box, the sandbox worker.

curl -s -o /dev/null -w '%{remote_ip}\n' https://api.anthropic.com/v1/models
curl -4 -s https://api.ipify.org; echo
curl -6 -s https://api6.ipify.org; echo

The first line prints the Anthropic address the host connected to. api.anthropic.com publishes both an IPv4 address (160.79.104.10) and an IPv6 address (2607:6bc0::10), so an answer starting with 2607: means this host reaches the API over IPv6 and an IPv4-only entry will lock it out. The second and third lines print the host's public IPv4 and IPv6 addresses. The third prints nothing on a host without IPv6. Behind NAT, the IPv4 answer is the router's public address, which is the one to list.

Two cases need a range instead of a single address:

  • Laptops on IPv6. macOS uses a temporary IPv6 address for outgoing connections and replaces it every 24 hours by default (sysctl net.inet6.ip6.temppltime returns 86400). An address you list today stops matching tomorrow. List the /64 prefix, or send developer traffic through a VPN whose exit you have listed.
  • Home and mobile connections. The provider can change the address without notice. The allowlist guide says so, and its own advice is to list the office, VPN and proxy networks people connect through.

Who else can send from the address you list

The allowlist cannot tell your traffic from anyone else's on the same address. Anthropic's guide makes the point about proxies: if you add a shared proxy address, "an attacker with a leaked key or a stolen sign-in could potentially use it through the proxy." The same holds for every address your platform shares with other customers, and several "static IP" products are shared by default.

Where the call runsWhat you would listWho else sends from those addresses
GitHub-hosted runnerThe actions ranges in api.github.com/meta: 8,350 on 9 November (5,622 IPv4, 2,728 IPv6), still 6,410 after merging adjacent rangesEvery Actions job on GitHub. It does not fit in 1,000 entries anyway
GitHub larger runner with static IP rangesThe ranges assigned to your runner poolYour pool only. Needs GitHub Enterprise Cloud, and macOS larger runners cannot have them
Vercel Static IPs (Pro, Enterprise)Two addresses per region"A small group of customers in the same region", per Vercel. Secure Compute (Enterprise) gives a dedicated pair
Railway Static Outbound IPs (Pro)The IPv4 addresses on the serviceRailway: they "may be shared with other customers". They change if you move the service to another region
Render, default outbound rangesYour region's ranges"All services in the same region". Render's dedicated IPs are reserved for your workspace
Your own VM, office or VPN exitIts public IPv4 and IPv6Whoever shares that NAT

A shared entry still narrows who can use a stolen key, from anyone on the internet to the customers of one platform in one region. That is weaker than it looks in the settings page. Where you have the choice, put the job behind an address only you use.

For GitHub Actions, switching to WIF does not help: WIF tokens are checked against the list like keys. That leaves three routes for a workflow that calls Claude, including anthropics/claude-code-action:

  1. A self-hosted runner on a machine whose exit address you list.
  2. A larger runner with static IP ranges, if the organization is on GitHub Enterprise Cloud.
  3. Running the action against Bedrock, Vertex AI or Foundry. The action authenticates to those three only with GitHub's OIDC token, so no key is stored. The allowlist does not apply there, so any address restriction has to come from the cloud provider's side.

Switch it on, then make the refusal loud

The toggle is on the Identity and access page in the Console, and an admin or owner can change it. Add the entries (comma-separated works), then decide on the checkbox Allow admins to access this settings page from any IP address. Anthropic recommends ticking it, so an admin who is locked out can still fix the list from anywhere. The cost is that someone who takes over an admin session can edit the list from anywhere too. If the organization has the Compliance API set up, its Activity Feed records allowlist changes, so you can audit them.

Then give it 15 minutes, and plan for the job you missed. We pointed each client at a local server that returns the documented refusal and counted the requests:

  • The Python SDK (2.6.0) raised anthropic.PermissionDeniedError after one request, with no retry. The Node SDK (1.6.0) threw PermissionDeniedError after one request.
  • claude -p (Claude Code 2.1.319, with an API key) exited 1 after one request and printed Failed to authenticate. API Error: 403 Your IP address isn't in this organization's allowed range. With --output-format json, the result carries "is_error": true and "api_error_status": 403, next to "subtype": "success". A wrapper that checks subtype alone records the run as a success.

The words "Failed to authenticate" invite the wrong fix. A new key from the same organization is refused from the same address, so check the address before you rotate anything.

A cron job can do that check for you. This wrapper turns the refusal into a line that names the address to add:

import sys
import urllib.request

import anthropic

client = anthropic.Anthropic()

def main():
    reply = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=200,
        messages=[{"role": "user", "content": "Summarise this week's tickets: ..."}],
    )
    print(reply.content[0].text)

try:
    main()
except anthropic.PermissionDeniedError as e:
    if "allowed range" not in str(e):
        raise
    # api64.ipify.org answers with the host's IPv6 address if it has one, IPv4 otherwise
    addr = urllib.request.urlopen("https://api64.ipify.org", timeout=10).read().decode()
    sys.exit(f"IP allowlist refused this host. Add {addr} in Console > Settings > Identity and access.")

Run against the test server, it printed the host's public address and exited 1, which is enough for most cron setups to send the alert.

If you lock yourself out without the checkbox, another admin on a listed network can add your address. When no admin can reach the page, Anthropic support can turn the allowlist off.

Shorten what a stolen key is worth

The guide is plain about the limit: "An allowlist limits where a key works, but not for how long." Two settings cover the time side. A new API key can expire after 3 hours, 1 day, 7 days, 30 days or a custom duration. The expiry is fixed when the key is created and cannot be changed later, so existing keys need to be replaced to get one. For workloads that already have an identity provider (GitHub Actions, AWS, Google Cloud, Kubernetes, Entra ID), WIF swaps the stored sk-ant- key for short-lived tokens. A WIF token is still checked against the allowlist, so the two work together.

Claude Enterprise has a separate allowlist in claude.ai settings that also blocks the desktop and mobile apps and Claude Code signed in with a Claude account. That one deserves its own rollout plan, because phones on mobile data and laptops off the VPN are blocked until they reach a listed network.

The same pre-flight applies to any key-based API you lock to addresses: find the callers from usage data, ask each host what it presents, and check who shares the address. Our earlier post on Claude Code guard hooks that fail open covers the other half of that habit, which is testing a safeguard by making it fail on purpose.

Get the next post when it ships

One email on Sunday with the new post and a short list of what shipped that week — new guides, tool updates, and a couple of links worth reading.