Introduction
A Cloud Access Security Broker (CASB) acts as a smart intermediary between your users and cloud services (SaaS, IaaS like AWS S3, Google Drive). It enforces security policies: authentication, data leak prevention (DLP), granular logging, and blocking non-compliant access. In 2026, with rising zero-day threats and regulations like DORA or NIS2, a custom CASB is essential for companies without budgets for Netskope or Zscaler.
This intermediate tutorial guides you through building a CASB proxy with Node.js and TypeScript. We'll create a server that intercepts HTTP requests to AWS S3, verifies JWT authentication, scans payloads for sensitive keywords (basic DLP), logs everything, and forwards or blocks. Why it matters: 70% of cloud breaches stem from uncontrolled access (Gartner 2025 report). By the end, you'll have a production-ready, Docker-scalable prototype. Estimated time: 30 min. (128 words)
Prerequisites
- Node.js 20+ and npm/yarn installed
- Intermediate knowledge of TypeScript and Express.js
- AWS account with S3 access (create a test bucket)
- Environment variables:
JWT_SECRET,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,S3_BUCKET - Tools: Docker for optional deployment
- TS-supporting IDE (VS Code recommended)
Project Initialization
mkdir casb-proxy && cd casb-proxy
npm init -y
npm install express http-proxy-middleware jsonwebtoken aws-sdk @types/express @types/jsonwebtoken @types/node typescript ts-node nodemon
npm install -D @types/http-proxy-middleware
npx tsc --init
touch src/server.ts src/policies.ts src/logger.ts .envThis command sets up a Node.js project, installs Express for the server, http-proxy-middleware for forwarding S3 requests, jsonwebtoken for auth, AWS SDK for S3 interactions, and TypeScript. Types ensure static safety. Avoid outdated versions to prevent CVE-2025 vulnerabilities.
Project Structure
Your folder structure:
casb-proxy/
├── src/
│ ├── server.ts # Main server and proxy
│ ├── policies.ts # DLP and auth logic
│ └── logger.ts # Structured logging
├── .env # Secrets
├── tsconfig.json # TS config
└── package.json
The flow: Client → CASB (auth + DLP) → AWS S3. Think of it like a hotel doorman checking ID and scanning luggage before room access.
TypeScript and package.json Configuration
{
"name": "casb-proxy",
"version": "1.0.0",
"main": "src/server.ts",
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.19.2",
"http-proxy-middleware": "^3.0.0",
"jsonwebtoken": "^9.0.2",
"@aws-sdk/client-s3": "^3.600.0",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^22.5.5",
"typescript": "^5.5.4",
"ts-node": "^10.9.2",
"nodemon": "^3.1.4"
},
"devDependencies": {
"@types/http-proxy-middleware": "^2.0.7"
}
}This package.json defines dev/prod scripts and lists key dependencies. Note @aws-sdk/client-s3 for native S3 interactions. Pitfall: Skip types, and TypeScript won't catch runtime errors like malformed JWTs.
Logging Module
import { createWriteStream } from 'fs';
import { format } from 'util';
export interface LogEntry {
timestamp: string;
userId?: string;
action: string;
target: string;
status: 'ALLOW' | 'BLOCK' | 'ERROR';
details?: string;
}
const logStream = createWriteStream('casb-logs.jsonl', { flags: 'a' });
export function log(entry: LogEntry): void {
const line = JSON.stringify({ ...entry, timestamp: new Date().toISOString() }) + '\n';
logStream.write(line);
console.log(`[CASB] ${entry.status} - ${entry.action} to ${entry.target} by ${entry.userId || 'anonymous'}`);
}
log({ action: 'STARTUP', target: 'CASB Proxy', status: 'ALLOW' });This JSONL logger records every event with timestamp, userId, and details. Line-by-line format works with ELK Stack or Splunk. Benefit: Scalable and queryable. Pitfall: Without 'flags: a', logs overwrite on restart.
Setting Up DLP Policies
Policies check: 1) Valid JWT, 2) Scan payloads for sensitive terms (e.g., 'confidential', SSN regex). If non-compliant, block and log. Imagine an anti-spam filter, but for sensitive data.
Security Policies Module
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
const SENSITIVE_PATTERNS = [/confidentiel/i, /\b\d{3}-\d{2}-\d{4}\b/, /SSN:/i];
export interface AuthUser {
userId: string;
roles: string[];
}
export function authenticate(req: Request, res: Response, next: NextFunction): void {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
res.status(401).json({ error: 'Token requis' });
return;
}
try {
const user = jwt.verify(token, JWT_SECRET) as AuthUser;
req.user = user;
next();
} catch (err) {
res.status(401).json({ error: 'Token invalide' });
}
}
export function dlpScan(body: string): boolean {
return !SENSITIVE_PATTERNS.some(pattern => pattern.test(body));
}
export function applyPolicies(req: Request, res: Response, next: NextFunction): void {
if (!dlpScan(JSON.stringify(req.body) + req.url)) {
res.status(403).json({ error: 'Données sensibles détectées' });
return;
}
next();
}The authenticate middleware parses JWT and attaches user to req. dlpScan uses regex to detect leaks (SSN, keywords). applyPolicies chains checks. Pitfall: Unescaped regex causes false positives; test with varied payloads.
AWS Configuration and .env
Add to .env:
JWT_SECRET=your-super-secret-key-2026
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
S3_BUCKET=your-test-bucket
PORT=3000
Protect this file with .gitignore.
Main CASB Server
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { log, LogEntry } from './logger';
import { authenticate, applyPolicies, AuthUser } from './policies';
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.raw({ type: '*/*', limit: '10mb' }));
const s3 = new S3Client({ region: 'us-east-1' });
const TARGET_BUCKET = process.env.S3_BUCKET!;
const PORT = (process.env.PORT || 3000) as number;
// Générer JWT pour tests
app.post('/auth/login', (req, res) => {
const token = require('jsonwebtoken').sign({ userId: 'user123', roles: ['admin'] }, process.env.JWT_SECRET!);
res.json({ token });
});
// Proxy S3 PUT (upload)
app.put('/s3/*', authenticate, applyPolicies, async (req, res) => {
const key = req.url.replace('/s3/', '');
const logEntry: LogEntry = { userId: (req.user as AuthUser).userId, action: 'UPLOAD', target: key, status: 'ALLOW' };
try {
await s3.send(new PutObjectCommand({ Bucket: TARGET_BUCKET, Key: key, Body: req.body }));
log(logEntry);
res.status(200).json({ success: true });
} catch (err) {
log({ ...logEntry, status: 'ERROR', details: (err as Error).message });
res.status(500).json({ error: 'Upload échoué' });
}
});
// Proxy S3 GET (download)
app.get('/s3/*', authenticate, (req, res) => {
const key = req.url.replace('/s3/', '');
log({ userId: (req.user as AuthUser).userId, action: 'DOWNLOAD', target: key, status: 'ALLOW' });
// Forward proxy pour GET complet
createProxyMiddleware({
target: `https://${TARGET_BUCKET}.s3.amazonaws.com`,
changeOrigin: true,
pathRewrite: { '^/s3': '' },
onProxyReq: (proxyReq) => proxyReq.setHeader('Authorization', req.headers.authorization || ''),
})(req, res);
});
app.listen(PORT, () => {
log({ action: 'LISTEN', target: `port ${PORT}`, status: 'ALLOW' });
console.log(`CASB Proxy sur http://localhost:${PORT}`);
});This Express server proxies S3 PUT/GET via AWS SDK. Auth + DLP before forwarding. /auth/login for JWT testing. Body limit at 10MB prevents DoS. Pitfall: No AWS region causes failures; always log before/after for audits.
Testing and Deployment
npm run dev- Get token:
curl -X POST http://localhost:3000/auth/login - Test safe upload:
curl -H "Authorization: Bearer" -X PUT http://localhost:3000/s3/test.txt -d "normal content" - Test DLP block:
-d "SSN: 123-45-6789"→ 403
casb-logs.jsonl. For production, build + Docker.Production Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci --only=production && npm run build
COPY --from=build /app/dist ./dist
COPY .env ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Multi-stage pour sécurité : exclut dev depsMulti-stage Dockerfile minimizes image size (<200MB). Copies .env at runtime. Runs prod without nodemon/ts-node. Pitfall: Skip npm ci for locked deps; scan image with Trivy for vulnerabilities.
Best Practices
- JWT Rotation: Implement refresh tokens and expire <15min.
- Advanced DLP: Integrate ML like Google DLP API beyond regex.
- Scalability: Use Redis for policy caching, PM2/K8s for clustering.
- Observability: Forward logs to ELK or Datadog.
- Compliance: Add GDPR consent for payload scans.
Common Pitfalls to Avoid
- No Rate Limiting: Add
express-rate-limitagainst auth brute-force. - Hardcoded Secrets: Always use .env + Vault in prod.
- Ignore CORS: For frontends, add restricted-domain CORS middleware.
- No HTTPS: Enforce via Nginx reverse proxy or Let's Encrypt.
Next Steps
- AWS SDK Docs: aws.amazon.com/sdk
- Open-Source CASB: Check Envoy Proxy for WASM policies.
- Advanced: Integrate with Okta for SSO.