StackPractices
intermediate Por Mathias Paulenko

Plantilla de Documentación de Seguridad de Red

Una plantilla para documentar el inventario de reglas de seguridad de VPC, firewall y DNS.

Temas: devops

Visión General

La red es lo primero que prueba un atacante y casi siempre lo último que alguien documenta. Reglas de firewall mal configuradas, security groups demasiado permisivos y registros DNS obsoletos son puntos de entrada habituales para atacantes, y casi imposibles de detectar sin un inventario. Esta plantilla te da ese inventario: cada regla con su justificación, su propietario y su fecha de revisión, para que puedas auditar la red en lugar de suponer.

La estructura funciona para una revisión de seguridad trimestral, para incorporar un servicio nuevo que necesita acceso a la red, o para demostrar cumplimiento cuando un auditor pregunta «¿quién aprobó esta regla y por qué existe?».

Cuándo Usar

Usa este recurso cuando:

  • Incorpores un servicio nuevo que requiere reglas de acceso a la red
  • Realices una auditoría de seguridad trimestral o prepares una prueba de penetración
  • Migres infraestructura a una VPC nueva o a otro proveedor de nube
  • Heredes un entorno donde nadie sabe por qué existe una regla

Sáltala en un sandbox de desarrollo de un solo propósito o en una prueba de concepto que se derriba en días: ahí el costo de la revisión supera al riesgo.

Solución

# Inventario de Seguridad de Red: `<Entorno>`

## 1. Alcance

| Campo | Valor |
|-------|-------|
| Entorno | `prod / staging / dev` |
| Proveedor de Nube | `AWS / GCP / Azure / On-prem` |
| CIDR de Red | `10.0.0.0/16` |
| Última Revisión | `YYYY-MM-DD` |
| Revisor | `@security-team` |

## 2. Segmentación de VPC / Red

| Segmento | CIDR | Propósito | Acceso a Internet | Peering |
|----------|------|-----------|-------------------|---------|
| Subnet pública | `10.0.1.0/24` | Load balancers, bastion | Sí (solo egress) | Ninguno |
| Subnet de aplicación | `10.0.2.0/24` | Microservicios | No | Peering a servicios compartidos |
| Subnet de base de datos | `10.0.3.0/24` | PostgreSQL, Redis | No | Peering a servicios compartidos |
| Subnet de administración | `10.0.4.0/24` | Runners de CI/CD, monitoreo | Sí (NAT) | Ninguno |
| Servicios compartidos | `10.1.0.0/24` | SSO, logging, secretos | No | Peering a todas las VPCs |

## 3. Reglas de Firewall / Security Groups

### 3.1. Reglas de Entrada

| Origen | Protocolo | Puerto | Destino | Justificación | Última Revisión |
|--------|-----------|--------|---------|---------------|---------------|
| `0.0.0.0/0` | TCP | 443 | ALB público | HTTPS público | `YYYY-MM-DD` |
| `10.0.2.0/24` | TCP | 5432 | Subnet de base de datos | Aplicación → DB | `YYYY-MM-DD` |
| `10.0.4.0/24` | TCP | 22 | Bastion host | Acceso admin | `YYYY-MM-DD` |
| `CIDR VPN` | TCP | 3389 | Windows jump host | Acceso admin | `YYYY-MM-DD` |

### 3.2. Reglas de Salida

| Destino | Protocolo | Puerto | Origen | Justificación | Última Revisión |
|---------|-----------|--------|--------|---------------|---------------|
| `0.0.0.0/0` | TCP | 443 | Todas las subnets | Actualizaciones de software, APIs | `YYYY-MM-DD` |
| `0.0.0.0/0` | TCP | 53 | Todas las subnets | Resolución DNS | `YYYY-MM-DD` |
| `0.0.0.0/0` | UDP | 53 | Todas las subnets | Resolución DNS | `YYYY-MM-DD` |
| `Rango IP SaaS` | TCP | 443 | Subnet de aplicación | API de terceros | `YYYY-MM-DD` |

## 4. Configuración de DNS

