Poner en la lista negra las direcciones IP de su sitio web Flask que se ejecuta en Linux
A veces quieres bloquear las direcciones IP inmediatamente. Este post describe un método para hacerlo.
Tienes un sitio web y funciona bien. Pero te das cuenta de que ciertos visitantes están tratando de meterse con tus formularios. Vienen de direcciones IP específicas. También hay bots que están escaneando tu sitio. Algunos son necesarios pero otros deben mantenerse alejados. ¿No odias esto? Sí, lo odio. En el pasado escribí una vez un módulo que devolvía una respuesta no tan agradable muy lentamente, byte a byte, ralentizando sus sistemas. O que devolvía una cantidad interminable de datos. Pero esa es otra historia.
Por ahora quiero centrarme en otro método: bloquear estas peticiones. Simplemente devuelve un HTTP 403 Forbidden. Quiero ser capaz de hacer esto sobre la marcha desde la sección de administración de mi sitio web. Allí especificamos las direcciones IP o el rango de direcciones IP que queremos bloquear. También hay otras formas de hacerlo, como usar los archivos .htaccess y la configuración del servidor web. Los mencionaré al final de este artículo.
Varias razones para bloquear
Ya mencioné que una de las razones para bloquear el acceso a su sitio es bloquear a los visitantes maliciosos. Quieren ver cómo pueden romper tu sitio, llenar tu sección de comentarios con publicidad o mensajes locos. Hay muchas razones por las que creo que una de ellas es que quieren forzarte a tomar un plugin anti-spam de terceros. Estos pueden ser muy efectivos ya que se conectan a enormes bases de datos con información de spam. Pero si queremos respetar la privacidad de nuestros visitantes no podemos usar tal plugin. Debemos usar otras formas, y un último recurso a menudo es el bloqueo de direcciones IP.
También puede ser necesario bloquear ciertos bots que escanean su sitio. Algunos bots generan cantidades locas de tráfico. Revisé todo el tráfico de este sitio por un cierto período y pareció que sólo el 10%, probablemente menos, de las solicitudes eran de visitantes reales! Por supuesto que no todos los bots son malos, pero algunos realmente no respetan las reglas. La mayoría de los bots pueden ser identificados por la cadena de Agente de Usuario. Encontré los siguientes dos que realmente quiero bloquear:
- SemrushBot
- AhrefsBot
Tenga cuidado con lo que bloquea, muchos bots se utilizan para que su sitio aparezca en los resultados de los motores de búsqueda. SemrushBot es sobre SEO, no estoy usando esto en este momento. El bloqueo de agentes de usuario no está cubierto en este post. No cambiará tan a menudo y puedes establecer bloqueos de otras maneras.
Lo bueno de las peticiones no deseadas
Si se implementa un registro adecuado, también se puede aprovechar de las solicitudes no deseadas. La siguiente lista muestra algunas peticiones que causaron un error HTTP 404 para este sitio:
http://peterspython.com/css/album.css
http://www.peterspython.com/wordpress
http://peterspython.com/blog/wp-includes/wlwmanifest.xml
http://peterspython.com/wordpress/wp-includes/wlwmanifest.xml
http://peterspython.com/website/wp-includes/wlwmanifest.xml
http://peterspython.com/public/ui/v1/js/sea.js
http://www.peterspython.com/public/ui/v1/js/sea.js
http://peterspython.com/vendor/phpunit/phpunit/phpunit.xsd
http://peterspython.com/vendor/phpunit/phpunit/LICENSE
http://www.peterspython.com/apple-touch-icon.png
http://peterspython.com/humans.txt
http://peterspython.com/license.txt
Vemos que los bots están buscando el archivo wlwmanifest.xml. Este parece ser un archivo asociado con 'Windows Live Writer', una aplicación de publicación de blogs desarrollada por Microsoft que fue descontinuada en 2017 y puede ser vulnerable. Otro ataque está buscando PHPUnit, una unidad PHP probando framework. Esto contenía una vulnerabilidad que puede no haber sido parcheada todavía. Otros bots de ataque pueden generar URLs que causan un error HTTP 500. Esto puede ser intencionado pero también puede ser causado por debilidades de su sitio.
La buena noticia es que puedes usar esta información para mejorar tu sitio. Siempre asegúrese de implementar un registro adecuado, los errores le dan información muy valiosa!
Limitado a IPv4 sólo direcciones IP
Bloquear a los visitantes por la dirección IP tiene sus limitaciones. Mucha gente en Internet obtiene su dirección IP cuando se conecta a un servidor usando DHCP. Esto es mayormente cierto para los teléfonos móviles. Así que ten cuidado con lo que bloqueas.
También está IPv6 que fue diseñado para superar la limitada disponibilidad de las direcciones IPv4 . Aunque algunos informes afirman que el 30% del tráfico de Internet está en IPv6, el número de servidores que realmente han habilitado IPv6 es mucho menor. Esto afortunadamente significa que no hay razón para migrar su servidor a IPv6 en este momento. Bloquear el spam con IPv6 es posible con este método pero hay un "gotcha".
Operaciones administrativas y reglas de direcciones IP de la lista negra
En el administrador quiero especificar las direcciones IP que quiero poner en la lista negra. Hay una tabla con registros de direcciones IP en la lista negra. Para las direcciones IP quiero poder especificar las direcciones IP de la siguiente manera:
- Una sola dirección IP, ejemplo: 1.2.3.4
- Una red IP, ejemplo: 1.2.3.0/24
- Un rango de direcciones IP, por ejemplo: 1.2.3.6-1.2.4.2
Especifico uno de estos en un solo registro y lo llamo "Regla de la lista negra de direcciones IP".
Caching para evitar el acceso a la base de datos
Ciertamente no queremos acceder a la base de datos en cada solicitud para ver si la solicitud está permitida. Eso ralentizaría las solicitudes. Por eso usamos el caching. En lugar de consultar la base de datos, primero revisamos el caché para ver si la dirección IP accedió al sitio antes. Para cada dirección IP tenemos un indicador llamado "permitido". Si es Verdadero entonces el acceso está permitido, si es Falso entonces el acceso está bloqueado.
Si la dirección IP está en la caché, hemos terminado, continuamos o bloqueamos. Si la dirección IP no está en el caché, comprobamos si está en la lista negra de reglas de direcciones IP. El resultado se añade al caché, y la próxima vez que una solicitud con esta dirección IP llegue a nuestro sitio, los datos estarán en el caché y la base de datos no será consultada.
Añadir y eliminar las reglas de la lista negra de direcciones IP
Supongamos que tenemos cientos, miles de elementos en nuestro caché. Ahora queremos hacer cambios usando el administrador, ya sea agregando una Regla de Dirección IP de la Lista Negra o removiendo una Regla de Dirección IP de la Lista Negra.
Añadir o eliminar una regla no es trivial porque la regla puede incluir direcciones IP que ya están en la caché. La forma más simple es vaciar el caché y dejar que se reconstruya de nuevo. Esto ralentizará las siguientes peticiones durante un corto tiempo. La única otra forma es escanear las direcciones IP en la caché y comprobar si coinciden con la regla de la lista negra de direcciones IP añadida o eliminada. Si coinciden, las eliminamos de la caché. Tengo algunas ideas de cómo implementar esto, pero aún no lo he hecho.
Añadiendo marcas de tiempo
Para un máximo rendimiento, la información de las direcciones IP en la caché es de sólo lectura y no caduca. Esto significa que puede crecer enormemente con el tiempo si tienes muchos visitantes. Debido a que la mayoría de los visitantes acceden a su sitio por unos pocos minutos, podemos agregar una marca de tiempo a las direcciones IP en caché que se actualiza en cada acceso. La marca de tiempo hace que sea fácil eliminar las entradas antiguas.
Las solicitudes en el mismo momento
Supongamos que dos peticiones, la petición A y la petición B, llegan al mismo tiempo, ambas usando la misma dirección IP. Si no están en la caché ambas comprobarán si su dirección IP está bloqueada buscando en la tabla de Reglas de la Lista Negra de Direcciones IP. Luego ambas actualizan el ítem cached_access. La solicitud A crea primero el elemento permitido. Pero luego la solicitud B crea el ítem permitido, sobrescribiendo el ítem permitido de la solicitud A. Lo mismo ocurre si queremos actualizar la marca de tiempo del ítem en caché. Esto puede parecer malo, pero en realidad no lo es tanto. Sólo debemos asegurarnos de que la operación de creación es atómica.
Usando el sistema de archivos Linux como caché
Por el momento elijo implementar el cacheo con los archivos. El sistema de archivos Linux es lo suficientemente rápido para manejar esto para mi aplicación. No quiero añadir algo como Redis, quiero mantener las dependencias al mínimo.
Si tenemos un archivo "permitido" por dirección IP, entonces el archivo puede ser pequeño, el contenido es 0 (bloqueado) o 1 (permitido). Para evitar un gran número de archivos en un directorio y una búsqueda lenta, creamos subdirectorios basados en la dirección IP. Dividimos la dirección IP por el punto ('.') y la usamos para crear directorios. La marca de tiempo del archivo "permitido" cambia automáticamente cuando se lee el archivo. En Linux tenemos las siguientes marcas de tiempo:
- mtime (ls -l)
La última vez que el contenido del archivo fue modificado - ctime
La última vez que el estado del archivo, por ejemplo, los permisos, cambió - atime (ls -lu)
La última vez que el archivo fue leído
Para nuestro propósito podemos usar un tiempo como una marca de tiempo. No tenemos que actualizar la hora del archivo. Hay un problema si quieres poder mostrar el contenido de los archivos permitidos en el admin. Esto leería los archivos y cambiaría las marcas de tiempo. Podemos solucionarlo creando una copia del archivo "permitido". La lectura de la copia no cambia la hora del archivo original "permitido".
Una advertencia cuando se utiliza el Linux tiempo de acceso atime
Hay mucha información en Internet sobre las marcas de tiempo de Linux pero sólo muy pocos mencionan que esto puede no funcionar como se espera. Les invito a mirar los enlaces de abajo sobre esto. Por ejemplo, puedes comprobar si relatime es una opción de montaje con este comando:
cat /proc/mounts | grep relatime
El resumen es:
- La actualización a tiempo en cada lectura está desactivada por defecto por razones de rendimiento
- Desde el kernel 2.6.30, el relatime es la opción por defecto
- Desde el kernel 2.6.30, la última hora de acceso a un archivo siempre se actualiza si tiene más de 1 día de antigüedad
Esto significa que todavía podemos usar un tiempo pero debemos respetar una resolución de un día. No hay problema para mí pero espera, vamos a probar si esto realmente funciona.
ls -l
El resultado es:
total 8
-rw-r--r-- 1 flaskuser flaskgroup 1 apr 16 15:47 allowed
-rw-r--r-- 1 flaskuser flaskgroup 1 apr 16 15:47 allowed_copy
A continuación queremos ver el atime, o tiempo de acceso, del archivo permitido:
stat allowed
El resultado es:
File: allowed
Size: 1 Blocks: 8 IO Block: 4096 regular file
Device: 806h/2054d Inode: 38805116 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1002/flaskuser) Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:47:06.024559817 +0200
Modify: 2020-04-16 15:47:06.024559817 +0200
Change: 2020-04-16 15:47:06.024559817 +0200
Birth: -
Ahora cambiamos la hora de acceso al día anterior:
sudo touch -a -t 202004151530.02 allowed
El resultado del comando stat muestra que el tiempo de acceso es un día antes:
File: allowed
Size: 1 Blocks: 8 IO Block: 4096 regular file
Device: 806h/2054d Inode: 38805116 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1002/flaskuser) Gid: ( 1002/flaskgroup)
Access: 2020-04-15 15:30:02.000000000 +0200
Modify: 2020-04-16 15:47:06.024559817 +0200
Change: 2020-04-16 15:52:12.472562630 +0200
Birth: -
Ahora generamos una petición en el sitio web y después de la petición ejecutamos el comando stat de nuevo:
File: allowed
Size: 1 Blocks: 8 IO Block: 4096 regular file
Device: 806h/2054d Inode: 38805116 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1002/flaskuser) Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:56:24.200564941 +0200
Modify: 2020-04-16 15:47:06.024559817 +0200
Change: 2020-04-16 15:52:12.472562630 +0200
Birth: -
El tiempo de acceso se actualizó hasta hoy. Las solicitudes posteriores ya no actualizan el tiempo de acceso. Trabajando como se esperaba, he aprendido algo hoy.
Detalles de la implementación
Llamé a la clase CachedAccess. En Flask's before_request lo instancio de la siguiente manera:
@app.before_request
def before_request():
...
g.ip_address = get_ip_address()
...
cached_access = CachedAccess()
if not cached_access.is_allowed():
# bye bye
abort(403)
Y aquí están las partes (importantes) de la clase:
class CachedAccess:
def __init__(self):
...
def log_block(self, reason):
...
def is_allowed_ip_address(self, ip_address_uint):
# check: single ip addresses
access_block_ip_address = db_select(
model_class_list=[AccessBlockIPAddress],
filter_by_list=[
(AccessBlockIPAddress, 'is_active', 'eq', True),
(AccessBlockIPAddress, 'ip_address_type', 'eq', 3),
(AccessBlockIPAddress, 'ip_address_uint', 'eq', ip_address_uint),
],
).first()
if access_block_ip_address is not None:
# found
return False
# check: network and range
access_block_ip_address = db_select(
model_class_list=[AccessBlockIPAddress],
filter_by_list=[
(AccessBlockIPAddress, 'is_active', 'eq', True),
(AccessBlockIPAddress, 'ip_address_type', 'in', [1, 2]),
(AccessBlockIPAddress, 'ip_address_from_uint', 'le', ip_address_uint),
(AccessBlockIPAddress, 'ip_address_to_uint', 'ge', ip_address_uint),
],
).first()
if access_block_ip_address is not None:
# found
return False
return True
def is_allowed(self):
# check if valid ip_address
try:
ip_address_uint = int( ipaddress.ip_address(g.ip_address) )
except Exception as e:
current_app.logger.error(fname + ': not a valid ip address = {}, {}'.format(g.ip_address, str(e)))
return True
# create ip_address_file
app_cached_access_dir = current_app.config['APP_CACHED_ACCESS_DIR']
ip_address_parts = g.ip_address.split('.')
ip_address_file = os.path.join(app_cached_access_dir, *ip_address_parts, 'allowed')
# check if file exists and read its contents
found = True
try:
with open(ip_address_file, 'r') as f:
allowed = f.read()
except:
found = False
if found:
# done
if allowed == '1':
return True
self.log_block(1)
return False
# check if g.ip_address matches a rule in blacklisted IP addresses table
allowed = self.is_allowed_ip_address(ip_address_uint)
# create directories for g.ip_address
ip_address_dir = os.path.dirname(ip_address_file)
try:
pathlib.Path(ip_address_dir).mkdir(parents=True, exist_ok=True)
except Exception as e:
current_app.logger.error(fname + ': error creating directories ip_address_dir = {}, {}'.format(ip_address_dir, str(e)))
return True
# create allowed temp file
temp_name = next(tempfile._get_candidate_names())
ip_address_temp_file = os.path.join(app_cached_access_dir, *ip_address_parts, temp_name)
try:
with open(ip_address_temp_file, 'w') as f:
f.write( '1' if allowed else '0' )
except Exception as e:
current_app.logger.error(fname + ': error writing ip_address_temp_file = {}, {}'.format(ip_address_temp_file, str(e)))
return True
# atomic move ip_address_temp_file to ip_address_file
try:
os.rename(ip_address_temp_file, ip_address_file)
except Exception as e:
current_app.logger.error(fname + ': error renaming ip_address_temp_file = {} to ip_address_temp_file = {}, {}'.format(ip_address_temp_file, ip_address_temp_file, str(e)))
return True
if allowed:
return True
self.log_block(2)
return False
Esto no es realmente muy difícil. Convierto la dirección IP en una Int sin firmar para poder comprobar si está en una red IP o en un rango de direcciones IP. Si se produce un error inesperado, registro el error y permito la dirección IP. Esto significa que no bloqueamos las solicitudes inesperadas.
Desarrollo y producción
En el desarrollo probablemente verá muchas solicitudes bloqueadas mientras se realizan las pruebas. La razón es que las imágenes, los archivos Javascript , etc. también son servidos por el servidor de desarrollo Flask . Puedes filtrar estas peticiones en tu código:
if request_path.startswith( ('/static/') ):
return
En la producción asumo que está sirviendo todo su contenido estático por el servidor web, Nginx, Apache, lo que significa que no se pierde tiempo. Sólo bloqueamos las solicitudes al código, las imágenes, etc. no se bloquean.
Bloqueo con Nginx
No quiero controlar mi servidor web Nginx para mantenerlo simple. Pero no es tan difícil decirle que bloquee las solicitudes. Si usas Nginx, puedes agregar unas pocas líneas para bloquear múltiples agentes user de la siguiente manera:
if ($http_user_agent ~* (wget|curl|libwww-perl) ) {
return 403;
}
Y para bloquear múltiples direcciones IP que puedes usar:
location / {
deny 127.0.0.1; # Individual IP Address
deny 1.2.3.0/24; # IP network
}
Pero esto no es lo que queremos. Queremos un bloqueo dinámico. Hay varias maneras de hacerlo, pero por supuesto tendrás que involucrarte mucho más en las especificaciones de Nginx . Hay suficientes ejemplos en Internet de cómo hacer esto.
Resumen
Realmente quería implementar una lista negra de direcciones IP sobre la marcha y no parecía tan difícil. No implementé todo en este momento. Esto significa que no hay actualizaciones inteligentes después de añadir o eliminar las reglas de la lista negra de direcciones IP. En su lugar, tengo un botón 'flush cache' que puedo pulsar después de hacer cambios en la tabla de la Lista Negra. Es como un 'rm -R' en Python.
La marca de tiempo de acceso Linux me retrasó al escribir este post, nunca usé el tiempo de acceso pero ahora conozco sus peculiaridades. Linux actualiza la hora de acceso una vez al día, eso está bien para mí.
Dudo que puedas obtener un mejor rendimiento pero tal vez quieras mirar otras opciones como el almacenamiento en caché del elemento en la memoria. Podrías usar TTLCache de las herramientas de caché de Python .
Enlaces / créditos
cachetools
https://pypi.org/project/cachetools/
Dynamic Blacklisting of IP Addresses
https://docs.nginx.com/nginx/admin-guide/security-controls/blacklisting-ip-addresses/
flask-ipban
https://github.com/Martlark/flask-ipban
flask-ipblock
https://github.com/closeio/flask-ipblock
flask-limiter
https://github.com/alisaifee/flask-limiter
how to know if noatime or relatime is default mount option in kernel?
https://superuser.com/questions/318293/how-to-know-if-noatime-or-relatime-is-default-mount-option-in-kernel
Why is cat not changing the access time?
https://superuser.com/questions/464290/why-is-cat-not-changing-the-access-time/464737#464737
Leer más
Flask
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow