Закрытое API: nginx secure link

Допустим, есть API для сугубо внутреннего пользования. Например, взаимодействие мобильных приложений и сайта. API нигде не офишируется, нет публичной документации и вообще никаких упоминаний где-либо вне организации. Но само API открытое. Нет никакой 0Auth авторизации, или JWT, или ещё чего-то. В какой-то момент API начинает работать не только на внутренние цели, но и на пользу нежелательным сторонним пользователям. Начинают напрямую пользоваться API методами, тем самым создавая лишнюю нагрузку на серверах, утечку данных и прочие негативные последствия. Как открытое API превратить в закрытое? При этом не поломать старые валидные клиенты (хотя бы на переходный период). И в перспективе пресечь любые несанкционированные запросы через API. Тут приведу один вариант, полностью средствами веб-сервера nginx. Если кто-то знает ещё удачные варианты - пишите в комментариях.

Модуль ngx_http_secure_link_module

На странице официальной документации отлично описана польза, которую даёт применение этого модуля. Смысл в том, что каждую ссылку можно сделать персональной и с ограниченным сроком жизни. Злоумышленник даже если перехватит ссылку - не сможет ею воспользоваться, т.к. в ссылке зашивается хеш из разной информации, в том числе можно проверять IP адрес клиента. Также можно ограничить срок жизни ссылки, например пятью минутами. После этого периода ссылка перестанет работать у кого-угодно (даже у реального владельца ссылки). Все проверки выполняются на стороне nginx, - до кода дело даже не дойдёт. Используется md5 алгоритм, который вычисляется довольно быстро, - т.е. нагрузка на сервер будет минимальна.

Установка модуля

Модуль ngx_http_secure_link_module обычно не собирается в сборке nginx. Какие модули включены в вашей сборке nginx можно увидеть командой:
nginx -V 2>&1|xargs -n1|grep module
Я обратил внимание, что в Fedora 33 этот модуль включён, а в Ubuntu 20.04 - нет. Если в вашей операционной системе этого модуля не окажется - придётся nginx собирать из исходников - это остаётся за рамками этой статьи.

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

Вот рабочий пример настройки nginx, где есть блок правил API в старом варианте (открытое API) и блоки правил для настройки защищённого режима (закрытое API). На переходный период оставляем оба варианта, а затем потребуется вручную убрать старый вариант и API станет закрытым.
# Защищённые ссылки
location ^~ /s/ {
    secure_link $arg_md5,$arg_expires;
    secure_link_md5 "$secure_link_expires$uri$remote_addr MY_own_SuPeR_sEcReT_string";

    if ($secure_link = "") {
        return 403;
    }

    if ($secure_link = "0") {
        return 410;
    }

    rewrite ^ /secure$uri;
}

# Блок только для внутренних правил nginx
location ^/secure/ {
    internal;

    # Подключаем обработку API
    rewrite ^/secure/s/api/v1/([_a-zA-Z0-9]+)/$ /mobile_api/v1/index.php?apiCode=$1 last;

    # Если предыдущие правила не сработали
    rewrite ^ $uri;
}

# API через защищённые ссылки. Сюда попадаем только из внутреннего location /secure/
location ~ ^/secure/s/api/ {
    rewrite /s/api/v1/([_a-zA-Z0-9]+)/$ /mobile_api/v1/index.php?apiCode=$1 last;
}

