Security Architecture: Zero Trust Principles

Security Architecture: Zero Trust Principles
Traditional network security assumed that traffic inside the corporate perimeter was safe: internal services could call each other freely because they shared the same network. Zero Trust discards this assumption. In a Zero Trust architecture, no request is trusted by default — every call, from any source (internal or external), must be authenticated and authorised.
This guide covers the Zero Trust model and its practical implementation: mutual TLS for service-to-service authentication, OAuth2 and OIDC for user authentication, JWT best practices, secrets management, and the layered security patterns used in production microservice systems.
The Zero Trust Model
Zero Trust is built on three principles:
- Never trust, always verify: authentication and authorisation are required for every request, regardless of network location
- Least privilege: every identity (user or service) gets only the permissions it needs for its current task
- Assume breach: design systems as if an attacker already has access to your internal network — segment, encrypt, and audit everything
Traditional perimeter security:
Internet ────► Firewall ────► Internal network (trusted)
All services trust each other
Zero Trust:
Internet ────► API Gateway (authenticate + authorise every request)
│
â–¼ Every service-to-service call also authenticated
Service A ──mTLS──► Service B
Service B ──mTLS──► Database
Service A ──mTLS──► Service CZero Trust became the standard in 2026 because the perimeter is no longer a meaningful boundary: cloud services, remote work, third-party integrations, and compromised internal services have dissolved it.
Mutual TLS (mTLS): Service-to-Service Authentication
Standard TLS authenticates the server to the client (the server presents a certificate). Mutual TLS (mTLS) authenticates both parties — the client also presents a certificate. This ensures that only authorised services can communicate with each other, even inside the same Kubernetes cluster.
Standard TLS:
Client ──────► Server (presents certificate)
"I trust this server"
mTLS:
Client (presents certificate) ◄──► Server (presents certificate)
"I trust this server AND the server trusts me"Implementing mTLS with Istio (service mesh)
In Kubernetes, a service mesh like Istio manages mTLS transparently — application code does not change:
# Enable strict mTLS for the entire namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # Reject any non-mTLS connections# Allow only specific services to call the payment service
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: payment-service-policy
namespace: production
spec:
selector:
matchLabels:
app: payment-service
action: ALLOW
rules:
- from:
- source:
principals:
- "cluster.local/ns/production/sa/order-service"
- "cluster.local/ns/production/sa/checkout-service"
to:
- operation:
methods: ["POST"]
paths: ["/charges", "/refunds"]Only order-service and checkout-service can POST to the payment service. Any other service attempting to call it is rejected at the network layer.
Implementing mTLS without a service mesh
// Node.js HTTPS server with client certificate validation
import https from 'https';
import fs from 'fs';
const server = https.createServer({
cert: fs.readFileSync('./certs/server.crt'),
key: fs.readFileSync('./certs/server.key'),
ca: fs.readFileSync('./certs/ca.crt'), // CA that signs client certs
requestCert: true, // Require client certificate
rejectUnauthorized: true // Reject uncertified clients
}, (req, res) => {
const clientCert = req.socket.getPeerCertificate();
// Verify the service identity from the certificate
if (clientCert.subject.CN !== 'order-service') {
res.writeHead(403);
res.end('Forbidden: untrusted service');
return;
}
res.writeHead(200);
res.end('Authorised');
});OAuth2 and OIDC: User Authentication
OAuth2 is an authorisation framework. OIDC (OpenID Connect) extends it with identity — it standardises how to get information about who the user is.
OAuth2 / OIDC flow:
1. User clicks "Sign in with Google"
2. Browser redirects to Google's authorisation server
3. User authenticates with Google (your app never sees the password)
4. Google redirects back with an authorisation code
5. Your server exchanges the code for tokens
6. Google returns:
- access_token: short-lived, used to call APIs
- id_token: JWT containing user identity (email, name, sub)
- refresh_token: long-lived, used to get new access tokens// Express middleware: validate Google OIDC token
import { OAuth2Client } from 'google-auth-library';
const oauthClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
async function authenticateUser(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization header' });
}
const idToken = authHeader.substring(7);
try {
const ticket = await oauthClient.verifyIdToken({
idToken,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
if (!payload?.email_verified) {
return res.status(401).json({ error: 'Email not verified' });
}
req.user = {
id: payload.sub,
email: payload.email!,
name: payload.name
};
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}JWT Security: Implementation and Pitfalls
A JWT (JSON Web Token) is a base64-encoded JSON payload with a cryptographic signature. It enables stateless authentication — any service can verify the token without a database lookup.
JWT structure:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 ↠Header (base64)
.eyJzdWIiOiIxMjMiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZXhwIjoxNzE0MDAwMDAwfQ ↠Payload (base64)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ↠Signature
Decoded payload:
{
"sub": "123",
"email": "alice@example.com",
"role": "admin",
"exp": 1714000000 ↠Unix timestamp expiry
}Issuing and verifying JWTs
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
// Use asymmetric keys (RS256) for production — private key signs, public key verifies
const privateKey = readFileSync('./keys/jwt-private.pem');
const publicKey = readFileSync('./keys/jwt-public.pem');
function issueToken(userId: string, role: string): { accessToken: string; refreshToken: string } {
const accessToken = jwt.sign(
{ sub: userId, role },
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m', // Short-lived access token
issuer: 'api.example.com',
audience: 'app.example.com'
}
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
privateKey,
{
algorithm: 'RS256',
expiresIn: '7d', // Longer-lived refresh token
issuer: 'api.example.com'
}
);
return { accessToken, refreshToken };
}
function verifyToken(token: string): jwt.JwtPayload {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Explicitly specify — prevents 'alg: none' attacks
issuer: 'api.example.com',
audience: 'app.example.com'
}) as jwt.JwtPayload;
}JWT security pitfalls
// ⌠Using HS256 with a weak secret
jwt.sign({ sub: userId }, 'password123', { algorithm: 'HS256' });
// Anyone who obtains the secret can forge tokens
// ⌠Not validating algorithm
jwt.verify(token, publicKey); // Without { algorithms: ['RS256'] }
// Vulnerable to "alg: none" attack where a malicious token skips verification
// ⌠Long-lived access tokens
jwt.sign({ sub: userId }, key, { expiresIn: '30d' });
// Stolen token is valid for 30 days — no way to revoke it
// ✓ Short-lived access tokens + refresh token rotation
// Access token: 15 minutes
// Refresh token: 7 days, stored in HTTP-only cookie, single-use
// On refresh: issue new access token AND new refresh token, invalidate old refresh tokenRefresh token rotation
async function refreshAccessToken(refreshToken: string, res: Response) {
const payload = verifyToken(refreshToken);
// Check if refresh token has been used (token rotation)
const tokenRecord = await db.refreshTokens.findOne({ token: refreshToken });
if (!tokenRecord || tokenRecord.used) {
// Possible token theft — invalidate all sessions for this user
await db.refreshTokens.deleteMany({ userId: payload.sub });
throw new Error('Refresh token reuse detected — all sessions invalidated');
}
// Mark old token as used
await db.refreshTokens.update({ token: refreshToken }, { used: true });
// Issue new token pair
const { accessToken, refreshToken: newRefresh } = issueToken(payload.sub, payload.role);
await db.refreshTokens.save({ token: newRefresh, userId: payload.sub, used: false });
// Set new refresh token in HTTP-only cookie
res.cookie('refresh_token', newRefresh, {
httpOnly: true, // JavaScript cannot read this
secure: true, // HTTPS only
sameSite: 'strict', // Prevents CSRF
maxAge: 7 * 24 * 60 * 60 * 1000
});
return accessToken;
}Secrets Management
Hardcoding credentials in source code is one of the most common security vulnerabilities found in code repositories. Use a dedicated secrets manager:
AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(secretName: string): Promise<Record<string, string>> {
const response = await client.send(new GetSecretValueCommand({
SecretId: secretName,
VersionStage: 'AWSCURRENT'
}));
return JSON.parse(response.SecretString ?? '{}');
}
// Application startup — fetch all needed secrets
const dbSecrets = await getSecret('production/database');
const apiSecrets = await getSecret('production/third-party-apis');
const pool = new Pool({
host: dbSecrets.DB_HOST,
password: dbSecrets.DB_PASSWORD, // Never in environment variables or code
database: 'production'
});HashiCorp Vault with automatic secret rotation
# vault-policy.hcl
path "secret/data/production/database" {
capabilities = ["read"]
}
path "database/creds/app-role" {
capabilities = ["read"]
}// Vault dynamic secrets — credentials are generated on-demand and expire
import vault from 'node-vault';
const vaultClient = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
async function getDatabaseCredentials() {
// Vault generates a unique username/password for this request
// Credentials expire automatically after the lease duration (1 hour)
const creds = await vaultClient.read('database/creds/app-role');
return {
username: creds.data.username, // e.g., v-app-role-xKj3mN
password: creds.data.password
};
}Dynamic secrets eliminate the rotation problem: each application instance gets unique credentials that expire automatically. Even if credentials are compromised, they expire within hours.
Defence in Depth: Layered Security
Zero Trust is one layer of a complete security architecture:
Layer 1: Edge (WAF, DDoS protection)
- Cloudflare WAF: blocks OWASP Top 10, SQL injection, XSS
- Rate limiting: prevents brute force and credential stuffing
- Bot detection: blocks automated scanning
Layer 2: API Gateway (authentication, authorisation)
- Validates all tokens before forwarding to services
- Enforces rate limits per user/API key
- Logs all requests for audit trail
Layer 3: Service mesh (mTLS, service authorisation)
- Every service-to-service call authenticated
- Fine-grained access control per endpoint
Layer 4: Application (input validation, output encoding)
- Validate all user input: SQL injection, XSS prevention
- Parameterised queries, not string concatenation
- Content Security Policy headers
Layer 5: Data (encryption at rest, column-level encryption)
- Encrypted database volumes (TDE)
- Application-level encryption for PII (credit card numbers, SSNs)
- Field-level encryption for sensitive columnsFrequently Asked Questions
Q: What is the difference between authentication and authorisation?
Authentication answers "Who are you?" — it verifies identity (username + password, certificate, biometric). Authorisation answers "What are you allowed to do?" — it checks permissions (can this user read invoices? can this service call the payment API?). In Zero Trust, both are required for every request. OIDC/OAuth2 handles authentication; RBAC (Role-Based Access Control) or OPA (Open Policy Agent) handles authorisation.
Q: Should I use JWTs or server-side sessions?
Both are valid. JWTs are stateless (no database lookup needed), enabling horizontal scaling and reducing database load. Server-side sessions (stored in Redis) are easier to revoke — simply delete the session entry. The practical recommendation: use JWTs with short expiry (15 minutes) plus refresh token rotation for most applications. Use server-side sessions when immediate revocation is a requirement (government applications, financial systems where a compromised token must be invalidated instantly).
Q: How do I prevent SQL injection in 2026?
Always use parameterised queries or an ORM that parameterises by default. Never concatenate user input into SQL strings:
// ⌠SQL injection vulnerability
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// ✓ Parameterised query — safe
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// ✓ ORM (Prisma, TypeORM) — parameterises automatically
const user = await prisma.user.findFirst({ where: { email } });Q: What is a WAF and when do I need one?
A Web Application Firewall (WAF) sits in front of your application and filters malicious requests before they reach your code. It detects and blocks SQL injection, cross-site scripting, path traversal, and other OWASP Top 10 attacks based on request signatures. Cloudflare WAF, AWS WAF, and ModSecurity are common options. A WAF is valuable for public-facing applications, but does not replace input validation in your application code — WAF rules have false negative rates and should be one layer of defence among several.
Key Takeaway
Zero Trust security architecture eliminates the concept of a trusted internal network. Every request — user-to-service and service-to-service — requires authentication and authorisation. Mutual TLS authenticates services to each other, preventing an attacker who has compromised one service from freely calling others. Short-lived JWTs with refresh token rotation minimise the window of exposure if a token is stolen. Secrets management via Vault or AWS Secrets Manager prevents credential hardcoding and enables automatic rotation. Defence in depth — WAF at the edge, mTLS between services, parameterised queries at the application layer, encryption at the data layer — ensures that no single compromised component exposes the entire system.
Read next: Cloud Native: The 12-Factor App and Kubernetes →
Part of the Software Architecture Hub — engineering the defense.
