DEV Community

Cover image for The Architecture of Zero-Trust APIs
Derek Mwale
Derek Mwale

Posted on

The Architecture of Zero-Trust APIs

There is a dangerous assumption hidden inside many APIs.

It usually looks innocent.

The request arrives from our frontend.

It came through HTTPS.

The user has a valid JWT.

The token was issued by our identity provider.

Therefore, the request is trusted.

This reasoning feels reasonable until you build a system large enough to discover how wrong it can be.

Because authentication answers only one question:

Who are you?

It does not automatically answer:

What are you allowed to do?

And even that isn't enough.

A serious API needs to ask:

What exactly are you trying to do, against which resource, under which conditions, using which privileges, and why should I trust this request?

That is where zero-trust architecture becomes interesting.

Zero trust is often presented as a security slogan:

Never trust, always verify.

But that phrase is only the beginning.

The deeper architectural idea is that trust should not be inherited merely because a request crossed a network boundary or possesses a valid credential.

Every request is an opportunity to verify identity, authorization, context, integrity, and intent.

The API becomes a security boundary rather than a passive transport mechanism.

And once you design APIs this way, authentication stops being the center of your security architecture.

Authorization becomes the center.


The Death of the Trusted Network

Traditional enterprise systems often evolved around a simple model:

Internet
   ↓
Firewall
   ↓
Trusted Network
   ↓
Internal Services
Enter fullscreen mode Exit fullscreen mode

The assumption was that once something entered the internal network, it could receive a higher level of trust.

This made sense when applications were mostly:

Browser
   ↓
Web Server
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

But modern architectures look different.

You might have:

Mobile App
      ↓
API Gateway
      ↓
Authentication Service
      ↓
User Service
      ↓
Order Service
      ↓
Payment Service
      ↓
Notification Service
      ↓
External Provider
Enter fullscreen mode Exit fullscreen mode

Add Kubernetes.

Add cloud infrastructure.

Add third-party integrations.

Add background workers.

Add internal APIs.

Add serverless functions.

Add service accounts.

Suddenly, the idea of an "internal network" being inherently trustworthy becomes almost meaningless.

An attacker who compromises one service doesn't necessarily need to attack the entire system directly.

They can move laterally.

The real security problem becomes:

If one component is compromised, how much authority does it have?

Zero-trust architecture tries to minimize the answer.


Zero Trust Starts With a Different Question

Traditional security often asks:

Is this request coming from a trusted place?

Zero trust asks:

What evidence do I have that this specific request should be allowed?

That distinction is enormous.

Imagine:

GET /api/users/42
Authorization: Bearer eyJ...
Enter fullscreen mode Exit fullscreen mode

The token is valid.

The user is authenticated.

But should the user be allowed to access user 42?

Not necessarily.

The token might represent:

User 17
Enter fullscreen mode Exit fullscreen mode

while the request targets:

User 42
Enter fullscreen mode Exit fullscreen mode

Authentication succeeded.

Authorization failed.

A zero-trust API understands that these are different operations.


Authentication Is Not Authorization

This distinction deserves repetition because so many API vulnerabilities come from confusing these concepts.

Authentication answers:

Who are you?
Enter fullscreen mode Exit fullscreen mode

Authorization answers:

What are you allowed to do?
Enter fullscreen mode Exit fullscreen mode

Identity might look like:

```json id="d7p4u1"
{
"sub": "user_17"
}




Authorization requires additional reasoning:



```text
Can user_17
read user_42?
Enter fullscreen mode Exit fullscreen mode

The API should never assume:

authenticated = authorized
Enter fullscreen mode Exit fullscreen mode

That equation is false.

A user can be authenticated and still be forbidden from:

  • reading another user's data
  • modifying another tenant's resources
  • deleting records
  • accessing administrative endpoints
  • viewing financial information
  • invoking privileged operations

Zero-trust API design makes this distinction explicit.


Every Request Carries a Security Context

A useful mental model is to think of every request as carrying a security context.

Conceptually:

Request
   ↓
Identity
   ↓
Authentication
   ↓
Authorization
   ↓
Resource
   ↓
Action
   ↓
Policy
   ↓
Decision
Enter fullscreen mode Exit fullscreen mode

For example:

Identity:
user_17

Action:
read

Resource:
invoice_839

Tenant:
company_42

Device:
registered

Token:
valid

Risk:
low

Decision:
allow
Enter fullscreen mode Exit fullscreen mode

Or:

Identity:
user_17

Action:
delete

Resource:
invoice_839