| Registro | Tipo | Valor | TTL | Propósito | Seguro |
|----------|------|-------|-----|-----------|--------|
| `api.example.com` | A | IP ALB | 60s | API pública | DNSSEC habilitado |
| `internal.example.com` | A | IP privada | 300s | Servicios internos | Solo zona privada |
| `cdn.example.com` | CNAME | `cdn.provider.com` | 300s | Entrega de assets | HTTPS forzado |

## 5. Controles de Acceso a la Red

| Control | Implementación | Alcance | Estado |
|---------|----------------|---------|--------|
| Protección DDoS | AWS Shield / Cloud Armor / Azure DDoS | Endpoints públicos | Activo |
| WAF | AWS WAF / Cloudflare / ModSecurity | Endpoints públicos | Activo |
| Detección de intrusiones | Suricata / GuardDuty / Azure Sentinel | Flow logs de VPC | Activo |
| VPN / Zero Trust | WireGuard / Zscaler / BeyondCorp | Todo acceso admin | Activo |
| Endpoints privados | VPC endpoints / PrivateLink / Private Link | Servicios AWS / Azure | Activo |

## 6. Registro de Cambios

| Fecha | Cambio | Solicitante | Aprobado Por | Ticket |
|-------|--------|-------------|--------------|--------|
| `YYYY-MM-DD` | Apertura del puerto 5432 para un nuevo servicio de analytics | `data-team` | `security-team` | `SEC-123` |
| `YYYY-MM-DD` | Eliminación de SSH de la subnet pública | `sre-team` | `security-team` | `SEC-124` |

Cómo Rellenar Cada Sección

Cada tabla de la plantilla se corresponde con un comando o una vista de consola que ya tienes. No la rellenes de memoria: hazlo desde la fuente de verdad.

SecciónQué capturarDe dónde sacarlo
AlcanceEntorno, proveedor, CIDR, revisorPágina de detalles de la VPC o aws ec2 describe-vpcs
SegmentaciónUna fila por subnet/VPC con su propósitoaws ec2 describe-subnets, gcloud compute networks subnets list, portal de Azure → VNets
Reglas de entradaOrigen, puerto, destino, justificaciónaws ec2 describe-security-groups, gcloud compute firewall-rules list, az network nsg rule list
Reglas de salidaLos mismos campos, para egressLos mismos comandos: las reglas de egress vienen en la misma salida
DNSRegistro, tipo, valor, TTL, seguroExportación de Route 53 / Cloud DNS / registrador, o dig/nslookup para comprobaciones puntuales
Controles de accesoEstado de DDoS, WAF, IDS, VPN y endpoints privadosLa consola de cada servicio; marca explícitamente lo que falte en lugar de omitir la fila
Registro de cambiosFecha, cambio, solicitante, aprobador, ticketTu sistema de tickets; si un cambio no tiene ticket, eso ya es un hallazgo

La única columna que la gente se salta es Justificación. También es la única que hace que el inventario valga algo seis meses después. «Abierto para depurar el despliegue en marzo» es una justificación; «hacía falta» no lo es.

Ejemplo Rellenado

Así queda la sección 3.1 completada para un entorno de producción de un SaaS mediano:

OrigenProtocoloPuertoDestinoJustificaciónÚltima Revisión
0.0.0.0/0TCP443alb-orders-prodEntrada pública HTTPS2026-09-01
10.0.2.0/24TCP5432rds-orders-primaryOrders API → Postgres2026-09-01
10.0.4.0/24TCP22bastion-prod-01SSH admin vía bastion2026-06-15
203.0.113.0/24TCP443alb-orders-prodAllowlist API del partner (Contrato P-118)2026-08-20

Hay dos cosas que copiar del ejemplo: cada fila nombra un recurso concreto (no «la base de datos»), y la regla del partner lleva su referencia de contrato, de modo que renovarla o eliminarla es un ticket en lugar de un proyecto de arqueología.

Explicación

La plantilla trata tu red como un sistema de defensa en profundidad. La segmentación implica que si una subnet queda comprometida, el atacante no puede moverse lateralmente hasta la base de datos. Cada regla exige una justificación y una fecha de revisión para que las reglas obsoletas no persistan para siempre: una regla que nadie sabe explicar es una regla que debería cerrarse. El DNS queda olvidado en la mayoría de las revisiones, pero un registro secuestrado u obsoleto redirige el tráfico a infraestructura del atacante con la misma eficacia que un puerto abierto.

flowchart diagram: Internet

Fíjate en la asimetría: la subnet pública es la única con entrada desde internet, y solo el load balancer termina ahí. Si tu inventario muestra una regla de aplicación o base de datos alcanzable desde 0.0.0.0/0 en entrada, esa es la primera fila a revisar.

Cuando documentes las decisiones de segmentación, acompaña este inventario con la plantilla de política de segmentación de red: el inventario registra lo que existe y la política registra lo que está permitido que exista. Se desincronizan más rápido de lo que parece.

Lo Que Suele Encontrar una Primera Revisión

Espera que la primera pasada sobre un entorno real saque a la luz los mismos problemas de siempre:

  • Puertos de administración abiertos al mundo: SSH o RDP alcanzables desde 0.0.0.0/0, casi siempre restos de un atajo de aprovisionamiento. Muévelos detrás del bastion o la VPN.
  • Reglas huérfanas: puertos abiertos para un servicio que ya no existe, normalmente de un proyecto que se lanzó o murió sin ticket de limpieza.
  • Una sola red plana: una única subnet que lleva tráfico web, de aplicación y de base de datos, lo que hace que cada revisión de reglas empiece desde cero.
  • Egress que nadie miró: las reglas de entrada reciben escrutinio mientras las de salida quedan abiertas de par en par — está bien para DNS y actualizaciones, pero cualquier otra cosa necesita justificación.
  • DNS sin propietario: registros apuntando a IPs retiradas, o nadie que sepa quién creó un registro A crítico.

Cada hallazgo se convierte en una fila del registro de cambios y en un ticket con propietario. El trabajo del inventario es hacerlos visibles, no arreglarlos sobre la marcha.

Variantes

EntornoEnfoqueNotas
AWSVPC + Security Groups + NACLsNACLs para reglas a nivel de subnet, SGs a nivel de instancia
GCPVPC Firewall Rules + Cloud ArmorLas reglas de firewall son con estado; usa service accounts para identidad
AzureNSGs + Azure FirewallLos NSGs aplican a subnets o NICs; Azure Firewall para logging central
KubernetesNetwork Policies + Cilium / CalicoAísla namespaces; default-deny es lo más seguro
Híbrido / On-premFirewall físico + SD-WANDocumenta las rutas lógicas y las físicas

La forma del inventario se mantiene entre proveedores; solo cambian los nombres de los campos. En Kubernetes, una «subnet» es un namespace y un «security group» es una NetworkPolicy; si operas contenedores a escala, la plantilla de baseline de seguridad de contenedores cubre los controles complementarios en la imagen y el runtime.

Lo que funciona

  1. Deniega todo por defecto y permite solo lo que tenga una justificación documentada.
  2. Revisa las reglas de firewall cada trimestre y elimina lo que haya perdido su justificación.
  3. Permite 0.0.0.0/0 en entrada solo hacia un load balancer que termine TLS; nunca hacia instancias.
  4. Activa los flow logs de VPC y conserva al menos 90 días; son el único registro que tendrás del movimiento lateral.
  5. Alcanza los servicios de nube a través de endpoints privados en lugar de la internet pública.

Errores Comunes

  1. Permitir SSH o RDP desde 0.0.0.0/0 en lugar de canalizar el acceso admin por un bastion o una VPN.
  2. Reutilizar el mismo security group en producción y staging, lo que difumina todos los límites que dibujaste.
  3. Dejar olvidadas reglas temporales tras un incidente o una sesión de depuración; nadie recuerda haberlas abierto.
  4. Recoger flow logs de VPC sin mirarlos nunca, así el movimiento lateral pasa desapercibido durante una brecha.
  5. Fijar direcciones IP en DNS en lugar de usar CNAMEs hacia los load balancers.

Cadencia de Revisión y Propiedad

