AWS: обзор и настройка Web Application Firewall и его мониторинга

Автор: | 16/07/2021
 

AWS WAF (Web Application Firewall) – сервис Amazon, выполняющий мониторинг HTTP(S) трафика, проверяя запросы, которые приходят к защищаемому приложению. Может быть подключен к AWS Application LoadBalancer, AWS CloudFront дистрибьюции, Amazon API Gateway и AWS AppSync GraphQL API.

В случае обнаружения запросов, попадающих под список правил блокировки и/или IP-адреса из списка запрещённых или имеющих плохую репутацию – блокирует такой запрос, возвращая клиенту ошибку 403.

AWS WAF состоит из четырёх основных компонентов:

  • Web ACL: списки контроля доступа, который содержат правила для проверки входящих запросов
  • IP Sets: списки IP адресов и/или сетей, которые можно использовать в ACL
  • Rules: собственно, правила, которые описывают как и какие запросы обрабатывать. Такими правилами может быть блокировка списков IP, проверка заголовков, проверка тела запроса и так далее
  • Rules groups: правила могут быть собраны в группы, кроме того, AWS предоставляет уже готовые группы, которые можно подключить к ACL – AWS Managed Rules и списки в AWS Marketplace

У AWS WAF есть “вместительность” для его ACL: к каждому ACL и Rules Group можно добавить только определённое количество правил и/или групп, и каждое правило занимает определённое количество WCU, а один ACL может вмещать в себе до 1500 WCU. О лимитах поговорим в Лимиты AWS WAF. См. AWS WAF Web ACL capacity units (WCU).

Из самых неудобных ограничений – это возможность подключения к одному ALB или CloudFront только одной ACL, что существенно сужает возможности для формирования этих самых ACL.

Отдельно интересновался влияет ли подключение WAF ACL к ALB/CloudFront на скорость ответа приложения клиенту, но ребята из AWS ответили, что нет, не влияет, и скорость не зависит от количества подключенных ACL и правил в них.

В этом посте запустим тестовое приложение, потом разберёмся с основными концепциями AWF, создадим ACL, подключим правила, протестируем работу, настроим мониторинг и алертинг с Prometheus.

Тестовое приложение

Используем простой Kubernetes Deployment, который создаст Kubernetes Pod с Nginx, Service и Ingress. Этот Ingress через AWS Load Balancer Controller создаст нам AWS Application LoadBalancer.

---
apiVersion: v1
kind: Namespace
metadata:
  name: test-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
  namespace: test-namespace
  labels:
    app: test
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
        version: v1
    spec:
      containers:
      - name: web
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        readinessProbe:
          httpGet:
            path: /
            port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc
  namespace: test-namespace
spec:
  type: NodePort
  selector:
    app: test
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test-namespace
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
    external-dns.alpha.kubernetes.io/hostname: "testapp.dev.example.com"
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: test-svc
          servicePort: 80

Проверяем Ingress и ALB:

[simterm]

$ kubectl -n test-namespace get ingress
NAME           CLASS    HOSTS   ADDRESS                                    PORTS   AGE
test-ingress   <none>   *       aadca942-***.us-east-2.elb.amazonaws.com   80      46s

[/simterm]

Проверяем ответ сервера:

[simterm]

$ curl -I testapp.dev.example.com
HTTP/1.1 200 OK

[/simterm]

Тут готово, переходим к AWS WAF.

Настройка AWS WAF

Создание Web ACL

Переходим в AWS Console > WAF, кликаем Create web ACL:

В данном случае, будем подключать AWS ALB, поэтому сначала (!) выбираем регион, а потом задаём имя ACL, оно же будет использовано для CloudWatch метрик, их рассмотрим в Метрики AWS CloudWatch и Prometheus:

Далее, выбираем защищаемый ресурс – созданный выше AWS ALB:

Теперь наш ALB при получении нового запроса будет сначала отправлять его в WAF для проверки правил, и потом либо отказывать клиенту в обработке запроса, либо передавать запрос приложению.

Правила добавим позже, пока оставляем действие по-умолчанию – Allow:

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

  • для Allow: добавить кастомный заголовок, который будет добавлен ко всем запросам, которые прошли проверки и были отправлены дальше к приложению
  • для Block: можно настроить код ответа, который будем возвращать всем заблокированным клиентам, указать кастомный заголовок, и тело ответа:

Снова-таки – тут пока оставляем всё по-умолчанию, можно будет настроить позже.

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

Пока пропускаем.

CloudWatch будем настраивать позже, тут пока тоже пропускаем:

