Нечеткие хеши (fuzzy hashes) используются для поиска похожих писем - то есть, при помощи такого метода можно найти сообщения, которые содержат такой же или незначительно измененный текст. Данная технология хорошо подходит для блокировки спам-рассылок, которые отправляются сразу многим пользователям. Так как хеш функция однонаправлена, то восстановить исходный текст, имея только хеш, невозможно, что позволяет направлять запросы сторонним хранилищам хешей без риска раскрытия информации.
Следует отметить, что rspamd использует нечеткие хеши не только для сравнения текстовых данных, но и изображений и других вложений в письмах. Однако в этом случае отсутсвует элемент нечеткой логики, и rspamd ищет точные соответствия для одинаковых объектов.
Данная статья предназначена для администраторов почтовых систем, которые хотят создать собственное хранилище хешей и обучать его самостоятельно.
В первую очередь необходимо выбрать источники образцов спама для обучения. Основной принцип - использование для обучения спам-рассылок, которые приходят многим пользователям. Существует два основных подхода к решению подобной задачи:
Пользователи являются одним из ресурсов, позволяющих оценивать качество работы системы фильтрации спама, поэтому желательно предусмотреть механизм использования жалоб пользователей для обучения хранилища хешей. К сожалению, пользователи часто жалуются и на легитимные письма, на которые они сами же и подписались: рассылки магазинов, уведомления от систем бронирования билетов и даже на личные письма, которые им по каким-то причинам не нравятся. Многие пользователи просто не видят разницу между кнопками “удалить” и “пожаловаться на спам”. Возможно, хорошей идеей будет запросить у пользователя дополнительную информацию о жалобе, например, почему он считает, что это письмо спам, а так же обратить внимание пользователя на то, что он может отписаться от получения рассылок.
Другим способом решения этой проблемы является ручная обработка жалоб пользователей на спам или же комбинация методов: назначить больший вес письмам, обработанным вручную, и меньший - всем остальным жалобам.
В rspamd также есть две возможности, позволяющие отсеивать некоторые “ложные” срабатывания:
Первый метод довольно прост: давайте назначим каждой жалобе некоторый вес и при каждом обучении будем прибавлять его к сохраненному значению соответствующего хеша. При проверке мы не будем учитывать хеши, вес которых меньше заданного порога. Например, если вес жалобы w=1
, а порог срабатывания t=20
, то для начала срабатывания хеша необходимо как минимум 20 жалоб пользователей. Кроме этого, при проверке для хеша, превышающего пороговое значение, rspamd назначает не максимальные очки, а их величина плавно растет от нуля до максимума (до значения метрики) при изменении веса хеша от порогового до удвоенного порогового значения (t .. 2*t).
Второй метод - фильтры обучения - позволяет написать некоторые условия на языке Lua, которые запрещают обучение, скажем, для писем с определенного домена (например, facebook.com). Возможности фильтров достаточно обширны, но требуют ручной работы по их настройке.
Для данного метода необходимо иметь такие почтовые адреса, на которые не приходит легитимная почта, зато приходит много спама. Основная идея заключается в том, чтобы “засветить” адрес в базах спамеров, не показывая его легитимным пользователям. Для этого можно, например, разместить на достаточно популярном веб-сайте элемент iframe, который не отображается пользователям (имеет свойство hidden или нулевой видимый размер), но содержит доступные ботам email-адреса ловушек. В последнее время этот метод стал не очень эффективен, так как спамеры научились обходить такие приемы.
Другой возможный способ создания ловушки - это поиск доменов, бывших ранее популярными, но не работающих в настоящее время (адреса из этих доменов находятся во многих спамерских базах). В этом случае нужны фильтры обучения, так как высока вероятность того, что в “черный” список попадут легитимные письма, например, от социальных сетей или служб рассылки.
В целом, создание собственных ловушек оправдано только в случае большой почтовой системы, так как это может быть затратно как в плане трудности сопровождения, так и в виде прямых материальных расходов на покупку доменов.
В данной главе я рассмотрю основные настройки хранилища и как оптимизировать его работу.
Важное замечание: хранилище работает не с письмами, а с готовыми хешами. То есть, чтобы преобразовать письмо в его хеш нужен отдельный процесс сканера или контроллера:
Функции хранилища:
Для хранения данных используется sqlite3, и это накладывает некоторые ограничения на архитектуру хранилища.
Во-первых, sqlite крайне плохо работает при конкурентных запросах на запись: в этом случае производительность СУБД падает на несколько порядков. Во-вторых, довольно затруднительно обеспечивать репликацию и масштабирование базы данных, так как для этого нужны сторонние инструменты.
В связи с этим, хранилище хешей rspamd всегда выполняет запись в БД строго из одного процесса. Для этого один из процессов ведет очередь обновлений, а остальные процессы просто передают запросы на запись от клиентов в эту общую очередь. По умолчанию очередь сбрасывается на диск один раз в минуту. Таким образом, хранилище рассчитано на профиль нагрузки с преобладанием запросов на чтение.
Другой важной задачей хранилища является удаление устаревших хешей из базы. Так как обычно пордолжительность рассылок спама ограничена, то нет смысла хранить хеши постоянно. Разумным будет сопоставить количество хешей, которые приходят на обучение в течение определенного времени, с достаточным для их хранения размером оперативной памяти. Например, 400 тысяч хешей занимают около 100 мегабайт, а полтора миллиона хешей занимают уже полгигабайта.
Не рекомендуется допускать увеличение базы до объема, который превышает размер доступной оперативной памяти, из-за значительного ухудшения производительности. Кроме того, нет смысла хранить хеши дольше, чем приблизительно три месяца. Поэтому, если у вас небольшое количество хешей, пригодных для обучения, то лучше установить устаревание в 90 дней. В противном случае лучше установить более короткий период устаревания.
За хранение нечетких хешей отвечает процесс rspamd под названием fuzzy_storage
. Для включения и настройки этого процесса можно использовать локальный файл конфигурации rspamd: etc/rspamd/rspamd.conf.local
:
worker "fuzzy" {
# Socket to listen on (UDP and TCP from rspamd 1.3)
bind_socket = "*:11335";
# Number of processes to serve this storage (useful for read scaling)
count = 4;
# Backend ("sqlite" or "redis" - default "sqlite")
backend = "sqlite";
# sqlite: Where data file is stored (must be owned by rspamd user)
database = "${DBDIR}/fuzzy.db";
# Hashes storage time (3 months)
expire = 90d;
# Synchronize updates to the storage each minute
sync = 1min;
}
По умолчанию rspamd не разрешает модифицировать данные в хранилище. Для того чтобы обучение было возможно, необходимо задать список доверенных IP-адресов и/или сетей. Как правило, лучше всего разрешить запись только с локальных адресов (127.0.0.1 и ::1), так как fuzzy storage использует портокол UDP, который не имеет защиты от подделки IP-адреса источника (что можно исправить путем настройки верификации обратного маршрута на маршрутизаторе, но это зачастую игнорируется системными администраторами):
worker "fuzzy" {
# Same options as before ...
allow_update = ["127.0.0.1", "::1"];
# or 10.0.0.0/8, for internal network
}
Важное замечание: Если в списке серверов в конфигурации плагина fuzzy_check
(см. далее) присутствует имя localhost
, необходимо разрешить доступ как с адреса 127.0.0.1, так и с адреса ::1, так как резолвер возвращает поочередно все известные для имени хоста адреса.
Также для разграничения доступа может использоваться транспортное шифрование, которое мы рассмотрим далее.
Протокол работы с хранилищем хешей позволяет включать необязательное (opportunistic) или обязательное шифрование, основанное на криптосистеме с открытым ключом. Архитектура шифрования основана на конструкции cryptobox: https://nacl.cr.yp.to/box.html и похожа на алгоритм end-to-end шифрования DNSCurve: https://dnscurve.org/.
Чтобы настроить транспортное шифрование, в первую очередь нужно создать пару ключей для сервера хранилища с помощью команды rspamadm keypair -u
:
keypair {
pubkey = "og3snn8s37znxz53mr5yyyzktt3d5uczxecsp3kkrs495p4iaxzy";
privkey = "o6wnij9r4wegqjnd46dyifwgf5gwuqguqxzntseectroq7b3gwty";
id = "f5yior1ag3csbzjiuuynff9tczknoj9s9b454kuonqknthrdbwbqj63h3g9dht97fhp4a5jgof1eiifshcsnnrbj73ak8hkq6sbrhed";
encoding = "base32";
algorithm = "curve25519";
type = "kex";
}
Данная команда создает уникальную пару ключей, в которой открытый ключ (public key) может быть скопирован вручную на хост клиента (например, через ssh) или же опубликован каким-либо способом, гарантирующим достоверность (например, заверен цифровой подписью или размещен на HTTPS-сайте). Закрытый ключ (private key) должен храниться в секрете.
Каждое хранилище может работать одновременно с произвольным числом ключей:
worker "fuzzy" {
# Same options as before ...
keypair = [
{
pubkey = ...
privkey = ...
},
{
pubkey = ...
privkey = ...
},
{
pubkey = ...
privkey = ...
}
]
}
Эта возможность полезна для создания закрытых хранилищ, где доступ разрешен только тем клиентам, которым известен один из открытых ключей:
Для включения режима обязательного шифрования используется опция encrypted_only
:
worker "fuzzy" {
# Same options as before ...
encrypted_only = true;
keypair = [ {
...
} ]
...
}
В этом режиме клиенты, у которых нет действительного открытого ключа, не смогут обращаться к хранилищу.
Для проверки работы хранилища можно использовать команду rspamadm control fuzzystat
:
Statistics for storage 73ee122ac2cfe0c4f12
invalid_requests: 6.69M
fuzzy_expired: 35.57k
fuzzy_found: (v0.6: 0), (v0.8: 0), (v0.9: 0), (v1.0+: 20.10M)
fuzzy_stored: 425.46k
fuzzy_shingles: (v0.6: 0), (v0.8: 41.78k), (v0.9: 23.60M), (v1.0+: 380.87M)
fuzzy_checked: (v0.6: 0), (v0.8: 95.29k), (v0.9: 55.47M), (v1.0+: 1.01G)
Keys statistics:
Key id: icy63itbhhni8
Checked: 1.00G
Matched: 18.29M
Errors: 0
Added: 1.81M
Deleted: 0
IPs stat:
x.x.x.x
Checked: 131.23M
Matched: 1.85M
Errors: 0
Added: 0
Deleted: 0
x.x.x.x
Checked: 119.86M
...
Сначала отображается общая статистика хранилища: количество сохраненных и устаревших хешей, а также распределение запросов по версиям протокола клиента:
v0.6
- запросы от rspamd 0.6 - 0.8 (устаревшие версии, ограниченно совместимы)v0.8
- запросы от rspamd 0.8 - 0.9 (частично совместимы)v0.9
- незашифрованные запросы от rspamd 0.9+ (полностью совместимы)v1.1
- зашифрованные запросы от rspamd 1.1+ (полностью совместимы)Далее по каждому из ключей, сконфигурированных в хранилище, отображается подробная статистика по последним IP-адресам клиентов, от которых были получены запросы. И в заключение можно увидеть общую статистику по IP-адресам.
для изменения вывода этой команды можно использовать следующие опции (например, rspamadm control fuzzystat -n
):
-n
: выводить “сырые” числа без сокращения--short
: не выводить подробную статистику по ключам и IP-адресам--no-keys
: не выводить статистику по ключам--no-ips
: не выводить статистику по IP-адресам--sort
: сортировать:
checked
: по числу проверенных хешей (по умолчанию)matched
: по числу найденных хешейerrors
: по числу ошибочных запросовip
: по IP-адресу лексикографическиfuzzy_check
Плагин fuzzy_check
используется процессами-сканерами для проверки писем и процессами-контроллерами для обучения хранилища.
Функции плагина:
Обучение поизводится командой rspamc fuzzy_add
:
$ rspamc -f 1 -w 10 fuzzy_add <message|directory|stdin>
Где параметр -w
задает вес хеша, который мы обсуждали ранее, а -f
указывает номер флага.
Флаги позволяют хранить хеши различного происхождения в хранилище. Например, хеши спам-ловушек, хеши жалоб пользователей и хеши писем из “белого” списка. Каждый флаг может быть ассоциирован с собственным символом и, соответственно, иметь свой вес при проверке письма:
Флаг можно указывать как по номеру:
$ rspamc -f 1 -w 10 fuzzy_add <message|directory|stdin>
так и по символу:
$ rspamc -S FUZZY_DENIED -w 10 fuzzy_add <message|directory|stdin>
Соответсвие символов флагам нужно настроить в секции rule
.
Пример local.d/fuzzy_check.conf:
rule "local" {
# Fuzzy storage server list
servers = "localhost:11335";
# Default symbol for unknown flags
symbol = "LOCAL_FUZZY_UNKNOWN";
# Additional mime types to store/check
mime_types = ["*"];
# Hash weight threshold for all maps
max_score = 20.0;
# Whether we can learn this storage
read_only = no;
# Ignore unknown flags
skip_unknown = yes;
# Hash generation algorithm
algorithm = "mumhash";
# Use direct hash for short texts
short_text_direct_hash = true;
# Map flags to symbols
fuzzy_map = {
LOCAL_FUZZY_DENIED {
# Local threshold
max_score = 20.0;
# Flag to match
flag = 11;
}
LOCAL_FUZZY_PROB {
max_score = 10.0;
flag = 12;
}
LOCAL_FUZZY_WHITE {
max_score = 2.0;
flag = 13;
}
}
}
Пример local.d/fuzzy_group.conf:
max_score = 12.0;
symbols = {
"LOCAL_FUZZY_UNKNOWN" {
weight = 5.0;
description = "Generic fuzzy hash match";
}
"LOCAL_FUZZY_DENIED" {
weight = 12.0;
description = "Denied fuzzy hash";
}
"LOCAL_FUZZY_PROB" {
weight = 5.0;
description = "Probable fuzzy hash";
}
"LOCAL_FUZZY_WHITE" {
weight = -2.1;
description = "Whitelisted fuzzy hash";
}
}
Рассмотрим некоторые полезные опции, которые можно настраивать в модуле.
Во-первых, max_score
полезен для задания веса порога срабатывания данного хеша:
Вторым полезным параметром является mime_types
, который определяет, какие типы вложений проверять (и обучать!) в данном хранилище. Этот параметр представляет собой список допустимых типов в формате ["type/subtype", "*/subtype", "type/*", "*"]
, где *
заменяет любой допустимый тип. Как правило, достаточно сохранять хеши всех вложений типа application/*
. Текстовые части и вложенные изображения автоматически проверяются плагином fuzzy_check
(то есть, нет нужды добавлять image/*
в список проверяемых вложений). Стоит иметь в виду, что вложения и изображения проверяются на точное соответствие, в отличие от текстов, которые могут немного отличаться.
Очень важной является опция read_only
, если вы хотите обучать ваше хранилище. По умолчанию read_only=true
, что означает невозможность обучения хранилища:
read_only = true; # disallow learning
read_only = false; # allow learning
Параметр encryption_key
задает открытый ключ хранилища и включает шифрование запросов.
Параметр algorithm
определяет алгоритм генерации хешей из текстовых частей писем (для вложений и изображений всегда используется blake2b). Исторически в rspamd использовался алгоритм siphash. Однако он имеет определенные проблемы с производительностью, особенно на устаревшем “железе” (CPU до Intel Haswell). Поэтому при создании собственного хранилища стоит рассмотреть другие, более быстрые алгоритмы:
xxhash
mumhash
fasthash
Для большинства задач я рекомендую использовать mumhash
или fasthash
, которые демонстрируют отличную производительность на множестве платформ. Для оценки поризводительности вы можете скомпилировать набор тестов из исходного кода rspamd:
$ make rspamd-test
и запустить тест различных вариантов алгоритмов вычисления хешей на вашей платформе:
test/rspamd-test -p /rspamd/shingles
Важное замечание: невозможно изменить этот параметр без потери всех данных в хранилище, так как для работы с каждым хранилищем может одновременно использоваться только один алгоритм, а конвертировать один тип хеша в другой не представляется возможным, так как восстановление исходных данных по хешу принципиально невозможно.
Так как плагин fuzzy_check
отвечает в том числе и за обучение, то именно в его конфигурации задается скрипт, определяющий, какие сообщения будут поступать на обучение, а какие нет. Этот скрипт представляет собой функцию Lua с одним аргументом типа rspamd_task
и возвращает либо булево значение: true
- обучать или false
- не обучать, либо пару - булево значение и новый флаг, если в ходе выполнения скрипта выяснилось, что необходимо изменить исходный флаг обучения. Для задания условного скрипта служит параметр learn_condition
, а для записи скрипта удобнее всего использовать многострочный синтаксис heredoc
, который поддерживается UCL
:
# Fuzzy check plugin configuration snippet
learn_condition = <<EOD
return function(task)
return true -- Always learn
end
EOD;
Приведем практические примеры полезных скриптов. Например, часто бывает нужно запретить использовать для обучения письма, отправленные с некоторых доменов. Для этого можно задать такой скрипт:
return function(task)
local skip_domains = {
'example.com',
'google.com',
}
local from = task:get_from()
if from and from[1] and from[1]['addr'] then
for i,d in ipairs(skip_domains) do
if string.find(from[1]['addr'], d) then
return false
end
end
end
end
В некоторых случаях полезно распределять обучение хешей по различным флагам в соответствии с их источником, который можно закодировать, например, в заголовке X-Source
. Допустим, мы хотим иметь такое соответствие флагов и источников:
honeypot
- “черный” список: 1users_unfiltered
- “серый” список: 2users_filtered
- “черный” список: 1FP
- “белый” список: 3Тогда скрипт, осуществляющий такое распределение, может выглядеть следующим образом:
return function(task)
local skip_headers = {
['X-Source'] = function(hdr)
local sources = {
honeypot = 1,
users_unfiltered = 2,
users_filtered = 1,
FP = 3
}
local fl = sources[hdr]
if fl then return true,fl end -- Return true + new flag
return false
end
}
for h,f in pairs(skip_headers) do
local hdr = task:get_header(h) -- Check for interesting header
if h then
return f(hdr) -- Call its handler and return result
end
end
return false -- Do not learn if specified header is missing
end
Зачастую бывает нужно собирать хеши из различных источников или иметь локальную копию удаленного хранилища. Начиная с версии 1.3 в rspamd для решения этой задачи реализована поддержка репликации на уровне хранилища хешей:
Передача хешей инициируется мастером репликации, который отправляет команды обновления хешей (добавление, модификацию или удаление) всем заданным слейвам. Таким образом, слейвы должны иметь возможность принимать соединение от мастера, что нужно учитывать при настройке межсетевых экранов.
Для установки соединения слейв должен прослушивать тот же порт 11335 (по умолчанию), но по протоколу TCP. Синхронизация мастера и слейва осуществляется по протоколу HTTP с транспортным шифрованием HTTPCrypt. Для предотвращения повторных или неверных обновлений слейв проверяет версию обновления, и, если она меньше или равна локальной, то обновление отвергается. В случае же, если мастер опережает слейв более чем на одну версию, в лог файле слейва выводится сообщение вида:
rspamd_fuzzy_mirror_process_update: remote revision: XX is newer more than 1 revision than ours: YY, cold sync is recommended
В таком случае рекомендуется заново клонировать базу данных путем “холодной” синхронизации.
Данная процедура служит для инициализации нового слейва или восстановления репликации после потери связи мастера со слейвом.
Для выполнения синхронизации на хосте мастера нужно остановить сервис rspamd и сделать дамп базы данных хешей (rspamd можно и не останавливать, но если за время клонирования базы данных версия мастера увеличится более чем на 1, процедуру придется повторить):
sqlite3 /var/lib/rspamd/fuzzy.db ".backup fuzzy.sql"
далее полученный файл fuzzy.sql
копируется на все слейвы (rspamd на слейвах можно не останавливать):
sqlite3 /var/lib/rspamd/fuzzy.db ".restore fuzzy.sql"
После чего можно запускать rspamd сначала на слейвах, а затем на мастере.
Репликация настраивается в конфигурационном файле хранилища хешей - worker-fuzzy.inc. Мастер репликации настраивается следующим образом:
# Fuzzy storage worker configuration snippet
# Local keypair (rspamadm keypair -u)
sync_keypair {
pubkey = "xxx";
privkey = "ppp";
encoding = "base32";
algorithm = "curve25519";
type = "kex";
}
# Remote slave
slave {
name = "slave1";
hosts = "slave1.example.com";
key = "yyy";
}
slave {
name = "slave2";
hosts = "slave2.example.com";
key = "zzz";
}
Немного остановлюсь на настройке ключей для шифрования. Обычно для установления шифрованных соединений rspamd не требует конфигурирования ключа на обеих сторонах - ключ клиента генерируется автоматически. Однако в данном случае клиентом выступает мастер, поэтому на слейвах можно задать конкретный (открытый) ключ, по которому они будут проверять команды обновления. Также можно задать допустимые IP-адреса мастера, но защита по ключу является более надежной (кроме того, эти методы можно комбинировать).
Настройка слейва выглядит похоже:
# Fuzzy storage worker configuration snippet
# We assume it is slave1 with pubkey 'yyy'
sync_keypair {
pubkey = "yyy";
privkey = "PPP";
encoding = "base32";
algorithm = "curve25519";
type = "kex";
}
# Allow update from these hosts only
masters = "master.example.com";
# Also limit updates to this specific public key
master_key = "xxx";
Также на слейве можно настроить трансляцию флагов, получаемых с мастера, чтобы избежать коллизий с локальными хешами. Например, если мы хотим транслировать флаги 1
, 2
и 3
во флаги 10
, 20
и 30
соответственно, то можно написать такую конфигурацию:
# Fuzzy storage worker configuration snippet
master_flags {
"1" = 10;
"2" = 20;
"3" = 30;
};