Skip to content
Learni
View all tutorials
Sécurité

How to Implement a CASB with Node.js in 2026

Lire en français

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

terminal
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 .env

This 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

package.json
{
  "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

src/logger.ts
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

src/policies.ts
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

src/server.ts
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

  1. npm run dev
  2. Get token: curl -X POST http://localhost:3000/auth/login
  3. Test safe upload: curl -H "Authorization: Bearer " -X PUT http://localhost:3000/s3/test.txt -d "normal content"
  4. Test DLP block: -d "SSN: 123-45-6789" → 403
Logs in casb-logs.jsonl. For production, build + Docker.

Production Dockerfile

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 deps

Multi-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-limit against 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

Explore our Learni cloud security training to master enterprise CASB.