Zero Trust архитектура авторизации между микросервисами в Laravel с API Gateway, scopes, mTLS и signed requests

Авторизация backend-запросов между микросервисами в PHP/Laravel

Содержание
  1. Введение
  2. Базовый пример проблемы
  3. Кто вызывает endpoint
  4. Имеет ли сервис право выполнять действие
  5. Можно ли доверять содержимому запроса
  6. Internal API очень быстро разрастается
  7. Главная ошибка
  8. Плохие варианты
  9. Один общий API key на все сервисы
  10. Невозможно понять источник запроса
  11. Невозможно ограничить права
  12. Компрометация одного сервиса компрометирует всё
  13. Ротация превращается в боль
  14. Доступ только по IP
  15. Почему IP-based security ломается
  16. Доверие только внутренней сети
  17. Передача пользовательского JWT между сервисами
  18. Отсутствие подписи запроса
  19. Главная мысль
  20. Модель угроз
  21. Подмена вызывающего сервиса
  22. Replay attack
  23. Компрометация одного сервиса
  24. SSRF внутри инфраструктуры
  25. Утечка токена
  26. Lateral movement внутри инфраструктуры
  27. Подход 1: Static API Token
  28. Почему этот подход всё ещё используют
  29. Основные проблемы
  30. Нет service identity
  31. Нет разграничения прав
  32. Сложная ротация
  33. Single point of failure
  34. Когда подход допустим
  35. Подход 2: Per-service API keys
  36. Структура таблицы
  37. Почему нельзя хранить raw token
  38. Middleware авторизации
  39. Что это даёт
  40. Подход 3: Scopes и permissions
  41. Почему scopes обязательны
  42. Middleware scopes
  43. Granular authorization
  44. Подход 4: Подписанные запросы
  45. Общая схема
  46. Что это даёт
  47. Почему это важно
  48. Replay protection
  49. Timestamp verification
  50. Nonce verification
  51. Timestamp без nonce недостаточен
  52. Подход 5: OAuth2 Client Credentials
  53. Как это работает
  54. Преимущества
  55. Недостатки
  56. Подход 6: mTLS
  57. Как это выглядит
  58. Плюсы
  59. Минусы
  60. Практическая архитектура для Laravel
  61. Базовые компоненты
  62. ServiceAuthenticator
  63. SignatureVerifier
  64. Audit logging
  65. Ротация ключей
  66. Типичная схема
  67. Что важно
  68. Дополнительно полезно
  69. Типичные ошибки проектирования
  70. Один токен на всё
  71. Internal API без authentication
  72. Отсутствие scopes
  73. Нет replay protection
  74. Нет audit log
  75. Токены живут годами
  76. Internal endpoints доступны извне
  77. Минимальный production-ready вариант
  78. Когда что выбирать
  79. Финальный вывод

Введение

Когда разработчики начинают строить микросервисную архитектуру, вопрос авторизации между сервисами обычно решается в стиле:

  • это же внутренняя сеть
  • доступ только внутри Docker/Kubernetes
  • ну мы же доверяем своим сервисам
  • используем общий API key

Проблема в том, что микросервисы очень быстро превращают инфраструктуру в распределённую систему с десятками точек входа, фоновых обработчиков, очередей, callback’ов и внутренних API.

Именно в этот момент появляется главный вопрос:

Как сервис доказывает, что он действительно имеет право выполнять запрос?

Это уже не пользовательская авторизация.

Здесь нет формы логина, cookie или JWT пользователя в браузере.

Здесь один backend-сервис вызывает другой backend-сервис:

  • Orders создаёт платёж в Billing;
  • Billing отправляет событие в Notifications;
  • Gateway вызывает Users;
  • Analytics читает данные из Payments.

И каждый такой запрос должен отвечать минимум на четыре вопроса:

  1. Кто вызывает endpoint
  2. Можно ли этому сервису вызывать endpoint
  3. Можно ли выполнять конкретное действие
  4. Не был ли запрос подделан или повторён

