angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Внесите IP-адреса в черный список на вашем сайте Flask , работающем на Linux.

16 апреля 2020 возле Peter
В Flask

Иногда нужно немедленно блокировать IP-адреса. Этот пост описывает способ, как вы можете это сделать.

post main image
https://unsplash.com/@vladbahara

У вас есть сайт, и он отлично работает. Но вы заметили, что некоторые посетители пытаются испортить ваши формы. Они приходят с определенных IP-адресов. Также есть боты, которые сканируют ваш сайт. Некоторые из них необходимы, но другие должны держаться подальше. Разве вы не ненавидите это? Ненавижу. В прошлом я однажды написал модуль, который возвращал не очень приятный ответ очень медленно, байт-байт, замедляя работу их систем. Или вернул бесконечное количество данных. Но это другая история.

А пока я хочу сосредоточиться на другом методе: блокировании этих запросов. Просто верните HTTP 403 Forbidden. Я хочу иметь возможность сделать это "на лету" из раздела администрирования моего сайта. Там мы указываем IP-адреса или диапазон IP-адресов, которые мы хотим заблокировать. Есть и другие способы, например, использование файлов .htaccess и настроек веб-сервера. Я расскажу о них внизу этой заметки.

Несколько причин для блокировки

Я уже упоминал, что одной из причин для блокирования доступа к вашему сайту является блокировка вредоносных посетителей. Они хотят посмотреть, как они могут взломать ваш сайт, набить ваш раздел с комментариями рекламой или другими сумасшедшими сообщениями. Есть много причин, по которым я считаю, что одна из них - это то, что они хотят заставить вас захватить сторонний антиспам-плагин. Они могут быть очень эффективными, так как они подключаются к огромным базам данных с информацией о спаме. Но если мы хотим соблюдать конфиденциальность наших посетителей, мы не можем использовать такой плагин. Мы должны использовать другие способы, и последним средством часто является блокировка IP-адреса.

Также может быть необходимо заблокировать определенных ботов, которые сканируют ваш сайт. Некоторые боты генерируют сумасшедший трафик. Я проверил весь трафик на этот сайт за определенный период и оказалось, что только 10%, наверное, даже меньше, запросов было от реальных посетителей! Конечно, не все боты плохие, но некоторые действительно не соблюдают правила. Большинство ботов могут быть идентифицированы по строке User Agent. Я нашел следующие два, которые я действительно хочу заблокировать:

  • SemrushBot
  • AhrefsBot

Будьте осторожны, что блокировать, многие боты используются, чтобы получить ваш сайт в результатах поиска. SemrushBot около SEO, на данный момент я этим не пользуюсь. Блокирование пользовательских агентов в данном посте не рассматривается. Это не будет меняться так часто, и вы можете устанавливать блоки другими способами.

Хорошие вещи нежелательных запросов

Если вы реализовали правильную регистрацию, то вы также можете воспользоваться преимуществами нежелательных запросов. В списке ниже показаны некоторые запросы, которые вызвали ошибку HTTP 404 для данного сайта:

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

Мы видим, что боты ищут файл wlwmanifest.xml. Похоже, что это файл, связанный с 'Windows Live Writer', приложением для публикации блогов, разработанным Microsoft , которое было прекращено в 2017 году и может быть уязвимым. Другая атака ищет PHPUnit, PHP , тестирующую модуль framework. В нем содержалась уязвимость, которая, возможно, еще не была исправлена. Другие боты-атакующие могут генерировать URL, которые приводят к ошибке HTTP 500. Это может быть сделано по замыслу, но также может быть вызвано слабостями вашего сайта.

Хорошая новость заключается в том, что вы можете использовать эту информацию для улучшения вашего сайта. Всегда убедитесь, что реализована правильная запись в журнал, ошибки дают вам очень ценную информацию!

Ограничение только на IP-адреса IPv4

Блокирование посетителей по IP адресам имеет свои ограничения. Многие люди в интернете получают свой IP-адрес при подключении к серверу с помощью DHCP. В основном это относится к мобильным телефонам. Поэтому будьте осторожны, что блокировать.

Также существует IPv6 , который был спроектирован для преодоления ограниченной доступности адресов IPv4 . Хотя в некоторых отчетах говорится, что 30% интернет-трафика находится на IPv6, количество серверов, на самом деле включивших IPv6 , гораздо меньше. Это, к счастью, означает, что в данный момент нет причин для миграции вашего сервера на IPv6 . Блокировать спам IPv6 этим методом возможно, но есть одна загвоздка.