Запускаем curl на тестовый URL, пусть нагенерит нам графиков:

[simterm]

$ watch -n 1 curl -I testapp.dev.example.com

[/simterm]

IP set

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

Например, можно задать список офисных IP, по которым потом разрешать доступ. См. Creating an IP set.

Учитывайте регионы AWS: для CloudFront требуется выбрать CloudFront (Global), для остальных ресурсов – регион, в котором ресурсы был создан.

Получаем текущий IP:

[simterm]

$ curl ifconfig.me
194.***.***.29

[/simterm]

Создаём IP Set, используем маску  /29, что бы включить все наши офисные IP:

И переходим к собственно правилам.

Создание ACL Rules

Переключаемся на созданный ACL, кликаем Add Rules, сейчас выбираем Add my own rules:

Добавим самое простое правило – блокировать все запросы из офиса.

Выбираем Rule type == IP Set, так как будем использовать добавленный выше IP Set, указываем имя, выбираем наш IP Set, используем Source IP address, и дефолтное действие – Block:

При указании действия Count – WAF просто будет считать количество запросов, попавших под это правило, не выполняя никаких действий. Может быть полезно при тестировании новых правил или ACL.

Указываем приоритет, но тут одно правило, так что приоритет 1:

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

[simterm]

$ curl -I testapp.dev.example.com
HTTP/1.1 403 Forbidden

[/simterm]

Теперь попробуем что-то поинтереснее: разрешить доступ только из офиса, и закрыть из мира.

Создать IP Set с CIDR 0.0.0.0/0 нельзя, поэтому делаем иначе – переключим Default Action нашей ACL в Block, а офисный IP добавим в разрешающее правило.

Редактируем Default web ACL action:

Проверим с какого-то “левого” IP, например с сервера, на котором хостится rtfm.co.ua. Через 15-20 секунд правило начинает работать:

Возвращаемся к созданном ранее ACL test-office-rule, и меняем Block на Allow из офиса:

Проверяем с офисной машины:

Ограничение по URI и приоритеты правил

Окей, а что, если мы хотим заблокировать доступ из мира только к определённому URI нашего ресурса, например /status, но разрешить из офиса?

Тут нам надо:

  1. вернуть дефолтное действие для ACL в Allow
  2. добавить правило на Allow /status из офиса
  3. добавить правило на Block /status из мира

Возвращаемся к действию по-умолчанию для ACL, снова указываем его в Allow – разрешаем весь трафик к приложению отовсюду:

Теперь надо создать два правила – разрешающее из офиса, и запрещающее из мира. Начнём с запрещающего.

На этот раз используем Rule builder, назовём правило block-status-uri-world, в Statement выбираем URI Path, указываем /status, в Action задаём действие Block:

Проверяем доступы.

Из офиса к /:

[simterm]

15:33:29 [setevoy@setevoy-arch-work ~]  $ curl -I testapp.dev.example.com
HTTP/1.1 200 OK

[/simterm]

И с “левого” IP к /:

[simterm]

root@rtfm-do-production-d10:~# curl -I testapp.dev.example.com
HTTP/1.1 200 OK

[/simterm]

Из офиса к /status:

[simterm]

16:08:42 [setevoy@setevoy-arch-work ~]  $ curl -I testapp.dev.example.com/status
HTTP/1.1 403 Forbidden

[/simterm]

И из мира:

[simterm]

root@rtfm-do-production-d10:~# curl -I testapp.dev.example.com/status
HTTP/1.1 403 Forbidden

[/simterm]

Отлично: доступ к корню сайта есть отовсюду, доступа к /status нет ниоткуда. Теперь разрешим доступ к /status из офиса.

Возвращаемся к Rules, и добавляем новое правило, назовём его allow-status-uri-office:

Далее, строим правило:

  1. если URI == /status
  2. AND если IP == test-bttrm-office-ip-set
  3. то Allow

Кстати, обратите внимание на значение Capacity – тут как раз указано, сколько WCU “скушает” наше правило из 1500, доступных в этой ACL.

Но по текущим приоритетам первым сработает правило block-status-uri-world, и доступ к /status мы не получим даже из офиса:

Поэтому, перемещаем правило allow-status-uri-office в начало списка правил:

Теперь приоритеты правил выглядят так:

Проверим не из офиса:

[simterm]

root@rtfm-do-production-d10:~# curl -I testapp.dev.example.com/status
HTTP/1.1 403 Forbidden

[/simterm]

И с офисной машинки:

[simterm]

16:18:00 [setevoy@setevoy-arch-work ~]  $ curl -I testapp.dev.example.com/status
HTTP/1.1 200 OK

[/simterm]

SQL injection block

Одна из наиболее неприятных атак – SQL-инъекции, которые определённо стоит банить.

В AWS WAF Managed rules достаточно много правил от самого AWS и партнёров, кроме того – можно настроить такую блокировку в своём правиле.

Создаём правило sqli-test:

Описываем правила:

  • проверяем Query
  • в Macth type выбираем Contains SQL Injection attack

В Actions оставляем Block, а в Custom responce укажем код 405 – Method not allowed, дабы запутать атакующего (хотя веселее было бы вернуть 200 🙂 ):

Сохраняем, проверяем запросом products?category=Gifts'--, взятым отсюда (как-то попозже копнём в SQL и XSS, да и другие атаки рассмотрим):

[simterm]

$ curl -I "http://testapp.dev.example.com/products?category=Gifts'--"
HTTP/1.1 405 Not Allowed

[/simterm]

Ура – мы заблокировали SQL-инъекцию в наше приложение!

Managed Rule groups

Посмотрим, что предлагает AWS и его партнёры в уже готовых наборах правил – а предлагается тут реально много.

Выбираем Add managed rule groups:

В AWS managed rule groups – наборы правил от AWS, все остальные – платные правила от партнёров, которые можно купить в AWS Marketplace.

Сами правила описаны в документации – AWS Managed Rules rule groups list.

Например, правило Known bad inputs проверяет значение заголовка Host в запросе, и если оно равно localhost – то такой запрос будет заблокирован, так как валидный запрос из мира такого значения содржать не должен.

Включаем его в наше правило:

При желании, можно настроить отдельные компоненты этой группы, например – изменить дефолтное действие (вместо Block задать Count), или настроить фильтр запросов, которые будут этим правилом обрабатываться:

Снова-таки – обращаем внимание на Capacity, тут эта группа отнимает целых 200 WCU из 1500 доступных для ACL.

Сохраняем, проверяем приоритеты:

Проверяем сейчас:

[simterm]

$ curl -I testapp.dev.example.com
HTTP/1.1 200 OK

[/simterm]

И добавим заголовок Host к curl со значением localhost:

[simterm]

$ curl -H "Host: localhost" -I testapp.dev.example.com
HTTP/1.1 403 Forbidden

[/simterm]

Вуаля! Запрос был заблокирован.

Rule groups и удаление ACL

Ещё один нюанс касается удаления правил: если правило создаётся напрямую в ACL, через Add rules > Add my own rules, то при удалении этого ACL будет удалено и правило в нём.

Что бы хранить ваши ручные правила постоянно – создаём их через Rule groups:

Затем в группе создаём правила, а потом эту группу подключаем к ACL.

Лимиты AWS WAF

См. все ограничения тут – AWS WAF quotas.

Из наиболее важных:

  • одна ACL на один ALB – крайне неприятное ограничение, которое стоит иметь ввиду при планированни ACL, хотя может быть увеличено через запрос в тех. поддержку, при этом стоит учитывать, что WCU влияют на стоимость:
    • If you have a web ACL that uses between 0 to 1,500 WCU, then all your requests will be charged at $0.60 per million requests (regular rate)
    • If you have a web ACL that uses between 1,501 to 2,000 WCU, then all your requests will be charged at $0.80 per million requests
    • If you have a web ACL that uses between 2,001 to 2,500 WCU, then all your requests will be charged at $1.00 per million requests.
  • максимум IP sets на аккаунт: 100
  • макимальное значения запросов в секунду (для ALB): 25.000
  • максимальный размер тела запроса, которое может быть проверено: 8кб
  • максимум API-вызовов к AWS на AssociateWebACL и DisassociateWebACL: 2 в секунду

Мониторинг AWS WAF

Хорошо, мы научились блокировать запросы. Составили ACL, прописали пачку правил – всё работает.

Но хочется мониторить эти события, и даже слать алерты в случае, если нас “ломают”.

Метрики AWS CloudWatch и Prometheus

Первое, куда мы пойдём посмотреть – это метрики CloudWatch:

Выбираем тут наш ACL или конкретное правило:

Вот наши тестовые заблокированные запросы.

Далее, мы можем собрать их в Prometheus с помощью cloudwatch-exporter:

region: us-east-2

metrics:

 - aws_namespace: AWS/WAFV2
   aws_metric_name: BlockedRequests
   aws_dimensions: [Region,Rule,WebACL]

Повторяем наш тестовый запрос, можно запустить его в цикле:

[simterm]

$ while true; do curl -I "http://testapp.dev.example.com/products?category=Gifts'--"; sleep 1; done

[/simterm]

Проверяем графики в CloudWatch:

Заблокированные запросы пошли, хорошо.

И добавим тестовый алерт – проверяем среднее значение роста в секунду для метрики aws_wafv2_blocked_requests_sum, используя диапазон в последние 5 минут:

- alert: "WAFBlockedRequestsAlert"
  expr: rate(aws_wafv2_blocked_requests_sum{rule!="ALL"}[5m]) > 0
  for: 1s
  labels:
    severity: warning
  annotations:
    summary: "AWS WAF blocked requests detected"
    description: "ACL name: `{{ $labels.web_acl }}`\nRule name: `{{ $labels.rule }}"
    tags: test, aws, security, databases

Если значение будет выше 0 – значит, WAF кого-то блокирует, можно об этом сообщить:

 

AWS WAF логи

См. Managing logging for a web ACL.

Для логгирования запросов нам потребуется AWS S3 корзина и AWS Kinesis Data Firehose delivery stream.

См. более полный пост по настройке логов – AWS: WAF WebACL логи и Logz.io.

AWS Kinesis Data Firehose delivery stream

Переходим в Kinesis, создаём новый Data Firehouse stream:

Имя стрима должно начинаться с aws-waf-logs-, выбираем Direct PUT or other sources, кликаем Next:

На следующей странице пропускаем трансформации, кликаем Next, далее для destination выбираем S3, выбираем или создаём новую корзину:

Можно указать префикс – он будет добавлен к имени каталога по году в корзине:

Дальше оставляем по-умолчанию (хотя где-то встречал, что надо подтюнивать, посмотрим по ходу работы):

Проверяем настройки, подтверждаем создание стрима:

WAF ACL logging

Возвращаемся к WAF ACL, вкладка Logging and metrics, кликаем Enable logging:

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

Ждём минут 5-10, проверяем логи в корзине:

Содержимое лога:

[simterm]

$ tail -5 aws-waf-logs-test-stream-1-2021-07-16-08-19-20-d9c8f13e-2ffb-41e5-84d4-7e771d60f8e6 
{"timestamp":1626423836381,"formatVersion":1,"webaclId":"arn:aws:wafv2:us-east-2:534***385:regional/webacl/test-acl/36d796cd-4767-45b3-9f03-711f6ac4ca08","terminatingRuleId":"test-sqli","terminatingRuleType":"REGULAR","action":"BLOCK","terminatingRuleMatchDetails":[{"conditionType":"SQL_INJECTION","location":"QUERY_STRING","matchedData":["category=Gifts","--"]}],"httpSourceName":"ALB","httpSourceId":"534***385-app/k8s-testname-testingr-ce71203b0d/ca9edcc886933ca9","ruleGroupList":[{"ruleGroupId":"AWS#AWSManagedRulesKnownBadInputsRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null}],"rateBasedRuleList":[],"nonTerminatingMatchingRules":[],"requestHeadersInserted":null,"responseCodeSent":405,"httpRequest":{"clientIp":"194.***.***.29","country":"UA","headers":[{"name":"Host","value":"testapp.dev.example.com"},{"name":"User-Agent","value":"curl/7.77.0"},{"name":"Accept","value":"*/*"}],"uri":"/products","args":"category=Gifts'--","httpVersion":"HTTP/1.1","httpMethod":"HEAD","requestId":"1-60f1421c-1ab92f9f5bc7be7f39a70c08"}}

[/simterm]

Далее, можно их собирать во что-то типа Logz.io, см. Configure Logz.io to fetch logs from an S3 bucket.

AWS ALB, Kubernetes Ingress и AWS WAF

Окей, теперь у нас есть ACL:

И этот ACL надо подключить к AWS ALB, который создавался из Kubernetes Ingress с помощью AWS Load Balancer Controller, для которого существует аннотация alb.ingress.kubernetes.io/wafv2-acl-arn.

Включаем отображение ARN:

Обновляем наш Ingress:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test-namespace
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
    alb.ingress.kubernetes.io/wafv2-acl-arn: "arn:aws:wafv2:us-east-2:534***385:regional/webacl/test-acl/36d796cd-4767-45b3-9f03-711f6ac4ca08"
...

Применяем:

[simterm]

$ kubectl apply -f test-deployment.yaml

[/simterm]

Проверяем ACL – Associated AWS resources:

Готово.

Ссылки по теме