Во многих проектах эта задача решается крайне примитивно:

Authorization: Bearer super-secret-token

Один токен на всю инфраструктуру.

Один секрет для всех сервисов.

Без scopes.

Без ротации.

Без аудита.

Без проверки подписи.

До первого инцидента это кажется «простым решением». После первого инцидента выясняется, что:

  • невозможно понять, кто именно сделал запрос;
  • невозможно ограничить доступ конкретному сервису;
  • невозможно безопасно отозвать токен;
  • компрометация одного сервиса компрометирует всю систему.

Особенно болезненно это проявляется в системах:

  • с платежами;
  • webhook’ами;
  • внутренними callback’ами;
  • асинхронной обработкой;
  • очередями;
  • административными API;
  • multi-tenant архитектурой.

Важно понимать:

Internal API — это такой же attack surface, как и публичный API.

Современная backend-архитектура давно ушла от модели:

внутренняя сеть = безопасная сеть

Даже внутри Kubernetes-кластера или Docker network сервис не должен получать доверие «по факту существования».

Zero Trust подход постепенно становится стандартом даже для внутренних взаимодействий.

В этой статье разберём:

  • какие существуют подходы к авторизации между микросервисами
  • почему простые схемы часто опасны
  • как реализовать service-to-service авторизацию в Laravel
  • как защищаться от replay attack
  • когда достаточно API key, а когда нужен OAuth2 или mTLS
  • как выглядит production-ready архитектура для internal API

Базовый пример проблемы

Представим типичную микросервисную схему:

Frontend
    ↓
API Gateway
    ↓
Orders Service
    ↓
Billing Service
    ↓
Notifications Service

Пользователь создаёт заказ.

Orders Service должен:

  • создать платёж;
  • зарезервировать баланс;
  • инициировать отправку уведомления.

Для этого сервис начинает вызывать внутренние endpoint’ы других сервисов.

Например:

POST /internal/payments/create
POST /internal/balance/reserve
POST /internal/notifications/send

На первый взгляд кажется, что всё просто.

Сервисы находятся внутри одной инфраструктуры.

Можно вызывать друг друга напрямую.

Но очень быстро появляются фундаментальные вопросы безопасности.

Кто вызывает endpoint

Billing Service получает запрос:

POST /internal/payments/create

Как понять:

  • это действительно Orders Service;
  • это другой внутренний сервис;
  • это случайный контейнер внутри сети;
  • это SSRF-атака через уязвимый сервис;
  • это разработчик вручную отправил curl из production-контейнера.

Если сервис доверяет любому запросу из внутренней сети — это уже проблема.


Имеет ли сервис право выполнять действие

Даже если запрос пришёл от настоящего Orders Service, возникает следующий вопрос:

Можно ли ему вызывать именно этот endpoint?

Например:

  • Orders может создавать платежи;
  • Analytics не должен создавать платежи;
  • Notifications вообще не должен иметь доступ к billing API.

Если все сервисы используют общий internal token, разграничения прав фактически не существует.

Любой скомпрометированный сервис получает доступ ко всей инфраструктуре.


Можно ли доверять содержимому запроса

Даже при наличии API key остаётся проблема подделки данных.

Например:

{
  "user_id": 1,
  "amount": 100000
}

Как убедиться, что:

  • тело запроса не было изменено;
  • запрос не был повторно отправлен;
  • злоумышленник не воспроизвёл старый запрос;
  • кто-то не подменил параметры внутри сети.

Для критичных операций простой bearer token уже недостаточен.

Особенно если речь идёт о:

  • платежах;
  • списаниях;
  • начислениях;
  • изменении баланса;
  • административных действиях.

Internal API очень быстро разрастается

В начале проекта обычно есть 2–3 внутренних endpoint’а.

Через год появляются:

  • internal callback’и;
  • webhook consumers;
  • event processors;
  • cron workers;
  • async consumers;
  • service mesh;
  • Kafka/RabbitMQ consumers;
  • GraphQL federation;
  • background synchronization;
  • межсервисные batch-операции.

Количество внутренних взаимодействий растёт быстрее, чем публичный API.

Именно поэтому service-to-service авторизация — это не «дополнительная защита», а обязательный архитектурный слой.


Главная ошибка

Самая опасная архитектурная ошибка выглядит так:

если запрос пришёл из внутренней сети — ему можно доверять

В современной инфраструктуре это неверно.

Причины:

  • компрометация одного контейнера даёт доступ к другим сервисам;
  • SSRF внутри инфраструктуры становится lateral movement;
  • developer tools могут случайно открыть internal endpoint наружу;
  • Kubernetes network policy часто настроены неполно;
  • staging/dev окружения часто имеют слабую защиту;
  • внутренние сервисы тоже содержат уязвимости.

Поэтому правильная модель выглядит иначе:

каждый сервис обязан доказывать свою identity

Именно вокруг этого дальше строятся:

  • API keys;
  • signed requests;
  • scopes;
  • OAuth2 Client Credentials;
  • mTLS;
  • service identity;
  • Zero Trust архитектура.

Плохие варианты

Перед тем как разбирать production-ready подходы, важно понять одну вещь:

Большинство проблем с безопасностью internal API появляются не из-за отсутствия сложной криптографии.

Они появляются из-за ложного ощущения доверия внутри инфраструктуры.

Ниже — самые распространённые антипаттерны, которые встречаются даже в крупных проектах.


Один общий API key на все сервисы

Классическая схема:

INTERNAL_API_TOKEN=super-secret-token

Все сервисы используют один и тот же токен:

Authorization: Bearer super-secret-token

На старте проекта это кажется удобным:

  • быстро внедрить;
  • не нужно хранить список сервисов;
  • не нужно делать scopes;
  • не нужно думать о ротации.

Но дальше начинаются системные проблемы.

Невозможно понять источник запроса

Если все сервисы используют один токен, backend теряет identity вызывающего сервиса.

Нельзя определить:

  • кто вызвал endpoint;
  • какой сервис скомпрометирован;
  • какой сервис злоупотребляет API;
  • кто создаёт повышенную нагрузку.

Логи становятся бесполезными:

internal request authorized

Но кто именно его выполнил — неизвестно.


Невозможно ограничить права

Общий токен означает полное отсутствие isolation.

Например:

Analytics Service

не должен иметь возможность:

  • создавать платежи;
  • менять баланс;
  • удалять пользователей.

Но при общем токене он технически получает тот же уровень доступа, что и Orders Service.

Фактически вся инфраструктура начинает работать по модели:

если сервис внутри сети — ему можно всё

Это нарушает базовый principle of least privilege.


Компрометация одного сервиса компрометирует всё

Это главный риск shared token architecture.

Представим:

  • найден SSRF в одном сервисе;
  • утёк .env файл;
  • токен попал в логи;
  • произошёл RCE в debug endpoint;
  • разработчик случайно отправил токен в сторонний сервис.

После этого злоумышленник получает доступ:

  • ко всем internal API;
  • ко всем сервисам;
  • ко всем privileged endpoint’ам.

Один секрет становится single point of failure для всей платформы.


Ротация превращается в боль

Когда один токен используется десятками сервисов:

  • его сложно заменить;
  • нужно одновременно обновлять множество deployment’ов;
  • легко получить partial rollout;
  • часть сервисов начинает падать;
  • rollback становится опасным.

В результате токен не ротируется годами.

А любой секрет, который не ротируется годами, однажды обязательно утечёт.


Доступ только по IP

Следующий популярный подход:

allow 10.0.0.0/8;
deny all;

или:

if (! str_starts_with($request->ip(), '10.')) {
    abort(403);
}

Смысл схемы:

если запрос пришёл из внутренней сети — значит ему можно доверять.

Проблема в том, что IP-адрес не является identity.

IP показывает только:

  • откуда пришёл пакет;
  • через какую сеть он прошёл.

Но IP не доказывает:

  • какой именно сервис делает запрос;
  • имеет ли он право выполнять действие;
  • не был ли сервис скомпрометирован.

Почему IP-based security ломается

В современной инфраструктуре:

  • контейнеры создаются динамически;
  • IP постоянно меняются;
  • сервисы масштабируются горизонтально;
  • ingress/controller может проксировать запросы;
  • internal requests идут через service mesh.

Дополнительно:

  • SSRF внутри инфраструктуры обходит IP restrictions;
  • compromised pod получает доступ ко всем внутренним endpoint’ам;
  • staging environment часто находится в той же сети;
  • VPN не гарантирует безопасность сервиса.

IP filtering — это дополнительный слой защиты.

Но не механизм авторизации.


Доверие только внутренней сети

Очень опасный паттерн:

internal endpoint не требует авторизации

Потому что:

  • endpoint не опубликован наружу;
  • route скрыт;
  • сервис «внутренний».

Например:

Route::post('/internal/balance/update');

без:

  • middleware;
  • authentication;
  • signature verification;
  • scopes.

Это превращает internal API в скрытый публичный API.

Как только злоумышленник получает доступ:

  • к одному контейнеру;
  • к SSRF;
  • к VPN;
  • к bastion-host;
  • к CI runner;
  • к staging;

он получает доступ ко всей внутренней инфраструктуре.


Передача пользовательского JWT между сервисами

Следующий спорный подход:

Frontend → Gateway → Orders → Billing → Notifications

И пользовательский JWT прокидывается через все сервисы.

На первый взгляд идея кажется логичной:

  • пользователь уже авторизован;
  • JWT уже существует;
  • можно использовать его везде.

Но здесь смешиваются две разные модели identity:

  • user identity;
  • service identity.

JWT пользователя не отвечает на вопрос:

Какой сервис вызывает endpoint?

Он отвечает только на вопрос:

Какой пользователь инициировал действие?

В итоге:

  • сервисы начинают доверять пользовательскому токену;
  • backend authorization становится неявной;
  • scopes пользователя смешиваются со scopes сервисов;
  • появляется риск confused deputy problem.

Отсутствие подписи запроса

Многие системы ограничиваются проверкой:

Authorization: Bearer token

Но bearer token защищает только доступ.

Он не защищает:

  • тело запроса;
  • query параметры;
  • replay attack;
  • повторную отправку;
  • изменение payload внутри инфраструктуры.

Для критичных операций этого недостаточно.

Например:

{
  "amount": 1000
}

может превратиться в:

{
  "amount": 100000
}

Если нет:

  • signature;
  • body hash;
  • nonce;
  • timestamp verification.

Bearer token не гарантирует целостность запроса.


Главная мысль

Большинство insecure internal API строятся вокруг одной идеи:

внутри инфраструктуры все друг другу доверяют

Современная production-инфраструктура должна строиться наоборот:

каждый сервис обязан постоянно подтверждать свою identity и права

Именно вокруг этой идеи дальше строятся:

  • per-service credentials;
  • scopes;
  • request signing;
  • OAuth2 Client Credentials;
  • mTLS;
  • Zero Trust architecture.

Модель угроз

Перед выбором механизма авторизации нужно определить:

От чего именно мы защищаемся?

Без threat modeling большинство систем приходят либо к недостаточной защите, либо к бессмысленно сложной архитектуре.

Service-to-service авторизация должна учитывать не только внешние атаки, но и внутренние компрометации инфраструктуры.


Подмена вызывающего сервиса

Главный риск:

сервис притворяется другим сервисом

Например:

  • Analytics Service выдаёт себя за Orders Service;
  • compromised pod отправляет internal requests;
  • злоумышленник использует утёкший token.

