Claude API & Console Hardening Guide
Security hardening for the Claude API and Console — API key scoping and rotation, workload identity federation, workspace segmentation, data residency and retention, and usage/spend monitoring.
Overview
The Claude API (api.anthropic.com) and its admin Console are the developer platform surface of Anthropic — API keys, workspaces, data-residency and retention settings, and spend controls all live here. A compromised or over-scoped API key is the platform’s highest-frequency risk; workspace segmentation and workload identity federation are its strongest structural mitigations.
This is a product guide within the Anthropic platform. Organization-wide controls (SSO, roles, admin API keys, integration governance) live in the Anthropic Common Controls hub; Claude Code controls live in the Claude Code guide.
Intended Audience
- Platform engineers integrating the Claude API
- Security engineers governing AI API usage
- FinOps/engineering leaders managing AI spend
- GRC professionals assessing AI platform compliance
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Claude API and Console hardening: API key workspace scoping, rotation, and elimination via workload identity federation; workspace segmentation and membership; data residency and retention; usage monitoring and spend limits. Organization identity and Claude Code controls are covered by the sibling Anthropic guides.
Table of Contents
1. API Key Management
1.1 Scope API Keys to Workspaces
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | AC-3, AC-6 |
| SOC 2 | CC6.1, CC6.3 |
Description
Every standard API key in Anthropic Claude is scoped to a single workspace. Leverage this design by creating separate workspaces for different environments (development, staging, production) and teams, ensuring API keys cannot access resources across workspace boundaries.
Rationale
Why This Matters:
- A compromised development API key cannot access production workspaces
- Workspace-scoped keys enable granular cost tracking and rate limiting
- API keys persist when users are removed — they’re scoped to the organization, not individuals
- Keys can only be created via the Console (not via API) — another security design choice
Attack Prevented: Lateral movement from development to production, blast radius of key compromise
Prerequisites
- Organization Admin or Workspace Admin access
- Workspace naming convention established
ClickOps Implementation
Step 1: Create Workspace-Scoped Keys
- Navigate to: console.anthropic.com → Select target workspace
- Go to: Settings → API Keys
- Click Create Key
- Name the key descriptively:
{team}-{environment}-{purpose}(e.g., “ml-team-prod-inference”)
Step 2: Audit Existing Keys
- Navigate to: Settings → API Keys (org-wide view)
- Review each key’s workspace assignment
- Identify keys in the Default Workspace — migrate to dedicated workspaces
Time to Complete: ~10 minutes per key
Code Implementation
Code Pack: API Script
# List all API keys across the organization, grouped by workspace
info "Auditing API keys across all workspaces..."
API_KEYS=$(anthropic_list_all "/v1/organizations/api_keys?status=active") || {
fail "2.1 Failed to list API keys"
summary; exit 0
}
KEY_COUNT=$(echo "${API_KEYS}" | jq 'length')
info "Found ${KEY_COUNT} active API keys"
# Group by workspace
echo "${API_KEYS}" | jq -r 'group_by(.workspace_id) | .[] |
"Workspace: \(.[0].workspace_id)\n" +
([ .[] | " Key: \(.name // "unnamed") | Status: \(.status) | Created: \(.created_at)" ] | join("\n"))
'
# Flag unnamed keys
UNNAMED=$(echo "${API_KEYS}" | jq '[.[] | select(.name == null or .name == "")] | length')
if [[ "${UNNAMED}" -gt 0 ]]; then
warn "2.1 ${UNNAMED} API keys have no name — add descriptive names for auditability"
else
pass "2.1 All API keys have descriptive names"
fi
# Flag keys in default workspace
DEFAULT_WS_KEYS=$(echo "${API_KEYS}" | jq '[.[] | select(.workspace_id == null)] | length')
if [[ "${DEFAULT_WS_KEYS}" -gt 0 ]]; then
warn "2.1 ${DEFAULT_WS_KEYS} keys are in the default workspace — consider scoping to dedicated workspaces"
fi
# Disable an API key by setting status to inactive
# Usage: Set KEY_ID before running
if [[ -n "${KEY_ID:-}" ]]; then
info "Disabling API key ${KEY_ID}..."
anthropic_post "/v1/organizations/api_keys/${KEY_ID}" \
'{"status": "inactive"}' || {
fail "2.1 Failed to disable API key"
summary; exit 0
}
pass "2.1 API key ${KEY_ID} disabled"
fi
Validation & Testing
- List all API keys via Admin API — verify each has a workspace assignment
- Verify no unnamed keys exist
- Test that a key scoped to Workspace A cannot be used with Workspace B resources
Expected result: All API keys have descriptive names and are assigned to appropriate workspaces
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC6.3 | Logical access security; role-based access |
| NIST 800-53 | AC-3, AC-6 | Access enforcement; least privilege |
| ISO 27001 | A.9.4.1 | Information access restriction |
1.2 Rotate API Keys Regularly
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | IA-5(1) |
| SOC 2 | CC6.1 |
Description
Establish a 90-day rotation schedule for all API keys. Since API keys can only be created via the Console, rotation requires creating a new key, updating dependent services, and then disabling/archiving the old key via the Admin API.
Rationale
Why This Matters:
- Long-lived API keys increase the window of opportunity for attackers
- Keys may be accidentally exposed in logs, error messages, or code repositories
- Anthropic API keys persist after user removal — orphaned keys remain active
Attack Prevented: Stale credential exploitation, leaked key abuse
Prerequisites
- API key inventory with creation dates
- Deployment pipeline that supports key rotation (secrets manager integration)
ClickOps Implementation
Step 1: Identify Keys Due for Rotation
- Navigate to: console.anthropic.com → Settings → API Keys
- Review creation dates for each active key
- Flag any key older than 90 days
Step 2: Rotate
- Create a new key in the same workspace with the same naming convention
- Update the dependent application/service to use the new key
- Verify the application works with the new key
- Disable the old key (set status to
inactivevia Admin API) - After a 7-day grace period, archive the old key
Time to Complete: ~15 minutes per key (excluding application updates)
Code Implementation
Code Pack: API Script
# Identify API keys that have not been rotated within 90 days
info "Checking for stale API keys (>90 days since creation)..."
API_KEYS=$(anthropic_list_all "/v1/organizations/api_keys?status=active") || {
fail "2.2 Failed to list API keys"
summary; exit 0
}
CUTOFF=$(date -d '90 days ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || \
date -v-90d '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)
STALE_KEYS=$(echo "${API_KEYS}" | jq --arg cutoff "${CUTOFF}" \
'[.[] | select(.created_at < $cutoff)]')
STALE_COUNT=$(echo "${STALE_KEYS}" | jq 'length')
if [[ "${STALE_COUNT}" -gt 0 ]]; then
warn "2.2 ${STALE_COUNT} API keys are older than 90 days:"
echo "${STALE_KEYS}" | jq -r '.[] | " \(.name // "unnamed") | Created: \(.created_at) | Workspace: \(.workspace_id)"'
else
pass "2.2 All API keys are within 90-day rotation window"
fi
# Archive an old API key after rotation
# Usage: Set OLD_KEY_ID before running
if [[ -n "${OLD_KEY_ID:-}" ]]; then
info "Archiving old API key ${OLD_KEY_ID}..."
anthropic_post "/v1/organizations/api_keys/${OLD_KEY_ID}" \
'{"status": "archived"}' || {
fail "2.2 Failed to archive API key"
summary; exit 0
}
pass "2.2 API key ${OLD_KEY_ID} archived"
fi
Validation & Testing
- Run stale key audit script — zero keys older than 90 days
- Verify disabled keys return 401 when used
- Confirm application functionality with rotated keys
Expected result: No API key is older than 90 days; old keys are archived
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security over protected information assets |
| NIST 800-53 | IA-5(1) | Authenticator management — password-based authentication |
| ISO 27001 | A.9.3.1 | Use of secret authentication information |
1.3 Eliminate Static API Keys via Workload Identity Federation
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | IA-5(1), IA-9, AC-3 |
| SOC 2 | CC6.1 |
| CIS Controls | 5.6, 6.5 |
Description
Anthropic’s Workload Identity Federation (WIF) lets workloads authenticate to the Claude API using short-lived OpenID Connect (OIDC) tokens issued by an identity provider you already operate — AWS IAM, Google Cloud, Microsoft Azure / Entra ID, GitHub Actions, Kubernetes service accounts, SPIFFE/SPIRE, or Okta — instead of long-lived sk-ant-... API keys. Your workload presents a signed JWT to POST /v1/oauth/token (RFC 7523 jwt-bearer grant); Anthropic validates it against the trust rule you configured in the Console and returns a short-lived sk-ant-oat01-... access token bound to a service account in your organization. There are no static secrets to mint, store in CI, rotate, or leak.
WIF complements (rather than replaces) the workspace scoping in 1.1 and the rotation discipline in 1.2: the federation rule pins the upstream identity, and the minted access token still inherits the target workspace’s rate limits, billing, and OAuth scope (workspace:developer at launch). Use WIF anywhere a workload runs in a federable environment; keep static API keys only for environments that cannot present an OIDC JWT.
Rationale
Why This Matters:
- Removes long-lived
sk-ant-...API keys from CI runners, container images, and secrets managers — the highest-value Anthropic credential class - Tokens expire in minutes (default 3600s; minimum 60s), not never; SDKs refresh transparently before expiry
- Federation rule’s
subject_prefix,audience,claims, and CELconditionmatchers bind the credential to a specific workload identity (e.g., a single GitHub repo + branch, a specific Kubernetes service account, or an EKS IRSA role) - Audit trail attributes API calls to the federated workload identity, not just to “the API key”
- Eliminates the “key was leaked, rotation forgotten” incident class for federated workloads
Attack Prevented: Static API key exfiltration from CI logs, container images, secrets managers, or developer machines; long-lived credential abuse after personnel changes; lateral movement using a stolen long-lived key
Important caveat: WIF inherits the trust of your upstream IdP. A compromised IdP, an over-broad federation rule (e.g., repo:my-org/* without a branch claim), or a misconfigured audience value can grant broader access than intended. Pair WIF with your IdP’s existing controls (workload identity binding, conditional access, audit logging) for defense in depth.
Prerequisites
- Organization Admin access to the Claude Console (Settings → Workload identity)
- An OIDC-capable identity provider with a reachable JWKS endpoint (or an inline JWKS document for air-gapped clusters)
- A workload that can obtain an identity token from that provider (Kubernetes projected service-account token, GitHub Actions OIDC, AWS STS web identity, GCP metadata server, Azure IMDS, etc.)
- Workspace IDs (
wrkspc_...) for any workspaces the federated workload should act in ANTHROPIC_API_KEYandANTHROPIC_AUTH_TOKENremoved from anywhere the workload runs (they sit above federation in the SDK credential precedence chain and silently shadow it)
ClickOps Implementation
Step 1: Register a Federation Issuer
- Navigate to: console.anthropic.com → Settings → Workload identity → Issuers tab
- Click Create issuer and select the appropriate preset (AWS, Google Cloud, or generic OIDC for GitHub Actions / Kubernetes / Entra ID / Okta)
- Set Issuer URL to the exact
issclaim your IdP puts in its JWTs. Decode a sample token to verify:jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .iss' token - Set JWKS source to
discoveryfor any provider that serves/.well-known/openid-configuration. Useexplicit_urlfor providers without discovery, orinlinefor air-gapped clusters - URLs must be
https, port 443, public DNS (no IP literals) — except forinlineandexplicit_urlmodes where theissuer_urlis only string-compared
Step 2: Create a Service Account
- Go to: Settings → Service accounts → Create service account
- Name it after the workload it represents (
inference-worker,ci-deploy,eks-prod-namespace-foo) - Note the service account ID (
svac_...) - Add the service account to each target workspace via that workspace’s Members page — the federated token inherits the workspace’s rate limits and usage attribution
Step 3: Create a Federation Rule
- Back on Workload identity → Federation rules tab → Create rule
- Select the issuer from Step 1 and the service account from Step 2
- Configure the Match block as narrowly as possible:
- Static matchers:
subject_prefix(with optional trailing*for prefix match), exactaudience, and a map of exactclaimsvalues - CEL matcher: a CEL
conditionexpression for complex logic (nested claims, list membership, boolean logic) - At least one of
subject_prefix,claims, orconditionis required — a rule that only matchesaudienceis rejected
- Static matchers:
- Set Authorization scope (
workspace:developerat launch) and Token lifetime (60–86400 seconds; default 3600) - Note the rule ID (
fdrl_...); the workload passes it on every token-exchange request
Step 4: Migrate the Workload Off the Static Key
- Configure WIF in parallel with the existing
ANTHROPIC_API_KEY - Smoke-test with
ant auth statusfrom inside the workload to confirm the SDK is exchanging the federated token (not falling back to the API key) - Unset
ANTHROPIC_API_KEYeverywhere it is injected (CI secrets, container env, shell profiles). Re-confirmant auth statusreports the federation source as winning - Revoke the old API key in Settings → API keys
Time to Complete: ~30–60 minutes for first issuer + rule; ~10 minutes per additional rule
Code Implementation
Code Pack: Config
{
"version": "1.0",
"authentication": {
"type": "oidc_federation",
"federation_rule_id": "fdrl_REPLACE_ME",
"service_account_id": "svac_REPLACE_ME",
"identity_token": {
"source": "file",
"path": "/var/run/secrets/anthropic.com/token"
}
},
"organization_id": "00000000-0000-0000-0000-000000000000",
"workspace_id": "wrkspc_REPLACE_ME",
"base_url": "https://api.anthropic.com"
}
Code Pack: API Script
# Read the IdP-issued JWT from the projected token file (Kubernetes / GHA / etc.)
JWT=$(cat "${ANTHROPIC_IDENTITY_TOKEN_FILE}")
# Build the token-exchange body — workspace_id is required when the rule
# is enabled for more than one workspace
WORKSPACE_FIELD=""
if [ -n "${ANTHROPIC_WORKSPACE_ID:-}" ]; then
WORKSPACE_FIELD=$(printf ',\n "workspace_id": "%s"' "${ANTHROPIC_WORKSPACE_ID}")
fi
RESPONSE=$(curl -sS -f --max-time 10 "${ANTHROPIC_API_BASE}/v1/oauth/token" \
-H "content-type: application/json" \
--data @- <<JSON
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "${JWT}",
"federation_rule_id": "${ANTHROPIC_FEDERATION_RULE_ID}",
"organization_id": "${ANTHROPIC_ORGANIZATION_ID}",
"service_account_id": "${ANTHROPIC_SERVICE_ACCOUNT_ID}"${WORKSPACE_FIELD}
}
JSON
)
ACCESS_TOKEN=$(echo "${RESPONSE}" | jq -r '.access_token')
EXPIRES_IN=$(echo "${RESPONSE}" | jq -r '.expires_in')
SCOPE=$(echo "${RESPONSE}" | jq -r '.scope')
# Sanity-check the prefix — federated tokens are sk-ant-oat01-…
case "${ACCESS_TOKEN}" in
sk-ant-oat01-*) echo "Got federated access token (scope=${SCOPE}, expires_in=${EXPIRES_IN}s)" ;;
*) echo "ERROR: token-exchange did not return an oat01 token: ${RESPONSE}" >&2; exit 1 ;;
esac
# Call the Claude API with the short-lived federated token (Bearer auth)
curl -sS -f "${ANTHROPIC_API_BASE}/v1/messages" \
-H "authorization: Bearer ${ACCESS_TOKEN}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data @- <<'JSON' | jq -r '.content[0].text'
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello, Claude"}]
}
JSON
# Pre-deploy guardrail: fail the build if a static API key is present in
# the environment of a workload that is supposed to use WIF. ANTHROPIC_API_KEY
# sits ABOVE federation in the SDK's credential precedence chain, so a
# leftover key silently shadows federation.
for VAR in ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN; do
if [ -n "${!VAR:-}" ]; then
echo "FAIL: \$${VAR} is set; it will shadow Workload Identity Federation." >&2
echo " Unset it (\`unset ${VAR}\`) — empty-string also wins precedence." >&2
exit 2
fi
done
echo "OK: no static API key shadows the federation credentials"
Code Pack: SDK Script
from anthropic import Anthropic, WorkloadIdentityCredentials, IdentityTokenFile
# Explicit construction — recommended when you ship one image to multiple
# environments and want the federation parameters in code rather than env.
client = Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=IdentityTokenFile(
"/var/run/secrets/anthropic.com/token"
),
federation_rule_id="fdrl_REPLACE_ME",
organization_id="00000000-0000-0000-0000-000000000000",
service_account_id="svac_REPLACE_ME",
workspace_id="wrkspc_REPLACE_ME",
),
)
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.content[0].text)
# Zero-argument construction — recommended for production. Ship the same
# container image everywhere; inject these env vars per environment:
# ANTHROPIC_FEDERATION_RULE_ID
# ANTHROPIC_ORGANIZATION_ID
# ANTHROPIC_SERVICE_ACCOUNT_ID
# ANTHROPIC_WORKSPACE_ID (required when the rule covers >1 workspace)
# ANTHROPIC_IDENTITY_TOKEN_FILE (or ANTHROPIC_IDENTITY_TOKEN literal)
#
# IMPORTANT: ANTHROPIC_API_KEY sits ABOVE federation in the SDK's credential
# precedence chain. A leftover key silently shadows federation. The next block
# is a defensive startup check.
import os, sys
for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"):
if os.environ.get(var):
sys.exit(
f"FATAL: {var} is set; it will shadow Workload Identity Federation. "
"Unset it (empty-string also wins precedence)."
)
from anthropic import Anthropic
client = Anthropic() # SDK reads the ANTHROPIC_FEDERATION_* vars from env
Validation & Testing
- Run the token-exchange script — confirm it returns an
sk-ant-oat01-...token with the expectedscopeandexpires_in - From inside the workload, run
ant auth status— federation should be the winning credential source (not API key) - Trigger a
403test: temporarily change the federation rule’s match block to a non-matching value and confirm the exchange returns400 invalid_grant - Confirm the static-key guardrail: the API script in the pack exits non-zero if
ANTHROPIC_API_KEYis set - For the GitHub Actions workflow: confirm the job succeeds with no
ANTHROPIC_API_KEYrepo secret configured - Audit the authentication history page in the Console after a successful exchange — verify the issuer, rule, and matched claims are what you expect
Expected result: All federable workloads run with no sk-ant-... static keys in their environment; every Claude API call is attributable to a federation rule + service account in the audit trail
Monitoring & Maintenance
Ongoing monitoring:
- Watch the Console’s Workload identity → Authentication history for failed exchanges (
400 invalid_grant) — sustained failures indicate IdP key rotation, claim drift, or an attempted misuse - Inventory federation rules quarterly: archive any rule whose service account or issuer is no longer in active use
- Monitor for new
sk-ant-...API keys created in workspaces that were supposed to be 100% federated — drift indicates an engineer fell back to a static key
Maintenance schedule:
- Monthly: Review the active federation rules list against current production workloads
- Quarterly: Review the match blocks of every rule for over-broad scope (especially CEL
conditionexpressions and baresubject_prefixpatterns ending in*) - Annually: Tabletop exercise an IdP-compromise scenario — confirm you can disable a federation issuer in the Console and that all dependent workloads fail closed
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | None | Workloads run unchanged; SDKs handle the exchange/refresh loop |
| System Performance | Low | One extra HTTPS round-trip per token refresh (every ~hour by default) |
| Maintenance Burden | Low | Eliminates manual key rotation; only changes when IdP trust changes |
| Rollback Difficulty | Easy | Re-issue a static API key and revert the workload’s env vars |
Potential Issues:
ANTHROPIC_API_KEYshadow: A leftover key in the env wins precedence over WIF and silently keeps the workload on a static credential. Check withant auth status.- Empty-string variables:
ANTHROPIC_API_KEY=""is treated as “API key path with an empty key” — unset the variable entirely, do not blank it. - JWKS rotation lag: In
discoveryandexplicit_urlmodes, Anthropic caches the JWKS for up to 60 seconds. If your IdP rotates and signs immediately, exchanges may briefly fail. Publish new keys 15+ minutes before first use; ininlinemode you must update the issuer config manually. - Multi-workspace rules: When a federation rule covers more than one workspace,
workspace_idis required on every exchange (400 invalid_request: workspace_id_required).
Rollback Procedure:
- In the Console, create a new standard API key in the target workspace
- Inject it as
ANTHROPIC_API_KEYin the workload’s environment - Restart the workload — it will pick up the static key (which sits above WIF in precedence) without any code change
- Optionally archive the federation rule
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security over protected information assets |
| NIST 800-53 | IA-5(1) | Authenticator management — short-lived authenticators replacing static credentials |
| NIST 800-53 | IA-9 | Service identification and authentication |
| NIST 800-53 | AC-3 | Access enforcement |
| ISO 27001 | A.9.4.3 | Password management system (eliminating long-lived shared credentials) |
| CIS Controls | 5.6 | Centralized account management |
| CIS Controls | 6.5 | Require MFA for administrative access (via the upstream IdP) |
| NIST AI RMF | GOVERN-1.4 | Authority and accountability for AI system credentials |
2. Workspace Security
2.1 Segment Workspaces by Environment
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | AC-4, SC-7 |
| SOC 2 | CC6.6 |
Description
Create separate workspaces for development, staging, and production environments. Each workspace provides an isolated boundary for API keys, rate limits, spend limits, and data residency settings. Anthropic allows up to 100 workspaces per organization (archived workspaces do not count).
Rationale
Why This Matters:
- Workspace segmentation limits blast radius of API key compromise
- Enables different rate limits and spend caps per environment
- Data residency can be set per workspace (immutable after creation)
- Simplifies cost attribution and usage monitoring
Attack Prevented: Cross-environment contamination, production data exposure via development keys
Prerequisites
- Organization Admin access
- Environment naming convention (e.g.,
engineering-prod,engineering-dev,analytics-prod)
ClickOps Implementation
Step 1: Plan Workspace Structure
- Define workspaces for each team and environment combination
- Determine data residency requirements per workspace (
workspace_geois immutable after creation)
Step 2: Create Workspaces
- Navigate to: console.anthropic.com → Settings → Workspaces
- Click Create Workspace
- Enter workspace name following naming convention
- Configure data residency settings if required
- Repeat for each planned workspace
Step 3: Archive Unused Workspaces
- Identify workspaces with no recent activity
- Archive via Console (caution: this deactivates ALL API keys in the workspace and is irreversible)
Time to Complete: ~5 minutes per workspace
Code Implementation
Code Pack: API Script
# List all workspaces and their configuration
info "Listing all workspaces..."
WORKSPACES=$(anthropic_list_all "/v1/organizations/workspaces") || {
fail "3.1 Failed to list workspaces"
summary; exit 0
}
WS_COUNT=$(echo "${WORKSPACES}" | jq 'length')
info "Found ${WS_COUNT} workspaces (limit: 100 per organization)"
echo "${WORKSPACES}" | jq -r '.[] |
" \(.display_name) | ID: \(.id) | Geo: \(.settings.workspace_geo // "default") | Archived: \(.archived_at // "no")"'
pass "3.1 Workspace inventory complete"
# Create a new workspace for environment segmentation
# Usage: Set WS_NAME and optionally WS_GEO before running
if [[ -n "${WS_NAME:-}" ]]; then
BODY="{\"name\": \"${WS_NAME}\"}"
if [[ -n "${WS_GEO:-}" ]]; then
BODY=$(echo "${BODY}" | jq --arg geo "${WS_GEO}" '. + {settings: {workspace_geo: $geo}}')
fi
info "Creating workspace '${WS_NAME}'..."
RESULT=$(anthropic_post "/v1/organizations/workspaces" "${BODY}") || {
fail "3.1 Failed to create workspace"
summary; exit 0
}
NEW_ID=$(echo "${RESULT}" | jq -r '.id')
pass "3.1 Workspace created: ${NEW_ID}"
fi
Validation & Testing
- List all workspaces via Admin API — verify naming convention adherence
- Verify production workspaces have data residency configured
- Confirm workspace count is within the 100-workspace limit
Expected result: Separate workspaces exist for each team/environment; naming convention followed
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6 | System boundaries and security measures |
| NIST 800-53 | AC-4, SC-7 | Information flow enforcement; boundary protection |
| ISO 27001 | A.13.1.3 | Segregation in networks |
2.2 Manage Workspace Membership
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | AC-2, AC-6 |
| SOC 2 | CC6.2, CC6.3 |
Description
Assign users to only the workspaces they need. Workspace roles (workspace_user, workspace_developer, workspace_admin, workspace_billing) provide granular access control within each workspace. Organization admins automatically inherit workspace_admin in every workspace.
Rationale
Why This Matters:
- Users should only access workspaces relevant to their team and function
- Workspace-level roles limit what actions a user can take within that workspace
- Regular membership audits catch stale access from role changes or departures
Attack Prevented: Unauthorized workspace access, privilege creep, insider threat
Prerequisites
- Workspace Admin or Organization Admin access
- Team-to-workspace mapping documented
ClickOps Implementation
Step 1: Review Current Membership
- Navigate to: console.anthropic.com → Select workspace → Members
- Review each member’s workspace role
- Document any users who don’t belong in this workspace
Step 2: Adjust Membership
- Remove users who no longer need access
- Downgrade workspace roles where appropriate (e.g.,
workspace_admin→workspace_developer) - Add users to workspaces they need access to
Time to Complete: ~10 minutes per workspace
Code Implementation
Code Pack: API Script
# Audit membership for a specific workspace
# Usage: Set WORKSPACE_ID before running
WORKSPACE_ID="${WORKSPACE_ID:-}"
if [[ -z "${WORKSPACE_ID}" ]]; then
info "Listing all workspaces to select for audit..."
WORKSPACES=$(anthropic_list_all "/v1/organizations/workspaces") || {
fail "3.2 Failed to list workspaces"
summary; exit 0
}
echo "${WORKSPACES}" | jq -r '.[] | " \(.id)\t\(.display_name)"' | column -t -s $'\t'
info "Set WORKSPACE_ID=<id> and re-run to audit a specific workspace"
summary; exit 0
fi
info "Auditing members of workspace ${WORKSPACE_ID}..."
MEMBERS=$(anthropic_list_all "/v1/organizations/workspaces/${WORKSPACE_ID}/members") || {
fail "3.2 Failed to list workspace members"
summary; exit 0
}
MEMBER_COUNT=$(echo "${MEMBERS}" | jq 'length')
info "Workspace has ${MEMBER_COUNT} members"
# List members with roles
echo "${MEMBERS}" | jq -r '.[] | " \(.user_id)\t\(.workspace_role)"' | \
column -t -s $'\t' -N "USER_ID,ROLE"
# Flag workspace admins
ADMIN_COUNT=$(echo "${MEMBERS}" | jq '[.[] | select(.workspace_role == "workspace_admin")] | length')
if [[ "${ADMIN_COUNT}" -gt 2 ]]; then
warn "3.2 ${ADMIN_COUNT} workspace admins found — review for least privilege"
else
pass "3.2 Workspace admin count (${ADMIN_COUNT}) is appropriate"
fi
# Remove a user from a workspace
# Usage: Set WORKSPACE_ID and REMOVE_USER_ID before running
if [[ -n "${REMOVE_USER_ID:-}" ]]; then
info "Removing user ${REMOVE_USER_ID} from workspace ${WORKSPACE_ID}..."
anthropic_delete "/v1/organizations/workspaces/${WORKSPACE_ID}/members/${REMOVE_USER_ID}" || {
fail "3.2 Failed to remove workspace member"
summary; exit 0
}
pass "3.2 User ${REMOVE_USER_ID} removed from workspace"
fi
Validation & Testing
- List workspace members via Admin API for each workspace
- Verify no workspace has more than 2
workspace_adminmembers (excluding inherited org admins) - Confirm removed users cannot access workspace resources
Expected result: Each workspace has only authorized members at appropriate role levels
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.2, CC6.3 | Access provisioning; role-based access |
| NIST 800-53 | AC-2, AC-6 | Account management; least privilege |
| ISO 27001 | A.9.2.5 | Review of user access rights |
3. Data Security & Privacy
3.1 Enforce Data Residency Restrictions
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | SC-7, SA-9(5) |
| SOC 2 | CC6.6, P6.1 |
Description
Configure data residency at the workspace level to control where Claude processes inference requests. The workspace_geo setting (immutable after creation) controls data storage location. default_inference_geo and allowed_inference_geos control where requests are processed. Available regions include us and global.
Rationale
Why This Matters:
- Regulatory requirements (GDPR, data sovereignty laws) may mandate processing within specific regions
workspace_geocannot be changed after workspace creation — plan carefully- The
inference_geoparameter can also be set per-request by API callers, butallowed_inference_geosrestricts what values are permitted
Attack Prevented: Data sovereignty violations, regulatory non-compliance
Prerequisites
- Organization Admin access
- Data residency requirements documented per team/workspace
- Legal/compliance approval for geo settings
ClickOps Implementation
Step 1: Audit Current Settings
- Navigate to: console.anthropic.com → Settings → Workspaces
- Review each workspace’s data residency configuration
- Note any workspaces without explicit geo settings
Step 2: Configure New Workspaces with Correct Geo
- When creating new workspaces, select the appropriate
workspace_geo - This setting is immutable — double-check before confirming
Step 3: Restrict Inference Geos
- For regulated workspaces, set
allowed_inference_geosto["us"]only - Set
default_inference_geoto"us"to ensure all requests default to US processing
Time to Complete: ~5 minutes per workspace
Code Implementation
Code Pack: API Script
# Audit data residency settings across all workspaces
info "Auditing data residency configuration per workspace..."
WORKSPACES=$(anthropic_list_all "/v1/organizations/workspaces") || {
fail "4.1 Failed to list workspaces"
summary; exit 0
}
echo "${WORKSPACES}" | jq -r '.[] | {
name: .display_name,
id: .id,
workspace_geo: (.settings.workspace_geo // "not set"),
default_inference_geo: (.settings.default_inference_geo // "not set"),
allowed_inference_geos: (.settings.allowed_inference_geos // ["not restricted"])
} | " \(.name) | Geo: \(.workspace_geo) | Default Inference: \(.default_inference_geo) | Allowed: \(.allowed_inference_geos | join(", "))"'
# Flag workspaces without data residency configured
UNCONFIGURED=$(echo "${WORKSPACES}" | jq '[.[] | select(
.settings.workspace_geo == null or .settings.workspace_geo == ""
)] | length')
if [[ "${UNCONFIGURED}" -gt 0 ]]; then
warn "4.1 ${UNCONFIGURED} workspaces have no explicit data residency setting"
else
pass "4.1 All workspaces have data residency configured"
fi
# Restrict a workspace to US-only inference
# Usage: Set WORKSPACE_ID before running
if [[ -n "${WORKSPACE_ID:-}" ]]; then
info "Restricting workspace ${WORKSPACE_ID} to US-only inference..."
anthropic_post "/v1/organizations/workspaces/${WORKSPACE_ID}" '{
"settings": {
"default_inference_geo": "us",
"allowed_inference_geos": ["us"]
}
}' || {
fail "4.1 Failed to update workspace data residency"
summary; exit 0
}
pass "4.1 Workspace ${WORKSPACE_ID} restricted to US-only inference"
fi
Validation & Testing
- List all workspaces via Admin API — verify
workspace_geoandallowed_inference_geos - Attempt a request with
inference_geo: "global"against a US-restricted workspace — should fail - Verify new workspaces are created with correct geo from the start
Expected result: Regulated workspaces have explicit data residency configuration; inference geo restrictions enforced
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6, P6.1 | System boundaries; privacy — consent and choice |
| NIST 800-53 | SC-7, SA-9(5) | Boundary protection; processing, storage, and service location |
| ISO 27001 | A.18.1.4 | Privacy and protection of personally identifiable information |
3.2 Configure Data Retention Policies
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | SI-12 |
| SOC 2 | P4.1 |
Description
Understand and configure Anthropic’s data retention policies. By default, API inputs and outputs are retained for up to 30 days and are not used for model training. Enterprise customers can negotiate custom retention periods or Zero Data Retention (ZDR), where no inputs or outputs are stored after the response is delivered.
Rationale
Why This Matters:
- Sensitive prompts containing PII, financial data, or intellectual property are retained for 30 days by default
- ZDR eliminates server-side storage of prompts and completions entirely
- Custom retention periods allow organizations to balance compliance needs with debugging capabilities
Attack Prevented: Post-breach data exposure, regulatory non-compliance for data minimization
Prerequisites
- Claude Enterprise plan (for custom retention or ZDR)
- Data classification policy for content sent to Claude
- Legal review of Anthropic’s data handling agreement
ClickOps Implementation
Step 1: Review Default Retention
- Review Anthropic’s usage policy at anthropic.com/policies/usage-policy
- Confirm your plan’s default retention (API: 30 days, not used for training)
Step 2: Request Custom Retention (Enterprise)
- Contact your Anthropic account representative
- Specify desired retention period or request ZDR
- Obtain written confirmation of retention configuration
Step 3: Implement Data Handling Controls
- Establish guidelines for what data types may be sent to Claude
- Implement client-side PII redaction before sending sensitive prompts
- Use workspace segmentation to isolate sensitive vs. non-sensitive workloads
Time to Complete: ~30 minutes (policy review) + vendor coordination for custom retention
Validation & Testing
- Confirm retention period with Anthropic account team (Enterprise)
- Verify client-side PII redaction is in place for sensitive workloads
- Review data classification guidelines with engineering team
Expected result: Data retention policy documented and aligned with organizational requirements
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | P4.1 | Privacy — data minimization |
| NIST 800-53 | SI-12 | Information management and retention |
| ISO 27001 | A.8.10 | Information deletion |
4. Monitoring & Usage Controls
4.1 Monitor API Usage and Costs
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | AU-6, SI-4 |
| SOC 2 | CC7.2 |
Description
Use Anthropic’s Admin API usage and cost reporting endpoints to monitor token consumption, request patterns, and spending across workspaces. The usage API supports 1-minute, 1-hour, and 1-day bucket granularity with filtering by model, workspace, API key, service tier, and geography.
Rationale
Why This Matters:
- Unusual usage spikes may indicate compromised API keys
- Cost monitoring prevents unexpected bills from runaway applications
- Per-workspace usage data enables accurate cost attribution to teams
- Data is available within ~5 minutes of request completion
Attack Prevented: API key abuse, cryptocurrency mining via API, unauthorized bulk data extraction
Prerequisites
- Admin API key provisioned
- Monitoring infrastructure (Datadog, Grafana, etc.) or cron job for regular checks
ClickOps Implementation
Step 1: Review Usage Dashboard
- Navigate to: console.anthropic.com → Usage
- Review token usage charts, rate limit utilization, and cache rates
- Filter by workspace, model, and time period
Step 2: Review Cost Dashboard
- Navigate to: console.anthropic.com → Cost
- Review cost breakdown by workspace and model
- Identify any unexpected cost increases
Step 3: Configure Observability Integration
- Integrate with supported platforms: CloudZero, Datadog, Grafana Cloud, Honeycomb, or Vantage
- Set up alerts for anomalous usage patterns
Time to Complete: ~15 minutes (dashboard review) + integration setup time
Code Implementation
Code Pack: API Script
# Generate a daily usage report for the past 7 days, grouped by workspace
START_DATE=$(date -d '7 days ago' '+%Y-%m-%dT00:00:00Z' 2>/dev/null || \
date -v-7d '+%Y-%m-%dT00:00:00Z' 2>/dev/null)
END_DATE=$(date '+%Y-%m-%dT23:59:59Z')
info "Fetching usage report from ${START_DATE} to ${END_DATE}..."
USAGE=$(anthropic_get "/v1/organizations/usage_report/messages?start_time=${START_DATE}&end_time=${END_DATE}&group_by=workspace&bucket_width=1d") || {
fail "5.1 Failed to fetch usage report"
summary; exit 0
}
echo "${USAGE}" | jq -r '.data[] | " \(.workspace_id // "default") | Input: \(.input_tokens) | Output: \(.output_tokens) | Date: \(.bucket_start_time)"'
pass "5.1 Usage report retrieved"
# Generate a cost report for the past 30 days, grouped by workspace
COST_START=$(date -d '30 days ago' '+%Y-%m-%dT00:00:00Z' 2>/dev/null || \
date -v-30d '+%Y-%m-%dT00:00:00Z' 2>/dev/null)
info "Fetching cost report from ${COST_START}..."
COST=$(anthropic_get "/v1/organizations/cost_report?start_time=${COST_START}&end_time=${END_DATE}&group_by=workspace") || {
fail "5.1 Failed to fetch cost report"
summary; exit 0
}
echo "${COST}" | jq -r '.data[] | " \(.workspace_id // "default") | Cost: $\(.cost_usd) | Date: \(.bucket_start_time)"'
pass "5.1 Cost report retrieved"
Validation & Testing
- Run usage report API script — verify data returns for all active workspaces
- Run cost report API script — verify cost data is accurate
- Confirm observability integration is receiving data
Expected result: Usage and cost data is monitored regularly with alerts for anomalies
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2 | System monitoring |
| NIST 800-53 | AU-6, SI-4 | Audit record review; system monitoring |
| ISO 27001 | A.12.4.1 | Event logging |
4.2 Configure Spend Limits per Workspace
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | SA-9, SI-4 |
| SOC 2 | CC7.2, CC6.8 |
Description
Set per-workspace spend limits and rate limits to prevent cost overruns and abuse. Workspace limits cannot exceed organization-level limits. Configure both monthly spend caps and per-model rate limits (requests per minute, input/output tokens per minute).
Rationale
Why This Matters:
- A compromised API key without spend limits can generate unlimited costs
- Rate limits prevent individual workspaces from consuming the organization’s entire quota
- Workspace-level limits enable differentiated resource allocation (e.g., production gets higher limits)
Attack Prevented: Denial-of-wallet attacks, runaway cost from compromised keys or bugs
Prerequisites
- Organization Admin or Workspace Admin access
- Budget allocation per workspace/team
ClickOps Implementation
Step 1: Set Organization-Level Limits
- Navigate to: console.anthropic.com → Settings → Limits
- Review and configure organization-level spend limits
- For custom limits beyond Tier 4, contact Anthropic sales
Step 2: Set Workspace-Level Limits
- Navigate to the target workspace → Settings → Limits
- Configure:
- Monthly spend limit: Set below org limit (e.g., $500 for dev, $5000 for prod)
- Rate limits: RPM, ITPM, OTPM per model as needed
- Repeat for each workspace
Time to Complete: ~5 minutes per workspace
Code Implementation
Code Pack: API Script
# Check for cost anomalies — alert if any workspace exceeds a threshold
THRESHOLD_USD="${THRESHOLD_USD:-1000}"
PERIOD_DAYS="${PERIOD_DAYS:-7}"
START_DATE=$(date -d "${PERIOD_DAYS} days ago" '+%Y-%m-%dT00:00:00Z' 2>/dev/null || \
date -v-"${PERIOD_DAYS}"d '+%Y-%m-%dT00:00:00Z' 2>/dev/null)
END_DATE=$(date '+%Y-%m-%dT23:59:59Z')
info "Checking for workspaces exceeding \$${THRESHOLD_USD} in the past ${PERIOD_DAYS} days..."
COST=$(anthropic_get "/v1/organizations/cost_report?start_time=${START_DATE}&end_time=${END_DATE}&group_by=workspace") || {
fail "5.2 Failed to fetch cost report"
summary; exit 0
}
# Aggregate cost per workspace
OVER_THRESHOLD=$(echo "${COST}" | jq --argjson threshold "${THRESHOLD_USD}" '
[.data | group_by(.workspace_id)[] |
{workspace: .[0].workspace_id, total: ([.[].cost_usd] | add)} |
select(.total > $threshold)]')
ALERT_COUNT=$(echo "${OVER_THRESHOLD}" | jq 'length')
if [[ "${ALERT_COUNT}" -gt 0 ]]; then
warn "5.2 ${ALERT_COUNT} workspace(s) exceed \$${THRESHOLD_USD} threshold:"
echo "${OVER_THRESHOLD}" | jq -r '.[] | " \(.workspace) — $\(.total)"'
else
pass "5.2 No workspaces exceed \$${THRESHOLD_USD} threshold in ${PERIOD_DAYS}-day window"
fi
Validation & Testing
- Verify spend limits are set for every workspace via Console
- Run cost anomaly detection script to validate monitoring
- Test that requests return 429 when rate limits are exceeded (check
retry-afterheader)
Expected result: Every workspace has explicit spend and rate limits configured
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2, CC6.8 | System monitoring; change management |
| NIST 800-53 | SA-9, SI-4 | External system services; system monitoring |
| ISO 27001 | A.12.1.3 | Capacity management |
Compliance Quick Reference
Per-control compliance mappings appear inside each control above. For organization-level SOC 2 / NIST / ISO mappings spanning the Anthropic platform, see the Anthropic Common Controls hub.
Appendix A: References
See the Anthropic platform hub references for the shared reference list; key API/Console sources:
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026-08-03 | Split out of the monolithic Anthropic Claude guide as part of the multi-product platform restructure; carries the API key, workspace, data, and usage controls (formerly sections 2-5) renumbered into four sections. |
Contributing
Found an issue or want to improve this guide? Open an issue or PR on GitHub. Keep all code in Code Packs (no inline code blocks).