Muchas veces se tienen aplicaciones web a las que se necesita restringir el acceso solo a ciertas direcciones IP. Esto normalmente se puede realizar con el servidor web:
- Apache con mod_authz_host, como se explica aquí.
- nginx con ngx_http_access_module.
- lighttpd con mod_access.
Sin embargo, cada vez es más popular utilizar Cloudflare como un proxy inverso. Un proxy inverso es un servicio que recibe solicitudes de los clientes y las reenvía a uno o varios servidores de origen ubicados detrás de él. Los clientes no se comunican directamente con esos servidores, sino con el proxy inverso, que actúa como intermediario entre ambos extremos, como se ve en la siguiente figura:
En nuestro escenario, el servidor de origen debe aceptar conexiones únicamente desde los rangos de IP de Cloudflare (que pueden cambiar con el tiempo) y mantener la restricción usando la IP real del cliente.
Tabla de contenido
Tabla de contenido
Cloudflare
En la consola de Cloudflare
La primera parte de la solución se trabajará en Cloudflare.
En el panel de Cloudflare, ve a Security → WAF → Custom rules y crea una regla:
- Escribe un nombre fácil de identificar:
Nombre: Solo oficina
- Crea una “expresión” o regla que le indique al WAF de Cloudflare qué hacer. Esta puede ser la parte más complicada.
Expresión (Edit expression):
Una regla simple podría ser
(ip.src ne a.b.c.d)
que indica que “la IP fuente no sea a.b.c.d”. Esto bloquea todo el tráfico
que no provenga de tu IP de oficina antes de que llegue a tu servidor. Si más
adelante necesitas agregar otra IP, la expresión sería algo como:
(ip.src ne a.b.c.d and ip.src ne e.f.g.h).
Si se quiere aplicar una regla a hosts específicos, se puede hacer algo como:
(ip.src ne a.b.c.d and http.host in {"app.example.com" "admin.example.com"})
Así, www.example.com u otros subdominios que apunten a servidores sin
restricción quedan libres.
- Selecciona la acción deseada, en este caso Block:
Acción: Block
Esto hace que Cloudflare bloquee las peticiones provenientes de direcciones IP que no cumplan con la regla.
En el servidor web
Como mencioné arriba, las direcciones IP de los proxies de Cloudflare pueden cambiar periódicamente. El siguiente script actualiza un archivo con tales direcciones:
#!/usr/bin/env bashset -euo pipefail
RAW="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"TMP="$(mktemp)"
cleanup() { rm -f "$TMP"; }trap cleanup EXIT
mkdir -p "$(dirname "$RAW")"
{ echo "# Cloudflare trusted proxies" echo "# Generated on $(date -u '+%Y-%m-%d %H:%M:%S UTC')" echo curl -fsSL https://www.cloudflare.com/ips-v4 echo curl -fsSL https://www.cloudflare.com/ips-v6} > "$TMP"
LINES=$(grep -cE '^[0-9a-f:.\/]+$' "$TMP" || true)if [ "$LINES" -lt 5 ]; then echo "ERROR: solo $LINES rangos obtenidos. Abortando." >&2 exit 1fi
if cmp -s "$TMP" "$RAW" 2>/dev/null; then echo "Sin cambios." exit 0fi
mv "$TMP" "$RAW"chmod 644 "$RAW"echo "Lista actualizada ($LINES rangos)."Este script puede ejecutarse con cron o systemd. Como no soy fan de systemd, aquí está el procedimiento para cron:
sudo chmod +x /usr/local/sbin/update-cloudflare-proxies.shsudo crontab -e# Actualizar proxies de Cloudflare — lunes 4:00 AM0 4 * * 1 /usr/local/sbin/update-cloudflare-proxies.sh >> /var/log/cloudflare-proxies.log 2>&1Primera ejecución del script:
sudo /usr/local/sbin/update-cloudflare-proxies.shAhora, hay que indicarle al servidor web en el origen lo siguiente:
- La lista de direcciones IP de los proxies de Cloudflare.
- La dirección IP permitida.
- La restricción de acceso para todas las direcciones IP menos la permitida.
Apache
Se requiere el uso del módulo remoteip para obtener la dirección IP remota
real.
sudo a2enmod remoteipsudo systemctl restart apache2Apache lee la lista directamente, sin conversión:
<IfModule mod_remoteip.c> RemoteIPHeader CF-Connecting-IP RemoteIPTrustedProxyList /var/lib/cloudflare/cloudflare-trusted-proxies.lst</IfModule>El uso de RemoteIPHeader CF-Connecting-IP es vital, ya que le indica a
Apache que la dirección real del cliente proviene de Cloudflare en ese header.
Habilita esa configuración, ya sea creando el enlace simbólico en conf-enabled,
o con:
sudo a2enmod remoteipsudo systemctl restart apache2Luego, hay que aplicar la restricción para la IP fuente:
<VirtualHost *:443> ServerName www.example.com
# ... tu configuración SSL, DocumentRoot, etc. ...
# Solo permitir tu IP de oficina <Location "/"> Require ip a.b.c.d 2001:db8::1 </Location></VirtualHost>sudo apache2ctl configtestsudo systemctl reload apache2nginx
nginx no tiene equivalente a TrustedProxyList, así que necesitas generar un snippet a partir de la lista:
#!/usr/bin/env bashset -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"OUT="/etc/nginx/snippets/cloudflare-trusted-proxies.conf"
{ echo "# Generado desde $SRC — no editar" grep -E '^[0-9a-f:.\/]+$' "$SRC" | while IFS= read -r cidr; do echo "set_real_ip_from $cidr;" done echo "real_ip_header CF-Connecting-IP;"} > "$OUT"
nginx -t 2>/dev/null && systemctl reload nginxserver { listen 443 ssl; server_name app.example.com;
include snippets/cloudflare-trusted-proxies.conf;
allow a.b.c.d; allow 2001:db8::1; deny all;
# ...}lighttpd
Misma situación, requiere snippet generado:
#!/usr/bin/env bashset -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"OUT="/etc/lighttpd/conf-available/90-cloudflare-trusted-proxies.conf"
{ echo "# Generado desde $SRC — no editar" echo 'server.modules += ("mod_extforward")' echo 'extforward.headers = ("CF-Connecting-IP")' printf 'extforward.forwarder = (' FIRST=1 grep -E '^[0-9a-f:.\/]+$' "$SRC" | while IFS= read -r cidr; do [ "$FIRST" -eq 1 ] && FIRST=0 || printf ',' printf '\n "%s" => "trust"' "$cidr" done echo echo ')'} > "$OUT"
lighttpd -t -f /etc/lighttpd/lighttpd.conf 2>/dev/null && systemctl reload lighttpdinclude "conf-available/90-cloudflare-trusted-proxies.conf"
# Restricción por IP en el vhost$HTTP["host"] == "app.example.com" { $HTTP["remoteip"] !~ "^(a\.b\.c\.d|2001:db8::1)$" { url.access-deny = ("") }}En el firewall
Como defensa adicional, configura ufw para que el puerto 443 (y 80) solo acepten tráfico desde los rangos de Cloudflare, rechazando conexiones directas de cualquier otra IP:
#!/usr/bin/env bashset -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"
if [ ! -f "$SRC" ]; then echo "ERROR: $SRC no existe. Ejecuta update-cloudflare-proxies.sh primero." >&2 exit 1fi
CIDRS=$(grep -E '^[0-9a-f:.\/]+$' "$SRC")
if [ -z "$CIDRS" ]; then echo "ERROR: no se encontraron CIDRs en $SRC." >&2 exit 1fi
# Eliminar reglas previas de Cloudflare en puertos 80,443ufw status numbered | grep -E '80,443' | grep -oP '^\[\s*\K[0-9]+' | sort -rn | while read -r num; do yes | ufw delete "$num"done
# Agregar reglas actualizadaswhile IFS= read -r cidr; do ufw allow from "$cidr" to any port 80,443 proto tcpdone <<< "$CIDRS"
ufw reloadecho "ufw actualizado con $(echo "$CIDRS" | wc -l) rangos."Esto evita que alguien que descubra la IP de tu servidor pueda conectarse directamente, saltándose Cloudflare.
Verificación
Desde tu IP de oficina, accede a https://www.example.com — debería funcionar normalmente. Desde otra red (por ejemplo tu celular con datos móviles), intenta acceder: deberías recibir un bloqueo de Cloudflare (error 1020).
Revisa los logs de Apache para confirmar que las IP reales se registran correctamente. En el caso de Apache:
tail -f /var/log/apache2/access.logDeberías ver a.b.c.d y no una IP de Cloudflare.
Resumen del modelo de defensa
| Capa | Qué hace | Qué bloquea |
|---|---|---|
| Cloudflare WAF | Filtra por IP del visitante | Tráfico de IPs no autorizadas antes de llegar al servidor |
| Apache + mod_remoteip | Filtra por IP real restaurada | Tráfico no autorizado que logre pasar por Cloudflare |
| ufw | Filtra por IP de origen a nivel de red | Conexiones directas al servidor que no pasen por Cloudflare |
Las tres capas juntas te dan defensa en profundidad: si una falla, las otras sostienen la restricción.
Bonus
¿Cómo obtengo mi dirección IP de “salida”?
Se puede usar un sitio como whatismyip para obtener la dirección IP pública de nuestra salida.
Scripts
Puedes encontrar los scripts en mi GitHub, bajo licencia GPL 3.0.