Если backend не умеет проверять identity сервиса — любая внутренняя система становится доверенной по умолчанию.


Replay attack

Даже валидный запрос может быть опасен.

Например:

POST /internal/payments/charge

с payload:

{
  "payment_id": 100,
  "amount": 5000
}

Если запрос можно повторно воспроизвести:

  • произойдёт повторное списание;
  • повторная выдача бонусов;
  • повторная отправка webhook;
  • повторное изменение состояния.

Bearer token не защищает от replay attack.

Для этого нужны:

  • nonce;
  • timestamp;
  • request signature;
  • idempotency.

Компрометация одного сервиса

Очень частая ошибка:

если сервис trusted — ему можно всё

Но в реальности:

  • любой сервис может содержать RCE;
  • любой контейнер может быть скомпрометирован;
  • любой dependency может иметь supply-chain vulnerability.

Поэтому:

compromised service != compromised platform

должно быть обязательным архитектурным правилом.

Для этого нужны:

  • per-service credentials;
  • scopes;
  • least privilege;
  • audit logging.

SSRF внутри инфраструктуры

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

Если сервис умеет выполнять HTTP requests:

  • webhook fetching;
  • image downloading;
  • metadata parsing;
  • URL previews;
  • PDF rendering;
  • OpenGraph fetching;
  • external integrations;

то SSRF может стать точкой входа во внутреннюю инфраструктуру.

Если internal API доверяет только внутренней сети — SSRF автоматически получает доступ ко всем сервисам.


Утечка токена

Секреты утекают постоянно:

  • через логи;
  • через Sentry;
  • через debug output;
  • через CI artifacts;
  • через screenshots;
  • через .env;
  • через compromised developers machines.

Поэтому любой токен должен рассматриваться как временный секрет.

Архитектура обязана предусматривать:

  • ротацию;
  • отзыв;
  • аудит использования;
  • ограничение scopes;
  • expiration.

Lateral movement внутри инфраструктуры

После компрометации одного сервиса злоумышленник почти всегда пытается:

  • исследовать internal network;
  • получить доступ к другим сервисам;
  • расширить привилегии;
  • найти admin/internal endpoint’ы.

Если сервисы доверяют друг другу без проверки identity — lateral movement становится тривиальным.

Zero Trust архитектура пытается решить именно эту проблему.


Подход 1: Static API Token

Самый простой и самый распространённый вариант.

Сервисы используют общий секрет:

INTERNAL_API_TOKEN=super-secret-token

Клиент отправляет:

Authorization: Bearer super-secret-token

Laravel middleware проверяет значение:

if ($request->bearerToken() !== config('services.internal.token')) {
    abort(401);
}

Почему этот подход всё ещё используют

Причины понятны:

  • внедряется за 10 минут;
  • не требует отдельной таблицы;
  • подходит для MVP;
  • минимальный overhead;
  • легко отлаживать.

Для:

  • 2–3 сервисов;
  • dev environment;
  • pet-project;
  • временной интеграции;

этого действительно может быть достаточно.


Основные проблемы

Но у схемы фундаментальные ограничения.

Нет service identity

Backend не знает:

  • кто именно сделал запрос;
  • какой сервис скомпрометирован;
  • какой сервис создаёт нагрузку.

Нет разграничения прав

Любой сервис получает одинаковые права.

Невозможно реализовать:

  • read-only сервисы;
  • admin-only операции;
  • ограничение privileged endpoint’ов.

Сложная ротация

Один токен используется везде.

Замена секрета требует:

  • coordinated deploy;
  • синхронного обновления конфигов;
  • rollback strategy.

Single point of failure

Компрометация токена = компрометация всей инфраструктуры.


Когда подход допустим

Только если:

  • сервисов мало;
  • нет критичных операций;
  • инфраструктура простая;
  • архитектура временная;
  • есть понимание ограничений.

