Automatización7 min de lectura

04. Backend Blindado y Despliegue Cloud Serverless a Costo Cero

Configuración de rate limiting aislado, solución de fugas de datos y despliegue a costo cero en Google Cloud Run.

04. Backend Blindado y Despliegue Cloud Serverless a Costo Cero

Cuando abres las puertas de una tienda en línea, no solo invitas a clientes honestos; también atraes a bots maliciosos, piratas informáticos que intentan descifrar contraseñas por fuerza bruta y scrapers de precios. En el software, la seguridad y la rentabilidad de la infraestructura suelen competir entre sí: queremos una API ultra segura, pero no queremos pagar facturas exorbitantes a fin de mes en servidores inactivos.

En esta entrega, analizaremos cómo implementamos en DevMart un sistema de seguridad estratificado mediante políticas de Rate Limiting aislado, cómo solucionamos una vulnerabilidad de fuga de datos crítica en Prisma y cómo configuramos un despliegue serverless en Google Cloud Run a costo cero.


🛡️ Control de Abuso: Rate Limiting Aislado

El Rate Limiting (límite de peticiones) es el primer escudo contra ataques de denegación de servicio (DDoS) y abusos de la API. En lugar de aplicar un límite genérico a toda la aplicación web, diseñamos tres niveles de protección en rateLimiters.js:

graph TD
    Client[Peticiones IP Cliente] --> API{Ruta de Destino}
    API -->|/webhook/telegram| LimiterA[telegramWebhookLimiter<br>Max: 60 req/min por IP<br>Retorna siempre HTTP 200]
    API -->|/auth/*| LimiterB[authLimiter<br>Max: 10 req/15 min<br>Skip exitosos]
    API -->|/products, /cart| LimiterC[generalLimiter<br>Max: 100 req/min por IP]

Nivel 1: Ruta de Autenticación (authLimiter)

La autenticación es el objetivo preferido de los ataques de fuerza bruta. Limitamos los intentos de inicio de sesión o registro a un máximo de 10 intentos por IP cada 15 minutos. 💡 Ajuste de negocio: Usamos la propiedad skipSuccessfulRequests: true. Si el usuario ingresa su contraseña correctamente, esa petición no consume su cuota de límite. Esto garantiza que un usuario legítimo nunca sea bloqueado por accidente si se equivoca de contraseña un par de veces.

Nivel 2: Webhook de Telegram (telegramWebhookLimiter)

El bot conversacional requiere un tratamiento especial. Si un atacante inunda nuestro bot con miles de mensajes, no queremos que esto agote el ancho de banda del catálogo web principal. Por ello, aislamos el webhook a un límite de 60 peticiones por minuto. ⚠️ Detalle crítico de resiliencia: Si la IP del webhook es limitada, el backend no responde con un error HTTP 429 (Too Many Requests), sino con un HTTP 200 OK silencioso. De lo contrario, Telegram interpretaría el 429 como un fallo de red y reintentaría indefinidamente, saturando el servidor.

Nivel 3: API General (generalLimiter)

Protege los endpoints de carrito y catálogo de scrapers de precios competitivos. Permite hasta 100 peticiones por minuto por IP, un número holgado para cualquier usuario humano que navega rápido por la tienda, pero infranqueable para bots automatizados.


🔍 Mini Caso de Estudio: El Peligro de "undefined" en Prisma ORM

Durante la auditoría de seguridad del código del backend de DevMart, identificamos y solucionamos un error conceptual común en Prisma ORM que causaba una fuga masiva de datos en las listas de favoritos de los usuarios.

El Problema original

El controlador de productos favoritos original tenía esta estructura:

export const getFavoriteProduct = async (request, response) => {
    try {
        const customerId = request.customer?.id; 
        const favorites = await ClientPrismaDB.favorite.findMany({
            where: {
                customerId: customerId,
            }
        });
        return response.status(200).json(favorites);
    } // ...
}

Si por algún motivo la sesión de autenticación fallaba, o se accedía a este endpoint sin el token JWT, request.customer?.id se evaluaba como undefined.

En la mayoría de las bases de datos relacionales tradicionales, buscar por una variable nula o no definida retornará un error o cero registros. Sin embargo, Prisma trata las variables con valor undefined como inexistentes en el objeto where.

Por lo tanto, la consulta en la base de datos se convertía en:

// Prisma interpreta que no hay filtro y devuelve TODOS los registros de la tabla
const favorites = await ClientPrismaDB.favorite.findMany({ where: {} });

Esto provocaba que cualquier usuario anónimo pudiera ver los productos favoritos de todos los clientes de la tienda, violando políticas básicas de privacidad de datos.

La Solución

Implementamos una cláusula de salvaguarda (Guard Clause) explícita antes de tocar la base de datos:

// src/controllers/favorites.controller.js
export const getFavoriteProduct = async (request, response) => {
    try {
        const customerId = request.customer?.id;
        if (!customerId) {
            // Detenemos la consulta de inmediato y respondemos con error de credenciales
            return response.status(401).json({ error: 'Unauthorized' });
        }

        const favorites = await ClientPrismaDB.favorite.findMany({
            where: { customerId: customerId }
        });
        return response.status(200).json(favorites);
    } // ...
}

🐳 Contenerización en Docker de Producción

Para garantizar que el software se ejecute de la misma manera en local que en el servidor de producción de la nube, creamos una configuración de construcción Docker optimizada en múltiples etapas (Multi-stage build):

# ---- Etapa de build ----
FROM node:22-slim AS builder
RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
COPY prisma ./prisma
RUN pnpm install --frozen-lockfile
COPY src ./src
RUN pnpm exec prisma generate

# ---- Etapa de producción ----
FROM node:22-slim AS runner
RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/package.json ./
COPY --from=builder /app/src ./src
EXPOSE 8080
CMD ["node", "src/server.js"]

¿Por qué este diseño?

  • Imágenes ligeras (node:22-slim): En lugar de usar la imagen completa de Node que pesa más de 1 GB, usamos la versión slim reducida, lo que reduce el tamaño del contenedor a unos 250 MB. Esto acelera el despliegue de red y reduce costos de almacenamiento.
  • Multi-stage: Las herramientas de desarrollo necesarias para instalar dependencias y compilar el esquema de Prisma se quedan en la etapa de builder. La imagen final (runner) solo contiene los módulos mínimos de ejecución, mejorando la seguridad (menos superficie de ataque) y la velocidad de inicio del contenedor.

☁️ Google Cloud Run: Escalado a cero y control de costos

Google Cloud Run es una plataforma serverless que ejecuta contenedores de Docker de forma automática. Para un e-commerce en etapa de lanzamiento, la mayor preocupación de costes es pagar por un servidor activo las 24 horas del día cuando solo entran visitas a ciertas horas.

En la configuración de Cloud Run de DevMart (.service.yaml), establecimos las siguientes políticas de auto-escalado:

spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0" # 🔍 ESCALADO A CERO
        autoscaling.knative.dev/maxScale: "3" # 🔍 LÍMITE DE PROTECCIÓN
    spec:
      containers:
        - image: IMAGE_URL_AQUI
          resources:
            limits:
              cpu: "1"
              memory: "512Mi"

Impacto comercial:

  • minScale: "0": Cuando no hay tráfico en la tienda, Cloud Run apaga todos los contenedores activos. Google solo nos cobra por los milisegundos exactos en que nuestro código procesa una solicitud. Durante la noche o periodos sin visitas, nuestro costo de hosting backend es exactamente $0 USD.
  • maxScale: "3": Si una campaña publicitaria o un post viral en redes sociales atrae tráfico masivo súbitamente, Cloud Run encenderá de forma instantánea nuevas instancias de la aplicación (hasta un máximo de 3) para absorber la demanda. Así, la web no se cae ante picos y el costo sigue bajo un estricto control de presupuesto de protección.

En el próximo y último post de esta bitácora, saltaremos al frontend para ver cómo diseñamos la experiencia de usuario con Tailwind CSS v4 y cómo sincronizamos automáticamente el carrito de invitados en tiempo real.


📅 Publicado en la Bitácora de Desarrollo de DevMart. Ver Índice General

Tags

#Seguridad#DevOps#Cloud Run#Serverless#Backend