Tenant:
company_42

Role:
viewer

Decision:
deny
Enter fullscreen mode Exit fullscreen mode

The important thing is that authorization becomes a decision based on context.

Not simply:

if token:
    allow()
Enter fullscreen mode Exit fullscreen mode

The API Gateway Is Not the Security Boundary

One of the most common mistakes in distributed systems is assuming that the API gateway handles security for everything behind it.

Imagine:

Internet
   ↓
API Gateway
   ↓
Order Service
   ↓
Payment Service
Enter fullscreen mode Exit fullscreen mode

The gateway validates the user's JWT.

Excellent.

But then the Order Service calls Payment Service.

Should Payment Service automatically trust the Order Service?

No.

This is where zero trust becomes architectural rather than cosmetic.

Each service should validate the identity and authority of the caller appropriate to its responsibilities.

The gateway can provide an important security layer.

It should not become the only security layer.


Defense in Depth for APIs

A strong API architecture might look like:

Client
  ↓
TLS
  ↓
Gateway
  ↓
Authentication
  ↓
Rate Limiting
  ↓
Authorization
  ↓
Service
  ↓
Resource Authorization
  ↓
Database Constraints
Enter fullscreen mode Exit fullscreen mode

Every layer contributes something.

TLS protects communication.

Authentication establishes identity.

Rate limiting controls abuse.

Authorization determines permitted actions.

Resource-level checks prevent unauthorized object access.

Database constraints protect invariants.

The architecture assumes that one layer can fail.

That is the essence of defense in depth.


Never Trust the Client

A frontend application is not a security boundary.

This sounds obvious.

Yet APIs routinely make decisions based on client-provided fields.

Imagine:

```json id="xj5m12"
{
"user_id": 42,
"role": "admin"
}




The frontend sends it.

The backend accepts it.

Now the attacker changes:



```text
role = admin
Enter fullscreen mode Exit fullscreen mode

This isn't sophisticated hacking.

The server simply trusted data that the client was never entitled to control.

A zero-trust API treats the client as potentially hostile.

The client can provide claims.

The server decides which claims are authoritative.


Resource Ownership Must Be Verified

One of the most important API security principles is object-level authorization.

Consider:

GET /api/orders/123
Enter fullscreen mode Exit fullscreen mode

Authentication tells you who is making the request.

But the API must still determine:

Does this user own order 123?
Enter fullscreen mode Exit fullscreen mode

A dangerous implementation might do:

```python id="b1w5c8"
order = Order.objects.get(id=order_id)
return order




A safer model is conceptually:



```python id="n3a1yr"
order = Order.objects.get(
    id=order_id,
    customer=request.user
)
Enter fullscreen mode Exit fullscreen mode

The difference is profound.

The first query asks:

Does the object exist?

The second asks:

Does this object exist and belong to the requester?

This is zero-trust thinking at the resource level.


Multi-Tenant Systems Make This Even More Important

Consider a SaaS application.

You have:

Tenant A
  ├── Users
  ├── Orders
  └── Invoices

Tenant B
  ├── Users
  ├── Orders
  └── Invoices
Enter fullscreen mode Exit fullscreen mode

A user from Tenant A should never access Tenant B's data.

A naive API might query:

```sql id="z8v8c9"
SELECT *
FROM invoices
WHERE id = 991;




The safer conceptual model is:



```sql id="s6w2cs"
SELECT *
FROM invoices
WHERE id = 991
AND tenant_id = current_tenant;
Enter fullscreen mode Exit fullscreen mode

The tenant boundary must be part of the data access logic.

Otherwise, one leaked identifier can become a cross-tenant data breach.

This is why authorization cannot live entirely at the controller layer.

It needs to be reflected throughout the architecture.


Authorization Should Follow the Resource

A common mistake is thinking about authorization only in terms of roles.

For example:

admin
editor
viewer
Enter fullscreen mode Exit fullscreen mode

Roles are useful.

But roles alone are often insufficient.

Consider:

Alice = manager
Bob = manager
Enter fullscreen mode Exit fullscreen mode

Both have the same role.

But Alice manages:

Department A
Enter fullscreen mode Exit fullscreen mode

while Bob manages:

Department B
Enter fullscreen mode Exit fullscreen mode

Both are managers.

Their resource permissions differ.

This is where systems move from simple RBAC toward more contextual authorization models.

The policy might become:

allow(user, action, resource)
Enter fullscreen mode Exit fullscreen mode

rather than:

allow(role, action)
Enter fullscreen mode Exit fullscreen mode

The resource matters.

The relationship matters.

The context matters.


Zero Trust Is Context-Aware

Imagine a financial API.

A user normally accesses the system from:

Lusaka
Enter fullscreen mode Exit fullscreen mode

during:

08:00–18:00
Enter fullscreen mode Exit fullscreen mode

Then a request arrives from an unfamiliar device at 03:00.

The credentials are valid.

Should the API automatically allow a high-value transfer?

A mature zero-trust architecture can consider additional signals:

identity
device
location
time
transaction value
recent behavior
authentication strength
risk level
Enter fullscreen mode Exit fullscreen mode

The decision could become:

low-risk request → allow

medium-risk request → require additional verification

high-risk request → deny or step-up authentication
Enter fullscreen mode Exit fullscreen mode

This is much closer to how modern security systems need to behave.


Short-Lived Credentials Are Powerful

Long-lived credentials create long-lived problems.

Suppose an access token remains valid for:

30 days
Enter fullscreen mode Exit fullscreen mode

If stolen, the attacker potentially has 30 days of access.

A zero-trust architecture generally prefers reducing credential lifetime where practical.

For example:

Access token → short lifetime
Refresh mechanism → controlled
Privileged operations → additional verification
Enter fullscreen mode Exit fullscreen mode

Short-lived credentials reduce the window of opportunity.

But token expiration is not enough.

A token being valid doesn't mean every request made with it should be allowed.

Expiration answers:

Is this credential still valid?

Authorization answers:

Should this operation be allowed?

Again, different questions.


Service-to-Service Authentication

Zero trust becomes especially interesting inside the backend.

Suppose:

Order Service
     ↓
Payment Service
Enter fullscreen mode Exit fullscreen mode

The Payment Service should not simply trust every request originating from the private network.

Instead, services can authenticate one another using mechanisms such as:

  • mutual TLS
  • workload identities
  • signed tokens
  • short-lived service credentials
  • cloud identity mechanisms

Now the Payment Service can establish:

Caller:
order-service

Identity:
verified

Requested operation:
create-payment

Authority:
allowed
Enter fullscreen mode Exit fullscreen mode

This creates explicit trust relationships.

The network location becomes much less important.


Service Identity Should Be First-Class

A useful architecture treats services as identities.

Instead of:

10.0.2.17 → trusted
Enter fullscreen mode Exit fullscreen mode

think:

order-service → authenticated workload
Enter fullscreen mode Exit fullscreen mode

IP addresses change.

Containers restart.

Pods move.

Infrastructure scales.

Service identity is more stable than network location.

This is particularly important in cloud-native systems.

Zero trust is essentially saying:

Don't trust the packet because of where it came from. Authenticate the workload that sent it.


The Database Should Participate in Zero Trust

Security shouldn't end at the API.

Suppose the API has a bug.

Can the database help prevent catastrophic damage?

Yes.

Use:

  • least-privilege database users
  • separate credentials by service
  • row-level security where appropriate
  • foreign keys
  • constraints
  • restricted schemas
  • transaction boundaries
  • auditing
  • encryption

Imagine three services:

Order Service
Payment Service
Analytics Service
Enter fullscreen mode Exit fullscreen mode

Why should Analytics Service have permission to:

DELETE FROM payments
Enter fullscreen mode Exit fullscreen mode

It shouldn't.

The principle of least privilege applies to internal services just as much as it applies to human users.


Least Privilege Is the Heart of Zero Trust

Zero trust is not simply about checking credentials more often.

It is about reducing authority.

Imagine a user has:

20 permissions
Enter fullscreen mode Exit fullscreen mode

but only needs:

3
Enter fullscreen mode Exit fullscreen mode

The remaining 17 are unnecessary attack surface.

The same applies to services.

If a notification service only needs to:

read notification preferences
write delivery status
Enter fullscreen mode Exit fullscreen mode

it shouldn't have access to:

payments
password hashes
administrative users
Enter fullscreen mode Exit fullscreen mode

Least privilege limits blast radius.

If something gets compromised, the attacker inherits less power.


Blast Radius Matters More Than Perfect Security

No system is perfectly secure.

A better goal is:

When something fails, how much can the attacker reach?

Imagine:

Compromised Notification Service
          ↓
Read entire database
          ↓
Game over
Enter fullscreen mode Exit fullscreen mode

Now compare:

Compromised Notification Service
          ↓
Can only read notification records
          ↓
Limited damage
Enter fullscreen mode Exit fullscreen mode

Zero trust assumes compromise is possible.

The architecture is designed around containment.

This is one of the most mature ways to think about security.


Rate Limiting Is Part of Trust

A valid identity can still abuse an API.

Suppose:

user_42
Enter fullscreen mode Exit fullscreen mode

has a valid token.

They send:

100,000 requests/second
Enter fullscreen mode Exit fullscreen mode

Authentication succeeds.

But the workload is clearly abnormal.

A zero-trust API should treat behavior as part of security.

Rate limiting can operate at multiple levels:

IP
User
Tenant
API key
Endpoint
Service
Device
Enter fullscreen mode Exit fullscreen mode

And limits can vary by operation.

For example:

GET /products
→ high limit

POST /payments
→ low limit

POST /login
→ very low limit
Enter fullscreen mode Exit fullscreen mode

Not all actions carry the same risk.


Sensitive Operations Need Stronger Verification

Not every endpoint deserves the same security posture.

Consider:

GET /profile
Enter fullscreen mode Exit fullscreen mode

versus:

POST /transfer-money
Enter fullscreen mode Exit fullscreen mode

They are not equivalent.

The second operation could require:

  • recent authentication
  • MFA
  • transaction signing
  • device verification
  • risk evaluation
  • stricter rate limits
  • additional authorization

This is sometimes called step-up authentication.

The idea is simple:

The more dangerous the action, the stronger the evidence required.


Zero Trust and API Design

Security should influence API design itself.

Compare:

PATCH /users/42
Enter fullscreen mode Exit fullscreen mode

with:

POST /users/42/change-email
Enter fullscreen mode Exit fullscreen mode

The second endpoint expresses a specific business operation.

That gives the server more context.

It can require:

current password
new email verification
MFA
rate limit
audit event
Enter fullscreen mode Exit fullscreen mode

Similarly:

POST /orders/42/cancel
Enter fullscreen mode Exit fullscreen mode

is more explicit than:

PATCH /orders/42
{
  "status": "cancelled"
}
Enter fullscreen mode Exit fullscreen mode

The more meaningful the operation, the easier it becomes to apply meaningful authorization.


Security Events Should Be Immutable

If an administrator changes permissions, don't merely update:

role = admin
Enter fullscreen mode Exit fullscreen mode

Record the event.

For example:

PermissionGranted
actor = admin_17
target = user_42
permission = billing.read
timestamp = ...
request_id = ...
Enter fullscreen mode Exit fullscreen mode

This creates accountability.

A security system without history is difficult to trust.

You need to know not only:

What is the user's permission now?
Enter fullscreen mode Exit fullscreen mode

but:

How did they get it?
Who granted it?
When?
From which operation?
Enter fullscreen mode Exit fullscreen mode

Security is fundamentally about evidence.


Observability Becomes Part of Security

Logs should answer questions such as:

Who made this request?
Which service processed it?
What resource was accessed?
Which authorization policy was evaluated?
Was access granted or denied?
Why?
What request ID connects the events?
Enter fullscreen mode Exit fullscreen mode

This is where distributed tracing becomes useful.

Imagine a request:

Request ID:
req_839201
Enter fullscreen mode Exit fullscreen mode

Then:

Gateway
   ↓
Order Service
   ↓
Payment Service
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Every component preserves the request context.

When something suspicious happens, you can reconstruct the path.

Without this, security investigations become guesswork.


Fail Closed

One of the simplest zero-trust principles is:

When authorization cannot be established, deny access.

Suppose the authorization service is unavailable.

A dangerous system might say:

Authorization unavailable.
Let's allow the request.
Enter fullscreen mode Exit fullscreen mode

That converts an outage into a security vulnerability.

A safer approach is:

Authorization unavailable.
Sensitive operation denied.
Enter fullscreen mode Exit fullscreen mode

Availability suffers.

But security remains intact.

For certain low-risk operations, carefully designed fallback behavior may be acceptable.

For privileged actions, uncertainty should generally mean denial.


Zero Trust Is Not Zero Performance

A common objection is:

If we verify everything, won't the system become slow?

It can, if designed badly.

But security decisions can be optimized.

Use:

  • local verification for signed tokens
  • short-lived cached policy decisions
  • efficient authorization indexes
  • sidecar or local policy engines
  • batched permission checks
  • precomputed relationships
  • database-level constraints

The goal isn't to perform an expensive security ceremony for every request.

The goal is to make authorization explicit without turning it into the bottleneck.

Security architecture is still architecture.


Policy Should Be Separated From Business Logic

Imagine writing authorization everywhere:

```python id="4qv5yz"
if user.role == "admin":
...




Eventually, your application becomes full of security rules scattered across controllers.

A cleaner architecture can centralize policy.

Conceptually:



```text
Request
   ↓
Policy Engine
   ↓
Allow / Deny
   ↓
Business Logic
Enter fullscreen mode Exit fullscreen mode

The policy engine might reason about:

subject
action
resource
context
Enter fullscreen mode Exit fullscreen mode

For example:

subject = user_42
action = read
resource = invoice_839
tenant = tenant_7
Enter fullscreen mode Exit fullscreen mode

Then:

ALLOW
Enter fullscreen mode Exit fullscreen mode

or:

DENY
Enter fullscreen mode Exit fullscreen mode

This separation makes policies easier to reason about, test, audit, and evolve.


The Architecture Becomes a Graph of Trust

A traditional architecture often looks like:

Client → API → Database
Enter fullscreen mode Exit fullscreen mode

A zero-trust architecture is more accurately represented as:

Client
  ↓
Identity Provider
  ↓
API Gateway
  ↓
Service A
  ↓
Service B
  ↓
Database
Enter fullscreen mode Exit fullscreen mode

But each arrow represents a trust decision.

Can client call API?
Can API call Service A?
Can Service A call Service B?
Can Service B read this resource?
Can Service B modify this record?
Enter fullscreen mode Exit fullscreen mode

Trust becomes explicit.

The architecture becomes a graph of permissions rather than a network of implicit assumptions.


Zero Trust Is a Mental Model

This is perhaps the most important part.

Zero trust isn't a product.

It isn't:

Install security tool X
Enter fullscreen mode Exit fullscreen mode

and suddenly become secure.

It is an architectural mindset.

When designing an endpoint, ask:

Who is calling this?

Then:

How do I know?

Then:

What are they trying to do?

Then:

Which resource are they touching?

Then:

Are they allowed to perform that action on that resource?

Then:

What happens if this service has already been compromised?

Then:

Can I reconstruct what happened afterward?

Those questions produce better APIs.


A Practical Zero-Trust API Architecture

A mature API might therefore look conceptually like:

                  ┌─────────────────┐
                  │ Identity System │
                  └────────┬────────┘
                           │
                           ↓
Client ──TLS──→ API Gateway
                    │
                    ├── Rate Limit
                    ├── Token Validation
                    ├── Request Validation
                    │
                    ↓
              Authorization
                    │
                    ↓
              Business Service
                    │
          ┌─────────┴──────────┐
          ↓                    ↓
   Resource Policy        Audit Event
          │                    │
          ↓                    ↓
      Database              Event Store
Enter fullscreen mode Exit fullscreen mode

And for service-to-service communication:

Service A
   │
   │ authenticated identity
   ↓
Service B
   │
   │ authorization decision
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

No component gets implicit trust simply because it is inside the network.


The Final Principle

The deepest lesson of zero-trust API architecture is surprisingly simple:

Trust should be earned per operation, not inherited from location.

A request isn't trusted because it came from your frontend.

A service isn't trusted because it lives inside Kubernetes.

A token isn't trusted to perform every operation because it is valid.

A user isn't trusted with every resource because they are authenticated.

An internal network isn't trusted because it is private.

Everything needs context.

Everything needs boundaries.

Everything important needs evidence.

The API should continuously answer:

Who are you?

What are you trying to do?

What are you trying to access?

Why are you allowed to do it?

Under what conditions?

What happens if you're compromised?

Can we prove what happened afterward?
Enter fullscreen mode Exit fullscreen mode

That is the real architecture of zero-trust APIs.

It isn't about creating a system where nobody is trusted.

It is about creating a system where trust is explicit, limited, verifiable, and revocable.

And that changes everything.

It changes how we design endpoints.

It changes how services communicate.

It changes how databases are structured.

It changes how tokens are issued.

It changes how permissions are modeled.

It changes how logs are written.

It changes how failures are handled.

Most importantly, it changes how we think about security.

The old architecture says:

You are inside.
Therefore, I trust you.
Enter fullscreen mode Exit fullscreen mode

The zero-trust architecture says:

You are here.
Now prove what you're allowed to do.
Enter fullscreen mode Exit fullscreen mode

That is a much harder system to build.

But it is also a much harder system to compromise.

And in a world where applications are no longer isolated machines but enormous networks of APIs, services, workloads, identities, databases, queues, and third-party systems, that distinction is no longer optional.

**The network is not the boundary anymore.

The request is.**

Top comments (0)