Static token — это не production-ready модель для зрелой микросервисной платформы.


Подход 2: Per-service API keys

Следующий уровень зрелости:

каждый сервис имеет собственную identity

Например:

ServiceAPI Key
Ordersord_xxx
Billingbill_xxx
Notificationsnotif_xxx
Analyticsan_xxx

Теперь backend умеет определять:

  • кто вызывает endpoint;
  • какие действия разрешены;
  • кто злоупотребляет API;
  • какой сервис скомпрометирован.

Структура таблицы

Например:

service_clients
ПолеНазначение
idидентификатор
nameимя сервиса
token_hashhash токена
scopesсписок разрешений
is_activeактивен ли ключ
last_used_atпоследнее использование
expires_atсрок жизни

Почему нельзя хранить raw token

Плохой вариант:

token = super-secret-token

в базе.

Правильнее:

hash('sha256', $token)

Причина та же, что и с паролями:

  • база может утечь;
  • дамп может попасть в backup;
  • доступ к БД ≠ доступ ко всем сервисам.

Middleware авторизации

Пример Laravel middleware:

class AuthenticateServiceRequest
{
    public function handle(Request $request, Closure $next): mixed
    {
        $token = $request->bearerToken();

        if ($token === null) {
            abort(401);
        }

        $serviceClient = ServiceClient::query()
            ->where('token_hash', hash('sha256', $token))
            ->first();

        if ($serviceClient === null || ! $serviceClient->is_active) {
            abort(401);
        }

        $request->attributes->set('serviceClient', $serviceClient);

        return $next($request);
    }
}

Что это даёт

Появляется:

  • service identity;
  • revoke конкретного сервиса;
  • audit logging;
  • scopes;
  • безопасная ротация;
  • least privilege.

Это уже production-grade foundation для большинства Laravel-проектов.


Подход 3: Scopes и permissions

Сам факт авторизации сервиса ещё не означает право выполнять действие.

Правильная модель выглядит так:

authenticated != authorized

Например:

ServiceРазрешения
Orderspayments.create
Billingbalance.write
Notificationsnotifications.send
Analyticsreports.read

Почему scopes обязательны

Без scopes любой сервис после авторизации получает полный доступ ко всей инфраструктуре.

Это приводит к:

  • privilege escalation;
  • lateral movement;
  • нарушению isolation.

Middleware scopes

Пример:

Route::post(
    '/internal/payments/create',
    CreatePaymentController::class
)->middleware([
    AuthenticateServiceRequest::class,
    EnsureServiceHasScope::class . ':payments.create',
]);

Middleware:

class EnsureServiceHasScope
{
    public function handle(Request $request, Closure $next, string $scope): mixed
    {
        $serviceClient = $request->attributes->get('serviceClient');

        if (! in_array($scope, $serviceClient->scopes, true)) {
            abort(403);
        }

        return $next($request);
    }
}

Granular authorization

Scopes позволяют:

  • отделить read от write;
  • ограничить admin actions;
  • изолировать billing API;
  • делать read-only integrations;
  • ограничивать blast radius при компрометации.

Это один из самых важных элементов Zero Trust модели.


Подход 4: Подписанные запросы

Bearer token решает только authentication.

Но не защищает:

  • body;
  • query params;
  • replay attack;
  • tampering.

Поэтому для критичных операций используют request signing.


Общая схема

Клиент отправляет:

X-Service-Name: orders
X-Timestamp: 1710000000
X-Nonce: 4f8d2c
X-Signature: abc123

Подпись строится из:

  • HTTP method;
  • path;
  • timestamp;
  • nonce;
  • body hash.

Например:

POST:/internal/payments/create:1710000000:4f8d2c:body_hash

И затем:

hash_hmac('sha256', $payload, $secret)

Что это даёт

Теперь backend может проверить:

  • запрос отправил настоящий сервис;
  • body не изменялся;
  • request не подделан;
  • replay attack невозможен.