Un inventario que nadie mantiene es peor que no tenerlo: te da respuestas seguras que son incorrectas.

  • Revisión trimestral: repasa cada regla cuya Última Revisión tenga más de 90 días. Cada una recibe uno de tres resultados: conservar (renovar justificación), eliminar (abrir el ticket) o marcar para aceptación de riesgo.
  • Un propietario por fila: el equipo que pidió la regla es dueño de su justificación. Cuando no pueden explicarla, la acción por defecto es eliminarla, no renovarla.
  • La aceptación de riesgo es una decisión documentada: si una regla no puede eliminarse pero tampoco es ideal (un proveedor exige una allowlist de IP fija, por ejemplo), registra al dueño del riesgo y una fecha de caducidad para la excepción; no dejes que «temporal» se vuelva permanente por silencio.
  • Disciplina de registro de cambios: cada entrada vincula un cambio de regla a un ticket. Si el registro y la configuración real no coinciden, confía en la configuración real y arregla el proceso que permitió la deriva.
  • Complementa con checklists adyacentes: la checklist de seguridad de endpoints cubre el lado del host de la misma revisión, y la plantilla de seguridad del pipeline de CI/CD cubre la ruta de despliegue que sigue tocando estas reglas.

Soluciones Avanzadas

Auditoría automatizada de security groups con AWS CLI

Exporta y analiza todas las reglas de security groups para encontrar configuraciones demasiado permisivas:

import boto3
import csv
from dataclasses import dataclass
from typing import List

@dataclass
class SecurityGroupFinding:
    group_id: str
    group_name: str
    direction: str  # ingress o egress
    protocol: str
    from_port: int
    to_port: int
    cidr: str
    severity: str  # critical / warning / ok
    vpc_id: str

