Zero trust for internal APIs: when the perimeter is not enough

A compromised pod inside the VPC can call every internal API that trusts network location alone. Zero trust for internal APIs means every service-to-service call carries identity, scoped authorization, and verification — not just a private IP.

Engineering5 min read
SecurityZero trustInternal APIsmTLSService mesh
Share

The breach started inside the perimeter. A compromised CI runner, a leaked service token, a lateral move from a staging environment with production network routes — the attacker never touched the public internet-facing load balancer. They called internal APIs directly: http://billing-service.internal/api/v1/invoices, http://user-service.internal/admin/export. No authentication. No authorization. The services trusted the VPC. Zero trust for internal APIs rejects that assumption: network location is a transport convenience, not proof of identity or intent.

Perimeter security — firewalls, private subnets, VPN — keeps strangers out. It does not keep compromised insiders, misconfigured services, or supply-chain attackers from moving laterally. Internal APIs are the soft underbelly of most architectures because teams secured the front door and left the hallway doors unlocked.

The castle-and-moat model fails east-west

Traditional model: public APIs authenticate users; internal APIs trust the network.

AssumptionReality
Only our services call internal APIsCompromised pods, debug tools, misrouted traffic
VPC isolation equals securityPeering, shared clusters, staging with prod routes
Internal APIs are low riskThey hold PII, billing, admin actions
mTLS is optional complexityIt is baseline identity for service-to-service

East-west traffic — service A calling service B inside the same cluster — often exceeds north-south traffic in volume. Securing only ingress ignores the majority of attack surface.

"It's internal" is not an authorization model. It is an absence of one.

OAuth scopes and authorization design applies to internal services too: a billing service calling user-service should present credentials scoped to exactly what it needs — not a god-token that reads every user record because "it's all internal."

Zero trust principles for internal APIs

Zero trust is not a product. It is a set of constraints applied consistently:

1. Never trust, always verify

Every request — including service-to-service — must present verifiable identity. Options: mTLS with service certificates, signed JWTs with short TTL, SPIFFE/SPIRE workload identity. The caller proves who it is; the callee verifies before executing.

2. Least privilege per call

Authorization is not binary "internal vs external." A service account for the notification worker gets users:read:email on user-service — not users:admin. Scope design mirrors external OAuth: narrow grants, auditable, revocable.

3. Assume breach

Design so a compromised service cannot exfiltrate the entire datastore. Rate limits on sensitive endpoints, audit logs on admin paths, segmentation so billing cannot call arbitrary user-service routes.

4. Encrypt and authenticate in transit

TLS everywhere — including inside the cluster. Plain HTTP on *.cluster.local is convenient and readable by any process with network access in the namespace.

5. Continuous verification

Short-lived credentials. Certificate rotation. Revocation when a service is decommissioned or compromised. Long-lived static API keys in environment variables are the internal equivalent of password reuse.

Implementation layers from pragmatic to comprehensive

Teams adopt zero trust incrementally. The layers stack:

LayerMechanismEffortCoverage
Network policyKubernetes NetworkPolicy, security groupsLowLimits which pods talk; no identity
mTLSService mesh (Istio, Linkerd) or cert-manager per serviceMediumCryptographic caller identity
Token authJWT or opaque token per request, validated at gateway/sidecarMediumIdentity + optional scopes
Policy engineOPA, Cedar, custom RBAC at API layerMedium–HighFine-grained authorization
Service mesh policymTLS + authorization policies in meshHighEnd-to-end east-west

Starting point for most teams: authenticate internal calls before optimizing policy engines. A shared internal API gateway that validates service JWTs beats a mesh nobody configured correctly.

Network policy alone is necessary but insufficient — it answers "can pod A reach pod B's port?" not "should this specific request be allowed?"

Common internal API anti-patterns

God service tokens. One INTERNAL_API_KEY in twelve services. Compromise one, own all. Per-service credentials with scoped permissions.

Admin routes without admin auth. /internal/admin/reindex callable from any pod in the namespace. Separate admin surface, stronger auth, audit log.

Trusting X-Forwarded-User from internal callers. Any service can forge headers. User identity must come from validated tokens, not trusted headers on internal hops.

Skipping auth in "dev" clusters that mirror prod topology. Staging with production data and no internal auth is a breach waiting for a misclick.

Supply chain as internal caller. Third-party webhooks and SaaS integrations often land in internal queues. Treat them as untrusted until verified — related to supply chain attacks that target trust.

How should teams roll out zero trust for internal APIs?

These questions scope the first sprint versus the full program.

Is mTLS required for every internal call?

Not always on day one. Minimum bar: some cryptographic identity on sensitive paths (billing, PII, admin). Expand to full mTLS as mesh or cert infrastructure matures. Plain HTTP internal calls to read-only health endpoints are lower priority than calls that mutate financial state.

How do scopes work for service-to-service calls?

Mirror user OAuth: issue service tokens with audience (aud), issuer (iss), scopes (billing:invoice:read), and short expiry. Callee validates all claims. Document which services may hold which scopes — same discipline as OAuth scope design.

What about legacy services that cannot add auth quickly?

API gateway or sidecar proxy in front of legacy service: terminate mTLS, inject validated identity header (signed by gateway, not forgeable), legacy service trusts only gateway-origin requests. Bridge pattern until native auth ships.

A common argument runs the other way

The opposing view holds that zero trust internal APIs add latency, operational burden, and debugging friction — that VPC isolation and network policies are sufficient for teams without nation-state adversaries.

Operational cost is real. So is lateral movement after a single compromised credential — which happens to startups and enterprises alike. Pragmatic zero trust starts on high-value paths: admin APIs, PII exports, payment mutations. Full mesh can wait; "no auth because internal" cannot.

Debug friction is managed with service identity in traces and structured audit logs — knowing which service called which endpoint is easier with identity than with anonymous internal traffic.

Key takeaways

  • VPC membership is not identity — verify every internal API call.
  • East-west traffic needs the same auth discipline as public APIs.
  • Least-privilege scopes apply to service accounts, not only users.
  • mTLS or signed tokens are baseline; network policy alone is insufficient.
  • God internal API keys are a single point of compromise.
  • Roll out on sensitive paths first; bridge legacy with gateway proxies.

Conclusion

The perimeter kept attackers out until one got in. Internal APIs designed for trust-on-network become highways for lateral movement. Zero trust for internal APIs means every call answers: who is calling, are they allowed to do this, and is the channel integrity-protected?

The audit starts with inventory: list internal endpoints, mark which require auth today, mark which hold sensitive data. The gap between those lists is the roadmap. Close it before the compromise, not after.

Related articles

Command Palette

Search for a command to run...