Почему это важно

Особенно критично для:

  • платежей;
  • списаний;
  • изменения баланса;
  • webhook’ов;
  • privileged operations.

Signed requests сильно уменьшают риск:

  • request tampering;
  • replay attack;
  • internal MITM.

Replay protection

Даже signed request можно повторно воспроизвести.

Поэтому обычно добавляют:

  • timestamp;
  • nonce;
  • expiration window.

Timestamp verification

Например:

if (abs(time() - $timestamp) > 60) {
    abort(401);
}

Запрос старше 60 секунд считается невалидным.


Nonce verification

Nonce хранится в Redis:

$cacheKey = 'nonce:' . $nonce;

if (Redis::exists($cacheKey)) {
    abort(401);
}

Redis::setex($cacheKey, 60, 1);

Повторный запрос с тем же nonce будет отклонён.


Timestamp без nonce недостаточен

Если злоумышленник перехватит запрос в пределах временного окна — его всё ещё можно повторить.

Поэтому:

timestamp + nonce

должны использоваться вместе.


Подход 5: OAuth2 Client Credentials

Когда сервисов становится много, shared secrets начинают плохо масштабироваться.

Тогда появляется отдельный authorization server.

OAuth2 Client Credentials flow решает именно service-to-service authentication.


Как это работает

Сервис получает access token:

POST /oauth/token

с:

client_id
client_secret

Authorization server выдаёт:

{
  "access_token": "...",
  "expires_in": 3600
}

И сервис использует его для internal API.


Преимущества

  • централизованная identity;
  • expiration token’ов;
  • scopes;
  • revoke;
  • audit;
  • standard approach.

Недостатки

  • сложнее инфраструктура;
  • появляется auth-service;
  • нужен token lifecycle management;
  • больше operational complexity.

Для маленьких проектов OAuth2 может быть overengineering.

Но для большой микросервисной платформы — это уже стандарт.


Подход 6: mTLS

mTLS — mutual TLS.

Сервисы аутентифицируют друг друга сертификатами.

Не только сервер доказывает identity клиенту.

Клиент тоже доказывает identity серверу.


Как это выглядит

Каждый сервис получает:

  • client certificate;
  • private key;
  • trusted CA.

Во время TLS handshake сервисы взаимно подтверждают identity.


Плюсы

  • сильная transport-level authentication;
  • отсутствие shared tokens;
  • хорошо интегрируется с service mesh;
  • сложно подделать identity.

Минусы

  • высокая operational complexity;
  • certificate lifecycle management;
  • rotation certificates;
  • сложная отладка;
  • нужен зрелый DevOps.

mTLS хорошо работает:

  • в Kubernetes;
  • в service mesh;
  • в enterprise инфраструктуре.

Но даже mTLS не заменяет scopes и business authorization.


Практическая архитектура для Laravel

Production-ready схема обычно выглядит так:

Service
    ↓
API Key / OAuth Token
    ↓
Authentication Middleware
    ↓
Scope Middleware
    ↓
Signature Verification
    ↓
Business Logic

Базовые компоненты

app/
  Modules/
    ServiceAuth/
      Domain/
        Services/
          ServiceAuthenticator.php
          ServicePermissionChecker.php
          SignatureVerifier.php
      Infrastructure/
        Http/
          Middleware/
            AuthenticateServiceRequest.php
            EnsureServiceHasScope.php
            VerifyServiceSignature.php

ServiceAuthenticator

Отвечает за:

  • поиск service client;
  • проверку token hash;
  • проверку expiration;
  • проверку active state.

SignatureVerifier

Проверяет:

  • body hash;
  • timestamp;
  • nonce;
  • request signature.

Audit logging

Каждый internal request желательно логировать.

Минимум:

  • service name;
  • endpoint;
  • method;
  • status;
  • correlation id;
  • execution time;
  • failure reason.

Но:

  • не логировать raw tokens;
  • не логировать signatures;
  • не логировать secrets.