# Простое API без защиты: переключить после переходного периода на deny
location ~ ^/api/ {
    # Убрать после переходного периода
    rewrite /api/v1/([_a-zA-Z0-9]+)/$ /mobile_api/v1/index.php?apiCode=$1 last;

    # Включить после переходного периода
    #deny all;
}
Теперь поясню, что происходит в этих наборах правил. Первый описываемый блок - URL, начинающиеся с /s/ - это будут наши новые защищённые ссылки. Т.е. тут можно сделать все применяемые ранее старые простые ссылки, только добавить перед ними /s/. Например, старая ссылка была /api/v1/auth/, а новая аналогичная защищённая ссылка будет /s/api/v1/auth/. Внутри локейшена определяются две директивы secure_link и secure_link_md5, - они позволяют применить две проверки к ссылкам: аутентичность и срок действия.
Для директивы secure_link_md5 определяется, как будет вычисляться md5 хеш для проверки ссылки. В приведённом примере первый блок - срок действия ссылки; второй блок - запрашиваемый URI; третий блок - удалённый адрес (т.е. IP адрес клиента); четвёртый блок - пробел; пятый блок - секретная строка (тут надо придумать свою секретную строку и прописать в конфиге nginx).
Когда nginx определяет, что входящий запрос подходит к этому локейшену, выполняются его правила. Сначала модуль secure link вычисляет и сверяет md5 хеш, затем устанавливает значение переменной $secure_link. Если хеш неправильный - переменная $secure_link имеет значение пустой строки. Если хеш правильный, но срок действия ссылки истёк, то переменная $secure_link становится равна "0". Далее просто проверяется значение переменной $secure_link и выполняется то или иное действие (в примере отдаются коды ответа 403 и 410). Если же все проверки пройдены успешно, то выполняется реврайт на /secure$uri, где $uri - вычисляется из запрошенной строки (для запроса на /s/api/v1/auth/ - $uri = /s/api/v1/auth/).
После первого локейшена в дело вступает второй - запросы, начинающиеся с /secure/. Внутри этого локейшена мы определяем директиву internal - это не позволит делать напрямую запросы на /secure/ и попадать в этот локейшн. Сюда попасть можно будет только из внутренних правил nginx, что и делается в первом описанном локейшене. Далее в этом локейшене делаем обработку всех возможных правил веб-сервера, которые требуются. В данном примере правило только для API (и ещё одно правило по-умолчанию).
Третий локейшн /secure/s/api/ - сюда можно попасть только из внутренних перенаправлений nginx, напрямую никак. При запросе на /secure/s/api/v1/auth/ отработает локейшн ^/secure/ - который объявлен как внутренний - следовательно nginx отдаст тут же ответ 404.
Последний локейшн в приведённом примере - ^/api/ - старый вариант API, без защищённых ссылок. В нём надо будет сделать замену после истечения переходного периода (когда все валидные клиенты будут настроены на работу с защищёнными ссылками). После пробного период тут нужно будет оставить только директиву, запрещающую доступ всем (nginx будет отдавать ответ 403).

Расчёт хеша

Из документации модуля можно почерпнуть пример, как рассчитывается хеш на примере какой-то ссылки. Приведу тут свой пример. Допустим есть старый открытый метод API /api/v1/auth/. Новый защищённый вариант ссылки со сроком до 1 января 2024 года (по гринвичу), секретным словом MY_own_SuPeR_sEcReT_string и клиентом с IP 127.0.0.1 (локально тестируем API) будет выглядеть так /s/api/v1/auth/?md5=1pzX968MiqPu_ZvYYht5Xg&expires=1704067200. Тут хеш посчитан вручную в консоли linux командой:
echo -n '1704067200/s/api/v1/auth/127.0.0.1 MY_own_SuPeR_sEcReT_string' | \
    openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =
Если бы в ссылке были бы ещё какие-то свои GET параметры - они бы не входили в рассчёт хеша.
Т.е. чтобы эта схема заработала, нужно во всех валидных клиентах прописать ту же самую секретную строку и настроить рассчёт хеша перед отправкой каждого запроса. При использовании срока действия ссылки, нужно учесть часовой пояс клиента и сервера. Если хакеры завладеют секретной строкой, то смогут пользоваться API, следовательно нужно предусмотреть этот сценарий и сделать возможным смену секретной строки.

Примеры ответов веб-сервера

Приведу возможные примеры ссылок и ответы веб-сервера с приведённым в статье конфигом.
  1. /secure/s/api/v1/auth/ - 404 - нельзя сразу попасть на /secure/s/ - это возможно только внутренними перенаправлениями в nginx
  2. /s/api/v1/auth/?md5=G59kF10zW7KJ87bEllGFvA&expires=1605687727 - 410 - вышло время жизни ссылки
  3. /s/api/v1/auth/?md5=_1pzX968MiqPu_ZvYYht5Xg&expires=1704067200 - 403 - доступ запрещён из-за неправильного хеша (тут подставлен ещё знак подчёркивания, чем испорчен хеш)
  4. /s/api/v1/auth/?md5=1pzX968MiqPu_ZvYYht5Xg&expires=1704067200 - 200 - нормальное исполнение запроса (передача управления в PHP-код от nginx)
  5. /api/v1/auth/ - 200 - старый вариант обращения к API - оставляем на время до тех пор, пока все клиенты не перейдут на новый формат защищённых ссылок (пока не обновятся приложения у конечных пользователей). После окончания периода поддержки обратной совместимости этот запрос будет возвращать 403 ответ

Комментарии

Популярные сообщения из этого блога

Пропорциональное распределение суммы

Битрикс: своя геолокация

Bitrix24 API - разбор демо приложения третьего типа