class SecurityGroupAuditor:
    def __init__(self, region: str = "us-east-1"):
        self.ec2 = boto3.client("ec2", region_name=region)

    def audit_all_security_groups(self) -> List[SecurityGroupFinding]:
        """Audita todos los security groups buscando reglas demasiado permisivas."""
        findings = []
        response = self.ec2.describe_security_groups()

        for sg in response["SecurityGroups"]:
            for rule_type in ["IpPermissions", "IpPermissionsEgress"]:
                direction = "ingress" if rule_type == "IpPermissions" else "egress"
                for rule in sg.get(rule_type, []):
                    for ip_range in rule.get("IpRanges", []):
                        cidr = ip_range.get("CidrIp", "N/A")
                        from_port = rule.get("FromPort", 0)
                        # 0.0.0.0/0 en puertos no web es crítico; en puertos web es aviso
                        if cidr != "0.0.0.0/0":
                            severity = "ok"
                        elif direction == "ingress" and from_port not in (80, 443):
                            severity = "critical"
                        else:
                            severity = "warning"

                        findings.append(SecurityGroupFinding(
                            group_id=sg["GroupId"],
                            group_name=sg["GroupName"],
                            direction=direction,
                            protocol=rule.get("IpProtocol", "all"),
                            from_port=from_port,
                            to_port=rule.get("ToPort", 0),
                            cidr=cidr,
                            severity=severity,
                            vpc_id=sg.get("VpcId", "default"),
                        ))
        return findings

    def export_findings(self, output_file: str) -> None:
        """Exporta los hallazgos a CSV para el registro de auditoría."""
        findings = self.audit_all_security_groups()
        with open(output_file, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow([
                "Group ID", "Group Name", "Direction", "Protocol",
                "From Port", "To Port", "CIDR", "Severity", "VPC ID"
            ])
            for finding in findings:
                writer.writerow([
                    finding.group_id, finding.group_name, finding.direction,
                    finding.protocol, finding.from_port, finding.to_port,
                    finding.cidr, finding.severity, finding.vpc_id,
                ])

        risky = [f for f in findings if f.severity == "critical"]
        print(f"Total rules: {len(findings)}")
        print(f"Critical rules: {len(risky)}")
        for r in risky:
            print(
                f"  RISK: {r.group_name} ({r.direction})"
                f" - {r.protocol}:{r.from_port} from {r.cidr}"
            )

# Ejemplo de uso
auditor = SecurityGroupAuditor(region="us-east-1")
auditor.export_findings("sg-audit-report.csv")

Análisis de flow logs de VPC para detección de movimiento lateral

Analiza flow logs de VPC para detectar patrones de tráfico sospechoso:

#!/bin/bash
set -euo pipefail

# Consulta los flow logs de VPC buscando tráfico rechazado en las últimas 24 horas
LOG_GROUP="/aws/vpc/flow-logs"
START_TIME=$(date -d '24 hours ago' +%s)000
END_TIME=$(date +%s)000

aws logs filter-log-events \
  --log-group-name "$LOG_GROUP" \
  --start-time "$START_TIME" \
  --end-time "$END_TIME" \
  --filter-pattern "REJECT" \
  --query 'events[*].message' \
  --output text | while read -r line; do
    # Campos del flow log (separados por espacio):
    # version account-id srcaddr dstaddr srcport dstport
    # protocol packets bytes start end action log-status
    SRC=$(echo "$line" | awk '{print $4}')
    DST=$(echo "$line" | awk '{print $5}')
    DSTPORT=$(echo "$line" | awk '{print $7}')
    ACTION=$(echo "$line" | awk '{print $13}')

    echo "REJECTED: $SRC -> $DST:$DSTPORT ($ACTION)"
  done

# Agrega por IP de origen para encontrar intentos de escaneo
echo ""
echo "=== Top Source IPs by Rejected Connections ==="
aws logs filter-log-events \
  --log-group-name "$LOG_GROUP" \
  --start-time "$START_TIME" \
  --end-time "$END_TIME" \
  --filter-pattern "REJECT" \
  --query 'events[*].message' \
  --output text | \
  awk '{print $4}' | \
  sort | uniq -c | sort -rn | head -20

Aplicación de network policies de Kubernetes con Cilium

Define y verifica políticas default-deny en todos los namespaces:

# default-deny-all.yaml - Aplica a cada namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
# allow-dns.yaml - Permite DNS explícitamente
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
---
# allow-frontend-to-backend.yaml - Comunicación específica entre servicios
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
#!/bin/bash
# Verifica que todos los namespaces tienen políticas default-deny
set -euo pipefail

NAMESPACES=$(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}')
for ns in $NAMESPACES; do
  POLICIES=$(kubectl get networkpolicy -n "$ns" -o json | \
    jq -r '.items[] | select(.metadata.name | test("default-deny")) | .metadata.name')

  if [ -z "$POLICIES" ]; then
    echo "WARNING: el namespace '$ns' no tiene network policy default-deny"
  else
    echo "OK: el namespace '$ns' tiene la política: $POLICIES"
  fi
done

Código companion: el repo companion de network-security-template tiene estos scripts como archivos ejecutables.

Ver También

Preguntas frecuentes

¿Cómo audito security groups existentes rápidamente?

Usa aws ec2 describe-security-groups (o equivalente) y exporta a CSV. Después ordena por las reglas que permiten entrada desde 0.0.0.0/0 y cruza cada una con tickets de cambio. Si una regla no tiene justificación o no se revisó en más de un año, márcala para eliminación y notifica al equipo propietario.

¿Debería usar Network ACLs o Security Groups?

Ambos. Los security groups funcionan con estado sobre instancias y son tu control principal. Los NACLs funcionan sin estado sobre subnets, así que úsalos como defensa de respaldo de grano grueso. Un NACL, por ejemplo, bloquea un puerto a nivel de subnet incluso si un security group lo permite por error.

¿Cómo aseguro los registros DNS?

Activa DNSSEC en las zonas públicas; frena el envenenamiento de caché. Dale a los registros que puedan necesitar cambios rápidos un TTL corto (60-300s). Vigila los cambios no autorizados con los logs de auditoría del proveedor DNS. En el DNS interno, mantén todo en zonas privadas alcanzables solo desde la VPC y restringe las transferencias a servidores autorizados.

¿Cada cuánto debería revisar las reglas de firewall?

Trimestralmente es la cadencia que se sostiene en la práctica: las revisiones mensuales acaban saltándose y las anuales dejan acumular demasiada mugre. Todo lo que toque 0.0.0.0/0 en entrada merece una mirada en cada revisión, y las reglas temporales deberían llevar fecha de caducidad desde el momento en que se abren.

¿Qué hago con una regla que nadie sabe justificar?

Primero mira en los flow logs si hubo tráfico real durante los últimos 90 días. Si está en silencio, elimina la regla y espera quejas: con el cambio registrado, revertir es trivial. Si transporta tráfico, sigue el origen hasta su propietario y pídele que escriba la justificación o que migre a una regla mejor acotada.