Zero Trust Security Model for Developers
What zero trust means beyond the buzzword: identity-based access, mutual TLS, network microsegmentation and applying it at the application layer.

The traditional security model is a castle with a moat: a strong perimeter (firewall, VPN) protecting a trusted internal network. Once you are inside, you can access everything. This model fails catastrophically when an attacker breaches the perimeter — which they inevitably do via phishing, compromised credentials, or supply chain attacks.
Zero trust flips the model: never trust, always verify. Every request — whether it comes from inside or outside the network — must be authenticated, authorized, and encrypted. There is no trusted internal network.
The Core Principles
Zero trust is not a product you buy. It is a set of architectural principles that change how you think about access control, network communication, and identity verification.
// Traditional security: trust the network
interface CastleAndMoat {
perimeter: 'firewall + VPN';
internalTraffic: 'trusted by default'; // ← The vulnerability
authCheck: 'at the perimeter only';
// Once inside, an attacker has lateral movement to everything
}
// Zero trust: trust nothing, verify everything
interface ZeroTrustModel {
perimeter: 'does not exist';
allTraffic: 'encrypted and authenticated';
authCheck: 'at every service, every request';
principle: 'least privilege — minimum access for minimum time';
assumption: 'the network is already compromised';
}
// Zero trust principles as code
const zeroTrustPrinciples = {
verifyExplicitly: {
rule: 'Authenticate and authorize every request',
how: 'Use identity tokens (JWT, mTLS) — not network location',
},
leastPrivilege: {
rule: 'Grant minimum required access for minimum time',
how: 'Short-lived tokens, scoped permissions, JIT access',
},
assumeBreach: {
rule: 'Design as if attackers are already inside',
how: 'Encrypt internal traffic, segment networks, log everything',
},
};Identity-Based Access (Not Network-Based)
In a zero trust model, access decisions are based on identity — who you are, what device you are using, and what context you are in — not on which network you are connected to.
// ❌ Network-based access control
function handleRequest(req: Request): Response {
// If the request comes from an internal IP, trust it
const clientIp = req.headers.get('x-forwarded-for');
if (isInternalIp(clientIp)) {
// No authentication required — "it's internal"
return processRequest(req); // Attackers love this
}
return new Response('Forbidden', { status: 403 });
}
// ✅ Identity-based access control
async function handleRequest(req: Request): Promise<Response> {
// Every request must present a valid identity token
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
// Verify the token regardless of network origin
const identity = await verifyToken(token);
if (!identity) {
return new Response('Invalid token', { status: 401 });
}
// Check if this identity has access to this specific resource
const hasAccess = await checkPermission(identity, req.url, req.method);
if (!hasAccess) {
return new Response('Forbidden', { status: 403 });
}
return processRequest(req, identity);
}Mutual TLS Between Services
In traditional architectures, internal services communicate over plain HTTP. In a zero trust architecture, every service-to-service connection is authenticated using mutual TLS (mTLS) — both sides present certificates to prove their identity.
// Setting up mTLS in Node.js
import { createServer, request } from 'https';
import { readFileSync } from 'fs';
// Server: requires client certificates
const server = createServer({
key: readFileSync('server-key.pem'),
cert: readFileSync('server-cert.pem'),
ca: readFileSync('ca-cert.pem'), // Trust only certs signed by this CA
requestCert: true, // Require client certificate
rejectUnauthorized: true, // Reject connections without valid certs
}, (req, res) => {
// The client's identity is in the certificate
const clientCert = (req as any).socket.getPeerCertificate();
const clientService = clientCert.subject.CN; // e.g., "billing-service"
console.log(`Authenticated request from: ${clientService}`);
// Authorize based on the service identity
if (!isAllowedCaller(clientService, req.url)) {
res.writeHead(403);
res.end('Service not authorized for this endpoint');
return;
}
handleRequest(req, res, clientService);
});
// Client: presents its own certificate
function callService(url: string): Promise<unknown> {
return new Promise((resolve, reject) => {
const req = request(url, {
key: readFileSync('client-key.pem'),
cert: readFileSync('client-cert.pem'),
ca: readFileSync('ca-cert.pem'),
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', reject);
req.end();
});
}Service-Level Authorization
Authentication tells you who is making the request. Authorization tells you whether they are allowed to do what they are requesting. In zero trust, every service enforces its own authorization — no relying on upstream services to have checked permissions.
// Policy-based authorization at every service
interface AccessPolicy {
service: string; // Which service can access
resource: string; // Which endpoint/resource
actions: string[]; // Which HTTP methods
conditions?: Condition[]; // Additional constraints
}
const policies: AccessPolicy[] = [
{
service: 'api-gateway',
resource: '/api/users/*',
actions: ['GET'],
conditions: [{ type: 'time', allowedHours: { start: 6, end: 22 } }],
},
{
service: 'billing-service',
resource: '/api/users/*/payment-methods',
actions: ['GET'],
// Billing can read payment methods but not user profiles
},
{
service: 'admin-service',
resource: '/api/users/*',
actions: ['GET', 'PUT', 'DELETE'],
conditions: [{ type: 'approval', requiredApprovers: 1 }],
},
];
function isAllowedCaller(
callerService: string,
requestPath: string,
method: string = 'GET'
): boolean {
return policies.some(policy =>
policy.service === callerService &&
matchesPath(requestPath, policy.resource) &&
policy.actions.includes(method) &&
(policy.conditions?.every(c => evaluateCondition(c)) ?? true)
);
}// ❌ Trusting upstream services to enforce authorization
function userService(req: Request): Response {
// "The API gateway already checked permissions"
// But what if the request bypasses the gateway?
// What if the gateway has a bug in its auth logic?
return getUser(req.params.id); // No authorization check here
}
// ✅ Every service enforces its own authorization
async function userService(req: AuthenticatedRequest): Promise<Response> {
// Verify the caller's identity (from mTLS or token)
const caller = req.identity;
// Check if THIS specific service allows THIS action
if (!isAllowedCaller(caller.service, req.url, req.method)) {
auditLog.warn('Unauthorized access attempt', {
caller: caller.service,
resource: req.url,
method: req.method,
});
return new Response('Forbidden', { status: 403 });
}
return getUser(req.params.id);
}Short-Lived Credentials
Long-lived API keys and service account passwords are a major risk. If compromised, they provide indefinite access. Zero trust uses short-lived, automatically rotated credentials.
// Token lifecycle management
interface TokenPolicy {
maxLifetime: number; // 15 minutes for access tokens
refreshWindow: number; // Can refresh within 5 minutes of expiry
rotationSchedule: string; // Service credentials rotate daily
}
// Short-lived service-to-service tokens
async function getServiceToken(
targetService: string
): Promise<string> {
const token = await tokenService.issue({
issuer: 'user-service',
audience: targetService,
expiresIn: '15m', // 15 minutes — not days or months
scopes: ['read:users'], // Minimum required scope
});
return token;
}
// The token service issues short-lived JWTs
interface ServiceToken {
iss: string; // Issuing service
aud: string; // Target service
exp: number; // Expires in 15 minutes
iat: number; // Issued at
scopes: string[]; // Minimum required permissions
jti: string; // Unique ID for revocation tracking
}Logging and Monitoring
Zero trust requires comprehensive logging because you can no longer assume that internal traffic is safe. Every authentication decision, authorization check, and access attempt must be logged for audit and anomaly detection.
// Audit logging for zero trust
interface AuditLogEntry {
timestamp: string;
requestId: string;
caller: {
service: string;
identity: string;
ipAddress: string;
};
target: {
service: string;
resource: string;
method: string;
};
decision: 'allowed' | 'denied';
reason: string;
policyMatched?: string;
}
function logAccessDecision(entry: AuditLogEntry): void {
// All access decisions are logged — both allowed and denied
// Denied requests are especially important for detecting attacks
console.log(JSON.stringify(entry));
// Alert on anomalies
if (entry.decision === 'denied') {
anomalyDetector.track(entry);
}
}Key Takeaways
- Never trust, always verify — authenticate and authorize every request regardless of network origin
- Use identity, not IP addresses — access decisions should be based on who is making the request, verified by tokens or certificates
- Enforce authorization at every service — do not rely on upstream services to have checked permissions
- Use mutual TLS between services — both client and server authenticate each other with certificates
- Issue short-lived credentials — 15-minute access tokens limit the blast radius of a compromised credential
- Log every access decision — comprehensive audit trails enable anomaly detection and forensic investigation


