Привет, Хабр, это моя первая статья. Меня зовут Константин, я системный инженер в компании ГНИВЦ. Здесь я хотел бы вам рассказать, что такое Envoy и как с его помощью можно упростить жизнь разработчикам и повысить надёжность взаимодействия микросервисов, минуя инфраструктуру для кого-то страшного и непонятного Kubernetes, а используя простой и старомодный Docker. Также эта статья поможет познакомиться с Envoy поближе и узнать, как он шагает в ногу с таким проектом как Istio.
Что это такое?
Envoy — это L4-L7-балансировщик, написанный на C++ и ориентированный на высокую производительность и доступность. Он обладает отличной observability, в отличие от обычного Nginx, где по метрикам всё скудно. Интереснее разве что Nginx+, но сегодня мы рассматриваем open-source решения. Envoy включает множество настроек для проксирования, они же фильтры, и возможностей для обеспечения безопасности.
Что будем рассматривать?
В статье хотелось бы рассказать про различные варианты настройки Envoy с примерами — для тех, кто не хочет читать официальную документацию, которая была автосгенерирована по .proto
файлам, а ознакомиться и понять: нужно ли нам это вообще? В основном берём статическую конфигурацию. В этой статье я не буду затрагивать динамическую конфигурацию и протокол xDS — это уже отдельная история, которую я опишу позже. Технологии, которые затронем: observability, circuit breaking, authentication, authorization, outlier detection, healt check, retries, mtls и, конечно же, паттерны отказоустойчивости.
Как envoy может помочь при разработке?
Здесь относительно всё просто. Разработчики, когда пишут REST API, много времени могут тратить не на бизнес-код, а на различную логику, которую можно заменить одним только прокси. Так вот вопрос: а чем же мы, как инженеры, можем им помочь? Например, такие задачи может решить Envoy:
-
Изоляция инфраструктурных задач — сюда могут входить retries и таймауты на выполнение различных запросов, балансировка, роутинг. Под роутингом мы можем деплоить как A/B-тестирование, canary, blue/green — смысл, я думаю, понятен.
-
Снятие нагрузки с бизнес-логики — Envoy умеет аутентифицировать и авторизировать запросы, а именно через OAuth2, JWT, RBAC. И с точки зрения безопасности есть что покрутить — SSL/TLS и лимитирование запросов (rate limit).
-
Мониторинг и трассировка — сюда входят логирование, метрики в формате OpenMetrics для Prometheus, а также поддержка трассировки (Jaeger, Zipkin, OpenTelemetry). Если один сервис вызывает другой, Envoy может автоматически вставлять Trace ID для отслеживания потока данных.
-
Обеспечение надежности — сюда можно отнести Circuit Breakers, Outlier Detection, Health Check
На выходе мы должны получить меньшее количество багов (но это не точно) и большее количество фичей в продакшн.
Паттерны отказоустойчивости
-
Circuit Breaker (прерывание цепи) — предотвращение перегрузки зависимых сервисов. Envoy поддерживает механизм circuit breaking, который отключает отправку запросов к сервису, если он становится недоступным или превышает заданные лимиты (например, по количеству ошибок, задержкам или запросам).
-
Retries and Timeouts (повторы и таймауты) — повторение запросов в случае временных сбоев или задержек. Envoy может автоматически повторять запросы в случае ошибок (например, 5xx, 4xx, таймаутов). Вы также можете задать стратегию ограничений на повторы.
-
Outlier Detection (выявление «проблемных» хостов) — автоматическое исключение хостов из кластера, если они ведут себя нестабильно (например, медленные ответы или высокая частота ошибок).Этот механизм позволяет исключать проблемные узлы из пула доступных конечных точек. Поддерживает проверки: HTTP, gRPC, Redis и Thrift
-
Load Balancing (балансировка нагрузки) — распределение запросов по доступным узлам.Envoy поддерживает несколько стратегий балансировки нагрузки а именно Round Robin, Least Request, Random, Maglev, Ring Hash. Кому интересно почитать дальше то вам сюда
-
Rate Limiting (ограничение скорости запросов) — защита от перегрузки путем ограничения частоты запросов. Envoy поддерживает локальные и глобальные ограничения скорости с помощью Rate Limit Service (RLS). Можно настроить ограничения на уровне маршрутов.
-
Health Checks (проверки работоспособности) — здесь много говорить не стоит, все и так знают, что такое активные проверки здоровья на endpoint. Поддерживает проверки: HTTP, gRPC, L3/L4, Redis и Thrift
-
Failover (переключение на резервные хосты) — автоматическое переключение трафика на резервные хосты в случае недоступности основного. Envoy поддерживает управление приоритетами в кластерах.
-
Traffic Shadowing (теневое копирование трафика) — клонирование трафика для тестирования на резервных сервисах. Позволяет отправлять копию реального трафика на тестовый сервис без влияния на основной
-
Fault Injection (имитация сбоев) — тестирование устойчивости системы при отказах. Вы можете настроить Envoy для имитации задержек или ошибок.
Весь этот список поддерживает envoy и, конечно же, мы его можем реализовать в нашей инфраструктуре. Разберем мы сегодня не все, но попытаемся охватить, что реально может понадобиться в боевой среде.
Установка как docke- контейнер
Начнем установку docker pull envoyproxy/envoy:v1.31.3 — звучит просто, согласитесь? Что нам нужно дальше? Конечно, запустить его, либо как docker-compose
, либо как обычный контейнер. Но я покажу вариант с docker-compose
. Здесь всё обычно: открываем порты под listener на 10000 (он по умолчанию слушает Envoy) и админский интерфейс, откуда мы будем брать метрики. Иначе зачем иметь большую наблюдаемость и не следить за ней?
version: '3.8' services: envoy: image: envoyproxy/envoy:v1.31.3 container_name: envoy ports: - "9901:9901" - "10000:10000" volumes: - ./envoy.yaml:/etc/envoy/envoy.yaml:ro restart: always
Что нам ещё нужно сделать: создать статический файл конфигурации, поместить его на сервер и через volume подкинуть в контейнер. На этом с запуском контейнера мы закончили.
Ниже — статическая конфигурация по умолчанию. Но есть нюанс: метрики публикуются по пути ip:9901/stats/prometheus
, API для управления Envoy доступно на порту 9901. Если к этому порту будет доступ у любого желающего, то он сможет делать с вашим Envoy всё, что захочет.
Поэтому лучше позаботиться об этом заранее и запустить интерфейс администратора на 127.0.0.1:9901
. На эту тему есть issue.
envoy-demo.yaml
admin: # Административный интерфейс для метрик и диагностики access_log_path: "/dev/null" address: socket_address: address: 0.0.0.0 port_value: 9901 static_resources: listeners: - name: listener_0 address: socket_address: address: 0.0.0.0 port_value: 10000 protocol: TCP filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http access_log: - name: envoy.access_loggers.stdout typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: prefix: "/" route: host_rewrite_literal: www.envoyproxy.io cluster: service_envoyproxy_io clusters: - name: service_envoyproxy_io type: LOGICAL_DNS # Comment out the following line to test on v6 networks dns_lookup_family: V4_ONLY load_assignment: cluster_name: service_envoyproxy_io endpoints: - lb_endpoints: - endpoint: address: socket_address: address: www.envoyproxy.io port_value: 443 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext sni: www.envoyproxy.io
Маршрутизация и mTLS между Envoy-контейнерами
Условно наши два сервиса service A и service B общаются между собой по TCP. Что мы можем сделать? Например, для безопасного соединения между сервисами подкинуть mTLS.
static_resources: listeners: - name: tcp_listener address: socket_address: { address: 0.0.0.0, port_value: 10000, protocol: TCP } filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: ingress_tcp cluster: backend_envoy transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext require_client_certificate: true common_tls_context: validation_context: trusted_ca: filename: certs/cacert.pem match_typed_subject_alt_names: - san_type: DNS matcher: exact: serviceA tls_certificates: - certificate_chain: { filename: "certs/serverkey.pem" } private_key: { filename: "certs/servercert.pem" } clusters: - name: backend_envoy connect_timeout: 0.5s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: backend_envoy endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: backend_envoy, port_value: 9001 } transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext sni: serviceB common_tls_context: validation_context: trusted_ca: filename: certs/cacert.pem match_typed_subject_alt_names: - san_type: DNS matcher: exact: "*.proxy-example" tls_certificates: - certificate_chain: { filename: "certs/clientcert.pem" } private_key: { filename: "/certs/clientkey.pem" }
Что у нас здесь происходит? Мы сначала объявляем listener 10000 и tcp-фильтр, с помощью которого мы будем управлять нашим трафиком, настраиваем так, чтобы наш Envoy принимал upstream и downstream. DownstreamTlsContext — это контекст TLS, когда подключаются к нам. UpstreamTlsContext — это контекст, когда мы роутим трафик на восходящий сервис. Далее происходит следующее, мы говорим Envoy: «проверяй все клиентские сертификаты на входе. Убедись, что ты знаешь об общем центре сертификации и что указан SAN (например, serviceA). Если что-то не совпадает — сбрасывай соединение. Если всё окей, то передай трафик на наш backend_envoy
и также проверь у него SAN(*.proxy-example), SNI(serviceB) и общий CA».
Итого, у нас есть два контейнера. Первый работает на upstream, Второй — на downstream. С более детальными настройками можно ознакомиться здесь и здесь. Мы получили безопасное mTLS-взаимодействие, но немного потеряли в latency — примерно на 2.5 ms.
Балансировка HTTP/2 с фильтрами
В основе конфигурации лежит пример балансировки трафика HTTP/2, но мы можем использовать и другие версии протокола — HTTP/1.1 или HTTP/3. Выбор зависит от возможностей серверов в upstream-кластере и их поддержки определённых протоколов.
static_resources: listeners: - name: http_listener address: socket_address: address: 0.0.0.0 port_value: 10000 protocol: TCP filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http access_log: - name: envoy.access_loggers.stdout typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog codec_type: AUTO route_config: name: local_route virtual_hosts: - name: http_service domains: ["*"] routes: - match: { prefix: "/" } route: timeout: 15s cluster: http_backend retry_policy: retry_on: "5xx,retriable-4xx" num_retries: 3 per_try_timeout: 2s http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: http_backend connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions common_http_protocol_options: idle_timeout: 1h auto_config: http_protocol_options: {} http2_protocol_options: allow_connect: true connection_keepalive: interval: 1s timeout: 2s http3_protocol_options: idle_timeout: 300000ms quic_protocol_options: max_concurrent_streams: 100 connection_keepalive: max_interval: 4s initial_interval: 150000ms allow_extended_connect: true circuit_breakers: thresholds: - priority: "DEFAULT" max_connections: 1024 max_pending_requests: 1024 max_requests: 1024 max_retries: 6 max_connection_pools: 1024 retry_budget: min_retry_concurrency: 3 outlier_detection: consecutive_5xx: 5 interval: 10s base_ejection_time: 30s max_ejection_time: 300s max_ejection_percent: 50 successful_active_health_check_uneject_host: false split_external_local_origin_errors: false failure_percentage_minimum_hosts: 3 success_rate_minimum_hosts: 3 consecutive_local_origin_failure: 5 enforcing_consecutive_local_origin_failure: 100 enforcing_local_origin_success_rate: 100 health_checks: - timeout: 2s interval: 30s unhealthy_threshold: 3 healthy_threshold: 2 method: "GET" http_health_check: path: "/health" request_headers_to_add: - header: key: "Host" value: "backend_service" load_assignment: cluster_name: http_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: address: backend_service port_value: 80 - endpoint: address: socket_address: address: backend_service_2 port_value: 80 - endpoint: address: socket_address: address: backend_service_3 port_value: 80
Что из этого конфига мы можем понять? Мы слушаем все сетевые интерфейсы на порту 10000 по TCP. Для всего трафика, который идет по HttpConnectionManager добавляется префикс ingress_http к метрикам. Логи пишем в stdout. Логи можно записывать как в stdout, так и в stderr, в формате JSON или TEXT. Вывод можно настраивать под любые потребности, но по умолчанию мы видим примерно следующее:
stdout log
[2016-04-15T20:17:00.310Z] "POST /api/v1/locations HTTP/2" 204 - 154 0 226 100 "10.0.35.28" "nsq2http" "cc21d9b0-cf5c-432b-8c7e-98aeb7988cd2" "locations" "tcp://10.0.2.1:80"
За подробностями как обычно сюда
Дальше по фильтру: всё, что пришло на /, отправляется в cluster http_backend, который имеет в себе 3 конечные точки для балансировки. Установлен глобальный таймаут на upstream — 15-и секунд. Если прилетают ошибки 5xx (p.s. тут тоже можно настраивать, на какие именно типы ошибок выполнять повторы), то выполняется 3 повтора с таймаутом 2 секунды на каждый. Если в течение 2-ух секунд начнётся ответ, счётчик повторов сбрасывается.
Cluster в Envoy — это одновременно список наших конечных точек и конфигурация отказоустойчивости. Таймаут на подключение к конечным точкам установлен в 0.25 секунды. Важно: у каждой системы свои настройки. Здесь мы приводим пример, как это можно сделать. Установлен STRICT_DNS (подробнее можно почитать по ссылке) и метод балансировки round robin. Также включён наш размыкатель Circuit Breakers, который при превышении заданных лимитов позволяет бэкенду «отдохнуть». По дефолту имеет неплохие настройки для всего upstream, но все зависит от того, насколько нагружена наша система. Почитать можно по линку.
Circuit Breakers включён по умолчанию. Если хотите его отключить, установите везде значение 1000000000.
Пример из документации
circuit_breakers: thresholds: - priority: DEFAULT max_connections: 1000000000 max_pending_requests: 1000000000 max_requests: 1000000000 max_retries: 1000000000 - priority: HIGH max_connections: 1000000000 max_pending_requests: 1000000000 max_requests: 1000000000 max_retries: 1000000000
В Envoy блок typed_extension_protocol_options
предоставляет возможность настраивать протоколы HTTP/1.1, HTTP/2 и HTTP/3 для upstream-соединений, адаптируя их под различные сценарии использования. При использовании AutoHttpConfig
кластер автоматически выбирает протокол через ALPN (Application-Layer Protocol Negotiation): HTTP/2 будет использоваться, если он поддерживается, в противном случае — HTTP/1.1. Если upstream-серверы не поддерживают ALPN, Envoy перейдёт на HTTP/1.1. Однако важно, чтобы транспортные сокеты поддерживали ALPN, иначе конфигурация завершится ошибкой. При наличии нестандартных ALPN-настроек Envoy сначала попробует их, но в случае их недоступности переключится на стандартные протоколы HTTP/2 и HTTP/1.1
Общие параметры соединений задаются через common_http_protocol_options
. Например, idle_timeout: 1h
определяет время бездействия, после которого соединение закрывается. Раздел auto_config
позволяет детально настроить параметры для каждого протокола. Для HTTP/1.1 используются стандартные настройки без изменений. В HTTP/2 включён режим allow_connect
, а также настроены интервалы keep-alive: сигнал каждые 1 секунду и таймаут на ответ 2 секунды. Для HTTP/3 добавлены параметры протокола QUIC: ограничение на количество потоков (max_concurrent_streams: 100
), интервалы keep-alive с начальным значением 150000 миллисекунд и максимальным 4 секунды, а также таймаут бездействия 300000 миллисекунд. Включена поддержка расширенного CONNECT, что полезно для проксирования.
Также у нас включены такие фичи как: Outlier detection, пассивная проверка, и Health checking, активная. С их помощью мы понимаем, кто из хостов жив, а кто «бьётся в конвульсиях» и ему надо дать полежать, подумать над своим поведением. Split_external_local_origin_errors — очень важная фича. По умолчанию она отключена (false). Когда она выключена, сбросы TCP, таймауты и 5xx ошибки от бэкенда валятся в одну кучу, что условно всё приравнивается к 5xx для Envoy. Если включить (true), то ошибки на уровне L4 и L7 начинают считаться отдельно. Таймауты, сбросы TCP и ошибки ICMP идут в одну группу, HTTP-ответы от бэкенда — в другую. Для TCP-роутинга всё немного проще: любая ошибка от TCP-фильтра приравнивается к 5xx как HTTP. successful_active_health_check_uneject_host — по умолчанию true. Это значит: если активная проверка показала, что хост здоров, то Envoy игнорирует все выбросы и считает его рабочим. Это довольно жёсткое поведение, поэтому принимайте решение, нужно ли вам это, или лучше отключить. Про алгоритм выброса можно почитать подробнее, но если вкратце: сначала он смотрит список доступных хостов и %, ниже которого нельзя опускаться и после начинает выкидывать «плохие» хосты на период 30s с максимальным значением выброса 5m. После того как хост показывает, что он здоров и готов работать, то он возвращает его в строй, и отсчитывается время в обратном порядке до значения, которое указано в yaml конфиге.
Panic mode — ключевой механизм балансировки трафика. По умолчанию он активируется, если процент здоровых хостов падает ниже 50%. В этом случае Envoy предполагает, что произошел сбой, и автоматически возвращает в строй все хосты.
У нас есть два управляющих тумблера:
-
Отключение паники — можно установить порог в 0%, чтобы механизм паники не срабатывал, и все хосты считались доступными
-
Активация паники — если процент доступных серверов опускается ниже 50%, Envoy заблокирует все хосты и вернет ошибку «503 — no healthy upstream».
Подробнее о настройке panic mode читайте здесь.
Health check: здесь всё просто. Отправляем GET запрос на /health, добавляем, как пример, заголовок Host: backend_service
и ждём ответа 200. Получили — хорошо, не получили — плохо. Можно настроить проверку как для всего cluster, так и для каждой конечной точки индивидуально. На этом заканчиваем с HTTP и переходим к gRPC, где расскажу поменьше, так как основные фишки вы уже освоили, но добавлю новых, чтобы не повторяться.
Локальный health check на endpoints
load_assignment: endpoints: - lb_endpoints: - endpoint: health_check_config: port_value: 8080 address: socket_address: address: 127.0.0.1 port_value: 80 address: socket_address: address: localhost port_value: 80
Балансировка gRPC с фильтрами
static_resources: listeners: - name: grpc_listener address: socket_address: { address: 0.0.0.0, port_value: 10000, protocol: TCP } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: grpc codec_type: AUTO route_config: name: grpc_route virtual_hosts: - name: grpc_services domains: ["*"] routes: - match: prefix: "/" route: cluster: grpc_backend retry_policy: retry_on: "cancelled,internal,deadline-exceeded" num_retries: 3 per_try_timeout: 2s request_mirror_policies: - cluster: shadow_backend runtime_fraction: default_value: numerator: 100 denominator: HUNDRED http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.grpc_http1_bridge typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_http1_bridge.v3.Config upgrade_protobuf_to_grpc: true - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: grpc_backend connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: allow_connect: true connection_keepalive: interval: 1s timeout: 2s load_assignment: cluster_name: grpc_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: 192.168.1.1, port_value: 50051 } - endpoint: address: socket_address: { address: 192.168.1.2, port_value: 50051 } - endpoint: address: socket_address: { address: 192.168.1.3, port_value: 50051 } - name: shadow_backend connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN http2_protocol_options: {} load_assignment: cluster_name: shadow_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: 192.168.2.1, port_value: 50052 }
Здесь у нас немного поменялось, если сравнивать с HTTP: добавили ошибки, свойственные для gRPC-ответов ("cancelled", "internal", "deadline-exceeded"
), для политики обработки повторов. Также сделали зеркалирование 100% трафика на кластер shadow_backend
, чтобы проверить, как ведёт себя условно новая версия приложения. Можно было поставить split
, чтобы 80% уходило на grpc_backend
, а 20% — на shadow_backend, но я решил показать именно зеркалирование. Добавили два новых фильтра: grpc_web
и grpc_http1_bridge
. Фильтры в envoy обрабатываются по порядку — сверху вниз, за этим нужно обязательно следить.
gRPC-Web — фильтр предназначен для преобразования HTTP/1.1 запросов, совместимых с gRPC-Web, в стандартные HTTP/2 gRPC запросы. Если у вас есть клиент, использующий протокол gRPC-Web (например, веб-приложение в браузере), который не поддерживает полноценный HTTP/2, этот фильтр позволяет преобразовать его запросы так, чтобы они работали с сервером gRPC.
gRPC HTTP1 bridge — фильтр предназначен для преобразования стандартных HTTP/1.1 REST запросов в gRPC-запросы. Если у вас есть клиенты, использующие обычные HTTP/1.1 REST запросы (например, JSON), но сервер поддерживает только gRPC, этот фильтр позволяет использовать эти REST запросы для вызова gRPC серверов. Фича upgrade_protobuf_to_grpc остается везде в положении true, а заголовки application/x-protobuf
будут автоматически преобразованы в gRPC. В этом случае фильтр добавит к телу кадр gRPC, описанный выше, и обновит заголовок content-type до отправки запроса на сервер application/grpc
В случае, если клиент отправляет content-length
заголовок, он будет удален перед продолжением, поскольку значение может конфликтовать с размером, указанным в кадре gRPC.
Тело ответа, возвращаемое клиенту, не будет содержать кадр заголовка gRPC для запросов, обновленных таким образом, т.е. тело будет содержать только закодированный Protobuf.
Аутентификация и авторизация запросов
Итак, Envoy поддерживает JWT Authentication
и OAuth2
. Мы не будем разбирать весь конфигурационный файл, а сосредоточимся на отдельных фрагментах. Как строится конфигурация, думаю, уже стало ясно, а если возникнут вопросы, всегда можно обратиться к официальной документации. Я расскажу, что можно настроить, а что — нет.
Поддерживаемые jwt алгоритмы
ES256, ES384, ES512, HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, EdDSA
static_resources: listeners: - name: listener_0 address: socket_address: protocol: TCP address: 0.0.0.0 port_value: 10000 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: path: "/" route: cluster: service1 - match: path: "/api" route: cluster: service1 - match: path: "/health" route: cluster: service1 http_filters: - name: envoy.filters.http.jwt_authn typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication providers: provider1: issuer: "https://auth.example.com/provider1" remote_jwks: http_uri: uri: "https://auth.example.com/provider1/.well-known/jwks.json" cluster: auth_cluster timeout: 5s forward: true forward_payload_header: x-jwt-payload require_expiration: true cache_duration: seconds: 300 rules: - match: prefix: "/" requires: provider_name: provider1 - match: prefix: "/api" requires: provider_name: provider1 - match: prefix: "/health" - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Самое интересное нас ждет в фильтрах, правилах и маршрутах.
Первый фильтр — JwtAuthentication
. Здесь у нас указан issuer
(издатель токена), поле, которое требует обязательного указания времени жизни токена (require_expiration
), и удаленный JWKS, который проверяет наши токены на валидность. JWKS может быть локальным или удаленным. Также в конфигурации указан upstream
-кластер, куда будут уходить запросы.
Дополнительно в конфигурации задано, что после проверки токена его полезную нагрузку необходимо передать в заголовке x-jwt-payload
на upstream
. Однако это опционально, и данное поведение можно отключить. Тумблеров для настройки здесь много, в конце оставлю все ссылки.
По умолчанию JWT-токен ищется в заголовке Authorization: Bearer <token>
или в GET-параметре /path?access_token=<JWT>
. Но, конечно же, это Envoy, поэтому здесь можно настроить все как угодно: указать кастомный заголовок для поиска токена, задать аудитории, которые будут приниматься или отклоняться. Если запрос содержит два токена: один из заголовка и другой из GET-параметра, то оба должны быть валидными.
И самое главное — это маршруты: на каких требуется аутентификация, а куда можно пройти без нее. Например, для /health
аутентификация не требуется, а для остальных маршрутов потребуется предоставить JWT. На каждый маршрут можно указать провайдера, который будет за него ответственным, поскольку их может быть несколько. Для примера я описал только одного провайдера.
Теперь рассмотрим, как мы можем авторизировать запросы. Наш фильтр называется envoy.filters.http.oauth2
static_resources: listeners: - name: listener_0 address: socket_address: protocol: TCP address: 0.0.0.0 port_value: 10000 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager http_filters: - name: envoy.filters.http.oauth2 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 config: token_endpoint: cluster: oauth uri: "https://<keycloak-host>/auth/realms/<realm>/protocol/openid-connect/auth" timeout: 3s forward_bearer_token: true use_refresh_token: true authorization_endpoint: "https://<keycloak-host>/auth/realms/<realm>/protocol/openid-connect/auth" redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback" redirect_path_matcher: path: exact: /callback signout_path: path: exact: /logout credentials: client_id: "<client-id>" token_secret: name: token auth_scopes: - user - openid - email - name: envoy.filters.http.csrf typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy filter_enabled: default_value: true additional_origins: - exact: "https://<frontend-domain>" - name: envoy.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router codec_type: "AUTO" stat_prefix: ingress_http route_config: virtual_hosts: - name: service domains: ["*"] routes: - match: prefix: "/" route: cluster: service timeout: 5s clusters: - name: service connect_timeout: 5s type: STATIC lb_policy: ROUND_ROBIN load_assignment: cluster_name: service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 8080 - name: oauth connect_timeout: 5s type: LOGICAL_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: oauth endpoints: - lb_endpoints: - endpoint: address: socket_address: address: auth.example.com port_value: 443 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext sni: auth.example.com
Когда пользователь отправляет запрос к защищённому ресурсу через Envoy, процесс авторизации начинается с проверки токена в запросе. Если токен отсутствует или недействителен, Envoy перенаправляет пользователя на страницу авторизации Keycloak, URL которой указан в параметре authorization_endpoint
. Этот запрос включает необходимые параметры, такие как client_id
, указанный в секции credentials
, и запрашиваемые области (auth_scopes
), включая openid
, email
и user
, которые необходимы для выполнения OpenID Connect (OIDC)-аутентификации.
После успешной авторизации Keycloak перенаправляет пользователя на URI, указанный в redirect_uri
. Этот параметр динамически формируется на основе входящего заголовка x-forwarded-proto
и текущего имени хоста (:authority
), что делает систему гибкой для работы в средах с изменяющимися протоколами (HTTP/HTTPS) и доменами. Также указана точка входа для обработки ответа на авторизацию через параметр redirect_path_matcher
, который ожидает, что Keycloak вернёт пользователя на путь /callback
с кодом авторизации.
Когда Envoy получает этот код, он использует его для запроса токена у Keycloak через указанный token_endpoint
. Этот запрос происходит на фоне, где Envoy аутентифицируется перед сервером с помощью идентификатора клиента (client_id
) и секретного ключа (token_secret
). Эти данные обеспечивают безопасное взаимодействие между Envoy и Keycloak.
После получения токена Envoy либо сохраняет его в cookie, либо использует для создания нового запроса к защищённому ресурсу, добавляя токен в заголовок Authorization: Bearer
. Этот токен валидируется при каждом запросе. Для сценариев, где требуется выйти из системы, используется путь signout_path
, который привязан к URI /logout
, позволяя завершить пользовательскую сессию. Когда сервер проверяет клиента и возвращает токен авторизации обратно в фильтр OAuth, независимо от формата этого токена, если forward_bearer_token
установлен в значение true, фильтр отправит cookie с именем BearerToken
в upstream. Кроме того, Authorization
заголовок будет заполнен тем же значением.
use_refresh_token
предоставляет возможность обновлять токен доступа с помощью токена обновления. Если этот флаг отключен, то после истечения срока действия токена доступа пользователь перенаправляется на конечную точку авторизации для повторного входа. Новый токен доступа можно получить с помощью токена обновления без перенаправления пользователя на повторный вход. Для этого необходимо, чтобы токен обновления был предоставлен authorization_endpoint
при входе пользователя в систему. Если попытка получить токен доступа с помощью токена обновления не удалась, пользователь перенаправляется на конечную точку авторизации.
В самом конце фильтр CSRF ограничивает список доменов, откуда могут приходить запросы, обеспечивая дополнительную безопасность.
Из недостатков: для корректной работы фильтра служба должна функционировать по протоколу HTTPS, поскольку файлы cookie используют атрибут ;secure
. Без HTTPS authorization_endpoint
, скорее всего, отклонит входящий запрос, а файлы cookie доступа не будут кешироваться, что помешает обходу будущих повторных входов в систему
Ссылки для чтения: oauth2 и jwt.
Envoy — это высокопроизводительный прокси-сервер, созданный для обслуживания современных распределенных систем. Он обеспечивает широкий спектр возможностей, включая маршрутизацию запросов, балансировку нагрузки, управление аутентификацией, обработку JWT-токенов, защиту от атак CSRF, а также интеграцию с внешними системами авторизации, мониторинга, трассировки и еще ОЧЕНЬ много других фильтров.
Одной из ключевых особенностей Envoy является гибкость его настройки. Благодаря модульной архитектуре и множеству фильтров, вы можете адаптировать его под практически любые задачи: от простого маршрутизатора до сложного API-шлюза или элемента mesh-сети. Envoy активно используется в микросервисной архитектуре и служит основой для системы Istio в sidecar mode он же service mesh.
С его помощью можно добиться не только высокой отказоустойчивости, но и гибкого управления трафиком, подробного мониторинга, а также строгого соблюдения правил безопасности. Это делает Envoy мощным инструментом для построения надежной инфраструктуры в условиях современных требований.
ссылка на оригинал статьи https://habr.com/ru/articles/864528/
Добавить комментарий