Ротация ключей

Секреты обязаны ротироваться.

Production architecture должна это поддерживать изначально.


Типичная схема

У сервиса одновременно существуют:

  • current key;
  • next key.

Во время rollout backend принимает оба ключа.

После завершения rollout старый ключ удаляется.


Что важно

Ротация не должна:

  • ломать production;
  • требовать downtime;
  • требовать мгновенного synchronized deploy.

Дополнительно полезно

  • expiration;
  • usage monitoring;
  • alerts на использование старых ключей;
  • forced revoke.

Типичные ошибки проектирования


Один токен на всё

Самая частая и самая опасная ошибка.


Internal API без authentication

“Это же internal endpoint”.

Обычно заканчивается плохо.


Отсутствие scopes

Authentication без authorization.


Нет replay protection

Критичные операции становятся повторяемыми.


Нет audit log

После инцидента невозможно понять:

  • кто делал запрос;
  • что произошло;
  • какой сервис скомпрометирован.

Токены живут годами

Любой долгоживущий секрет рано или поздно утечёт.


Internal endpoints доступны извне

Очень частая проблема:

  • debug routes;
  • forgotten nginx config;
  • staging exposure;
  • ingress misconfiguration.

Internal endpoint должен считаться hostile по умолчанию.


Минимальный production-ready вариант

Для большинства Laravel-систем достаточно следующей схемы:

  • HTTPS;
  • per-service API keys;
  • hash токенов в базе;
  • scopes;
  • middleware authentication;
  • audit logging;
  • key rotation;
  • timestamp + nonce для критичных endpoint’ов.

Это уже даёт:

  • service identity;
  • least privilege;
  • revoke;
  • replay protection;
  • auditability.

Без чрезмерной сложности.


Когда что выбирать

СистемаПодход
2–3 сервисаAPI keys + scopes
Платежиsigned requests + nonce
Большая платформаOAuth2 Client Credentials
Kubernetes + service meshmTLS
Enterprise Zero TrustmTLS + OAuth2 + policy engine

Финальный вывод

Service-to-service авторизация — это отдельный слой безопасности.

Её нельзя заменять:

  • internal network;
  • VPN;
  • Docker network;
  • Kubernetes cluster;
  • shared API token.

Современная архитектура должна исходить из модели:

внутренние сервисы не являются trusted по умолчанию

Каждый сервис обязан:

  • подтверждать identity;
  • иметь ограниченные permissions;
  • проходить audit;
  • иметь возможность revoke;
  • работать по least privilege.

Для большинства Laravel-проектов production-ready минимум выглядит так:

  • отдельный token на сервис;
  • scopes;
  • audit log;
  • ротация;
  • signed requests для критичных операций;
  • replay protection.

Именно это превращает internal API из «набора скрытых endpoint’ов» в полноценную безопасную backend-инфраструктуру.

Рейтинг
( Пока оценок нет )
Максим Гречушников/ автор статьи
Мне 40 лет, и больше половины жизни я занимаюсь разработкой. За это время я работал с корпоративными системами, highload-проектами, интеграциями, платежными решениями, внутренними платформами и legacy-кодом, который пережил не одного разработчика. Некоторые системы, кажется, переживут и нас. Моя специализация — PHP, Laravel, backend-архитектура, DDD, тестирование, рефакторинг и техническое лидерство. Мне интересны задачи, где нужно соединить код, бизнес, данные, процессы и здравый смысл. Последние годы я много работаю с LLM и AI-инструментами: использую их для проектирования, анализа кода, документации, тестов, ревью, работы с legacy и построения инженерных процессов. Для меня LLM — это не замена разработчику, а усилитель для тех, кто умеет думать системно. В блоге я пишу о Laravel, PHP, архитектуре, тестировании, инженерной практике, управлении разработкой и реальном применении AI в работе программиста.
Понравилась статья? Поделиться с друзьями: