Segurança e Deploy em Produção
Aula 7 de 7
Segurança
Helmet (Headers HTTP)
const helmet = require('helmet');
const express = require('express');
const app = express();
// Configuração completa
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
frameguard: { action: 'deny' },
hsts: { maxAge: 31536000, includeSubDomains: true }
}));
Rate Limiting
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Limitar por IP
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // máximo de 100 requisições
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Muitas requisições. Tente novamente mais tarde.' }
});
// Aplica globalmente
app.use(limiter);
// Limiter específico para login (mais restrito)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Muitas tentativas de login. Bloqueado por 15 minutos.' }
});
app.use('/auth/login', loginLimiter);
Proteção DOS e Slowloris
const rateLimit = require('express-rate-limit');
const slowloris = require('express-slowloris');
// Desacelerar cliente abusivo em vez de bloquear
const slowDown = rateLimit({
windowMs: 60 * 1000,
max: 30,
delayMs: (hits) => hits * 100, // delay incremental
});
app.use(slowDown);
Validação de Input
npm install zod joi
const { z } = require('zod');
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(['USER', 'ADMIN']).default('USER')
});
function validate(schema) {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (err) {
return res.status(400).json({
error: 'Dados inválidos',
details: err.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
});
}
};
}
app.post('/users', validate(userSchema), async (req, res) => {
const user = await prisma.user.create({ data: req.body });
res.status(201).json(user);
});
Prevenção SQL Injection
// Prisma já protege contra SQL injection
await prisma.user.findUnique({ where: { id: userId } }); // seguro
// Raw queries com Prisma (seguro se parametrizado)
await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;
// Nunca faça:
// await db.query(`SELECT * FROM users WHERE id = ${userId}`); // INJECTION!
Variáveis de Ambiente
npm install dotenv
// .env (NUNCA commitar)
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=minha-chave-secreta-super-segura
REDIS_HOST=localhost
NODE_ENV=production
LOG_LEVEL=info
// src/config.js
require('dotenv').config();
const config = {
port: parseInt(process.env.PORT, 10) || 3000,
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379
},
nodeEnv: process.env.NODE_ENV || 'development',
isProduction: process.env.NODE_ENV === 'production'
};
// Validar config essencial
const required = ['DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Variável de ambiente ${key} é obrigatória`);
}
}
module.exports = config;
Deploy
Docker
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
env_file: .env
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:
Nginx reverse proxy
# nginx.conf
upstream node_app {
server app:3000;
}
server {
listen 80;
server_name api.meudominio.com;
gzip on;
gzip_types application/json text/css application/javascript;
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
location /health {
access_log off;
return 200 "healthy\n";
}
}
CI/CD (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npm run lint
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t app .
- name: Deploy to server
run: |
ssh user@server "docker pull app && docker-compose up -d"
PM2 Ecosystem
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'api',
script: 'src/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '1G',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
log_type: 'json',
max_restarts: 10,
restart_delay: 4000,
autorestart: true,
watch: false,
kill_timeout: 5000,
listen_timeout: 3000
}]
};
Health Checks
// src/health.js
const express = require('express');
const router = express.Router();
const prisma = require('./prisma');
const redis = require('./redis');
router.get('/health', async (req, res) => {
const checks = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
// Check database
try {
await prisma.$queryRaw`SELECT 1`;
checks.database = 'connected';
} catch {
checks.database = 'error';
checks.status = 'degraded';
}
// Check Redis
try {
await redis.ping();
checks.redis = 'connected';
} catch {
checks.redis = 'error';
checks.status = 'degraded';
}
const httpStatus = checks.status === 'ok' ? 200 : 503;
res.status(httpStatus).json(checks);
});
router.get('/ready', (req, res) => {
// Ready check para Kubernetes/Docker
res.status(200).json({ ready: true });
});
module.exports = router;
Lab: Exercício - Deploy Completo
# Estrutura final do projeto
meu-app/
├── src/
│ ├── server.js
│ ├── app.js
│ ├── config.js
│ ├── health.js
│ ├── routes/
│ └── middleware/
├── prisma/
│ └── schema.prisma
├── .env
├── .env.example
├── .github/
│ └── workflows/deploy.yml
├── Dockerfile
├── docker-compose.yml
├── ecosystem.config.cjs
├── nginx.conf
└── package.json
# Deploy com Docker
docker compose build
docker compose up -d
# Deploy com PM2
pm2 start ecosystem.config.cjs --env production
pm2 save
pm2 startup # inicia com o sistema
# Verificar health
curl http://localhost:3000/health
# Logs
docker compose logs -f app
pm2 logs api
Segurança em camadas: helmet + rate-limit + validação de input + prepared statements. Sempre use
.envpara secrets e nunca commite. Docker + PM2 = deploy robusto com auto-healing. Health checks são essenciais para orquestração.