Операции администратора и правила IP-адресов в черном списке

У администратора я хочу указать IP-адреса, которые я хочу занести в черный список. Есть таблица с записями IP-адресов в черный список. Для IP-адресов я хочу иметь возможность указать IP-адреса следующим образом:

  1. Один IP-адрес, например: 1.2.3.4
  2. IP-сеть, например: 1.2.3.0/24.
  3. Диапазон IP-адресов, например: 1.2.3.6-1.2.4.2

Я указываю одну из них в одной записи и называю ее 'Blacklisted IP Address Rule'.

Кэширование, чтобы избежать доступа к базе данных

Конечно, мы не хотим получать доступ к базе данных по каждому запросу, чтобы проверить, разрешен ли этот запрос. Это приведет к замедлению запросов. Вот почему мы используем кэширование. Вместо того, чтобы опрашивать базу данных, мы сначала проверяем кэш, чтобы посмотреть, получал ли IP-адрес доступ к сайту раньше. Для каждого IP-адреса у нас есть флаг "разрешен". Если True, то доступ разрешен, если False, то доступ блокируется.

Если IP-адрес находится в кэше, то мы закончили, продолжили или заблокировали. Если IP-адреса нет в кэше, мы проверяем, есть ли он в правилах IP-адресов, занесенных в черный список. Результат добавляется в кэш, и в следующий раз, когда запрос с этим IP-адресом попадает на наш сайт, данные находятся в кэше и базу данных не нужно опрашивать.

Добавление и удаление правил IP-адресов, внесенных в черный список

Допустим, у нас в кэше сотни, тысячи элементов. Теперь мы хотим внести изменения, используя администратора, либо добавив правило IP-адреса из черного списка, либо удалив правило IP-адреса из черного списка.

Добавление или удаление правила не тривиально, потому что правило может включать IP адреса, которые уже находятся в кэше. Что следует делать с нашими кэшированными значениями? Самый простой способ - это промыть кэш и позволить ему пересобрать заново. Это замедлит следующие запросы на короткое время. Единственный другой способ - это просканировать IP-адреса в кэше и проверить, соответствуют ли они добавленному или удаленному правилу IP-адресов из черного списка. Если они совпадают, мы удаляем их из кэша. У меня есть несколько идей, как это реализовать, но я этого еще не сделал.

Добавление временных меток

Для максимальной производительности информация об IP-адресах в кэше доступна только для чтения и не имеет срока годности. Это означает, что со временем она может расти огромными темпами, если у вас много посетителей. Поскольку большинство посетителей заходит на ваш сайт всего на несколько минут, мы можем добавить временную метку к кэшированным IP-адресам, которая обновляется при каждом доступе. Штемпель времени позволяет легко удалять старые записи.

Запросы в тот же момент

Допустим, два запроса, запрос А и запрос В, приходят одновременно, оба с использованием одного и того же IP-адреса. Если их нет в кэше, оба будут проверять, заблокирован ли их IP-адрес с помощью поиска в таблице "Правила IP-адресов черного списка". Затем они оба обновят элемент cache_access. Запрос A сначала создает элемент разрешенного доступа. Но затем запрос B создает разрешенный элемент, перезаписывая разрешенный элемент запроса A. То же самое верно, если мы хотим обновить метку времени кэшируемого элемента. Это может показаться плохим, но на самом деле это не так уж и плохо. Мы просто должны убедиться, что операция создания атомарной.

Использование файловой системы Linux в качестве кэша

На данный момент я решил реализовать кэш с файлами. Файловая система Linux достаточно быстра, чтобы справиться с этим для моего приложения. Я не хочу добавлять что-то вроде Redis, я хочу сохранить зависимости минимальными.

Если у нас есть 'разрешенный' файл на IP-адрес, то файл может быть небольшим, содержимое - 0 (заблокировано) или 1 (разрешено). Чтобы предотвратить огромное количество файлов в каталоге и замедлить поиск, мы создаем подкаталоги на основе IP-адреса. Мы разделяем IP-адрес точкой ('.') и используем это для создания каталогов. Временная метка 'разрешенного' файла автоматически изменяется при чтении файла. В Linux у нас есть следующие метки времени:

  • mtime (ls -l)
    Последний раз содержимое файла изменялось.
  • ctime
    Последний раз статус файла, например, права доступа, изменялся.
  • atime (ls -lu)
    Последний раз, когда файл был прочитан.

Для нашей цели мы можем использовать время как временную метку. Нам не нужно обновлять время файла. Существует проблема, если вы хотите иметь возможность показать содержимое разрешенных файлов у администратора. Это может привести к чтению файлов и изменению временных меток. Мы можем преодолеть это, создав копию 'разрешенного' файла. Чтение копии не изменяет время оригинального 'разрешенного' файла.

Предупреждение при использовании времени доступа Linux atime

В интернете много информации о метках времени Linux , но лишь немногие упоминают, что это может работать не так, как ожидалось. Приглашаю вас ознакомиться с приведенными ниже ссылками на эту тему. Например, с помощью этой команды вы можете проверить, является ли реляционное время опцией монтирования:

cat /proc/mounts | grep relatime

Итог такой:

  • Обновление времени при каждом прочтении отключено по умолчанию из соображений производительности.
  • Начиная с версии ядра 2.6.30, реляционное время является опцией по умолчанию.
  • Начиная с версии ядра 2.6.30, последнее время доступа к файлу всегда обновляется, если он старше 1 дня.

Это означает, что мы все еще можем использовать время, но должны уважать разрешение одного дня. Для меня это не проблема, но подождите, давайте проверим, действительно ли это работает.

ls -l

В результате:

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

Далее мы хотим увидеть время или время доступа к разрешенному файлу:

stat allowed

В результате:

  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: -

Теперь мы меняем время доступа на предыдущий день:

sudo touch -a -t 202004151530.02 allowed

Результат команды stat показывает, что время доступа на один день раньше:

  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: -

Теперь мы генерируем запрос на сайте и после запроса снова запускаем команду stat:

  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: -

Время доступа было обновлено до сегодняшнего дня. Последующие запросы больше не обновляют время доступа. Работая, как и ожидалось, сегодня я кое-что узнал.

Детали внедрения

Я позвонил в класс CachedAccess. В Flask before_request я инстанцирую его следующим образом:

    @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)

И вот (важные) части класса:

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

На самом деле это не очень сложно. Я преобразовываю IP-адрес в неподписанный Int, чтобы мы могли проверить, находится ли он в IP-сети или в диапазоне IP-адресов. Если возникает неожиданная ошибка, я записываю ошибку в журнал и разрешаю IP адрес. Это означает, что мы не блокируем неожиданные запросы.

Разработка и производство

При разработке вы, вероятно, увидите много запросов, заблокированных во время тестирования. Причина в том, что образы, файлы Javascript и т.д. также обслуживаются сервером разработки Flask . Вы можете отфильтровать эти запросы в своем коде:

    if request_path.startswith( ('/static/') ):
        return

На производстве я предполагаю, что вы обслуживаете весь ваш статический контент веб-сервером, Nginx, Apache, а это значит, что время не теряется впустую. Мы только блокируем запросы к коду, изображения и т.д. не блокируются.

Блокировка с помощью Nginx

Я не хотел управлять моим Nginx веб-сервером, чтобы упростить его работу. Но не так уж и сложно сказать ему, чтобы он блокировал запросы. Если вы используете Nginx, вы можете добавить несколько строк, чтобы заблокировать несколько user агентов следующим образом:

    if ($http_user_agent ~* (wget|curl|libwww-perl) ) {
        return 403;
    }

И для блокировки нескольких IP-адресов вы можете использовать:

    location / {
        deny 127.0.0.1; # Individual IP Address
        deny 1.2.3.0/24; # IP network
    }

Но это не то, что мы хотим. Мы хотим динамической блокировки. Есть несколько способов сделать это, но, конечно, вам придется гораздо глубже вникать в специфику Nginx . В интернете достаточно примеров того, как это сделать.

Резюме

Мне очень хотелось реализовать черный список IP-адресов "на лету", и это оказалось не так уж и сложно. На данный момент я не все реализовал. Это означает, что никаких умных обновлений после добавления или удаления правил IP-адресов из черного списка. Вместо этого у меня есть кнопка 'flush cache', которую я могу нажать после внесения изменений в таблицу черного списка. Это как 'rm -R' в Python.

Метка времени доступа Linux задержала меня в написании этого сообщения, я никогда не использовал время доступа, но теперь я знаю его особенности. Linux обновляет время доступа один раз в день, меня это устраивает.

Сомневаюсь, что вы сможете получить лучшую производительность, но, возможно, вы захотите взглянуть на другие варианты, такие как кэширование элемента в памяти. Вы можете использовать TTLCache из кэш-инструментов Python .

Ссылки / кредиты

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

Подробнее:
Flask

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.