AWS: ALB та Cloudflare – налаштування mTLS та AWS Security Rules
0 (0)

Автор |  15/03/2026
Click to rate this post!
[Total: 0 Average: 0]

Під час підготовки інфраструктури для міграції RTFM з серверу в DigitalOcean до AWS (див. AWS: сетап базової інфраструктури для WordPress) вирішив заодно спробувати AWS ALB – ALB mutual authentication (мені чомусь здавалось, що цю фічу запустили на останньому re:Invent, в кінці 2024, але вона є з кінця 2023 року – див. Mutual authentication for Application Load Balancer reliably verifies certificate-based client identities).

Ідея полягає в тому, щоб дозволити підключення до ALB тільки тим клієнтам, які пройдуть аутентифікацію, і в випадку Cloudflare+AWS ALB – Cloudflare має підписувати всі свої запити з TLS-сертифікатом (див. Cloudflare Authenticated Origin Pulls), а AWS ALB буде їх перевіряти – таким чином доступ до Load Balancer URL буде можливий тільки для Cloudflare.

Надихнула мене на цю ідею нещодавня історія з TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death” – коли TCP-конекти відкривались напряму на порт 443 на сервері в DigitalOcean, а не йшли через Cloudflare.

Правда, окрім mTLS я пізніше все одно вирішив додати ще і обмеження по IP – налаштував доступ до ALB тільки для адрес Cloudflare, які задані в AWS Security Rules, бо mTLS – це всеж про аутентифікацію, а Security Rules – це вже саме захист на рівні мережі.

Хоча спочатку думав робити чисто mTLS – бо і цікаво було подивитись, як це в ALB працює – і лінь було робити автоматизацію для оновлення AWS Security Rules.

Приклад тут робився на тестовому домені – тому заодно додамо його на Cloudflare, але обидва рішення вже працюють і для самого rtfm.co.ua.

Для чого, власне, обмежувати доступ до AWS Application Load Balancer:

  • на Cloudflare є багато Security rules, які блокують небажаний трафік, якщо ж запити йдуть напряму на ALB – то вони дойдуть до NGINX, і тоді частину правил треба робити і там (в мене так і було)
  • вартість при DDoS: поки блог жив на сервері в DigitalOcean, де просто був Public IP для дроплету, то запити до цього IP на вартість не впливали (тільки додатковий трафік), але в AWS та ALB кількість підключень впливає на вартість

P.S. І знов наче намагаєшся писати стисло – а вийшло багато тексту 🙁

Вартість AWS ALB та LCU

Вартість AWS Load Balancer включає в себе погодинну оплату за сам інстанс, стандартну плату Data Transfer Charge, за виділені Public IPs, та окремо – Load Balancer Capacity Units (LCU).

Для Network Load Balancer є власний юніт – Network Load Balancer Capacity Unit (NLCU), а для Gateway Load Balancer, відповідно, GLCU.

Див. Elastic Load Balancing pricing.

LCU не дуже очевидний юніт, тому кілька слів про нього.

1 LCU – це:

  • New connections: нові підключення до ALB – 25 підключень в секунду “з’їдають” 1 LCU
  • Active connection: кожні 3000 активних підключень (або 1500 при використанні mTLS) на хвилину
  • Processed bytes: 1 LCU покриває 1 GB трафіку на годину (або 0.4 GB для Lambda targets) – рахується сума ingress + egress
  • Rule evaluations: перевірка 1.000 правил в секунду – це один LCU

Під Rule evaluations маються на увазі Listener rules:

І якщо маємо багато правил по типу “IF path is /api/* THEN forward to target-group-api” – платимо за кожну перевірку кожного нового HTTP request.

При визначені того, з якого саме параметра вище рахувати LCU, AWS бере той, який перший досяг потрібного значення: тобто, як тільки маємо 26 запитів на секунду, але при цьому Processed bytes був лише 10 мегабайт – то нам порахуються 1 витрачений LCU за рахунок New connections.

Отже, у випадку DDoS (а у RTFM кілька раз траплялись, хоча і не сильні) – ми легко можемо “попасти на гроші”.

Є калькулятор Load Balancer Capacity Unit Reservation Calculator – можна прикинути, скільки LCU буде коштувати навантаження.

AWS Load Balancer mTLS use cases

Насправді якщо використовувати обмеження доступу до ALB через Security Group зі списком тільки дозволених IP – то mTLS не треба взагалі, бо ми вже “ріжемо” підключення на рівні мережі AWS, ще до того, як запит взагалі дійде до самого Load Balancer.

Що дійсно може бути корисним – це:

  • аутентифікація між сервісами:
    • наприклад, у нас є Internal ALB, за яким живуть різні сервіси моніторингу, такі як VictoriaLogs
    • в логах може бути чутлива інформація типу токенів або навіть паролів, тому до цього ендпоінта треба мати обмежений доступ
    • vmauth з парольною аутентифікацію – це добре, але mTLS додасть ще один рівень захисту (див. VictoriaMetrics: VMAuth – проксі, аутентифікація та авторизація)
  • аутентифікація мобільних клієнтів на API:
    • в клієнтах “зашиті” сертифікати (certificate pinning), які вони використовують для доступу до API
    • але тут треба мати на увазі можливі проблеми з ротацією сертифікатів – або у випадку компрометації, або просто коли закінчується строк дії
  • IoT: у нас (поки що) нема, але в цілому – дуже корисний варіант

Налаштування ALB mutual TLS з Cloudflare

Спершу подивимось варіант з обмеженням доступу з mTLS, бо штука цікава і корисна.

А потім додамо вже з AWS Security Rules та автоматизацією оновлень правил в ній.

Налаштування Cloudflare

Пост писався ще до міграції RTFM, робив на тестовому домені, тому швиденько пройдемось по процесу додавання і налаштування домену в Cloudflare.

Додавання нового домену до Cloudflare

Переходимо в Domains, клікаємо Onboard a domain:

Вказуємо ім’я, решту налаштувань можна з дефолтними значеннями:

При імпорті записів з AWS DNS пропустило кілька записів по Let’s Encrypt validation, але зараз це ОК:

Отримуємо список нових Name Servers:

Міняємо у реєстратора домену:

Чекаємо на оновлення – час залежить від адміністратора доменної зони:

Для kiev.ua (в мене вже є і setevoy.kyiv.ua) це зайняло з півгодини:

Переходимо в SSL/TLS, включаємо опцію Authenticated Origin Pulls.

Як тільки ми включимо цю опцію, Cloudflre при кожному новому запиті до origin – в нашому випадку AWS ALB – почне додавати свій клієнтський сертифікат.

Тут дуже хотілось трохи детальніше описати за SSL/TLS handshake та ключі-сертифікати – але подумав, що в рамках цього посту це буде зайвим, тим більш, що колись розбирався з деталями в пості What is: SSL/TLS в деталях.

Хоча, може, і зроблю новий пост на цю тему, тим більш зараз вже TLS 1.3, а там описаний актуальний на той момент TLS 1.2.

Або можна почитати вже згаданий на початку Introducing mTLS for Application Load Balancer – там непогано описано базові концепції по самому TLS.

Отже, Cloudflre тільки передає клієнтський сертифікат, а далі вже справа origin – що з ним робити, і наступним кроком ми як раз і налаштуємо його перевірку на самому ALB.

Поки просто вмикаємо опцію – грошей вона не просить (окрім того, що вартість 1 LCU по Active connections буде 1500 замість 3000), значного навантаження на ALB чи додаткового трафіку не зробить:

Для домену додаємо запис CNAME з value == ALB URL:

Перевіряємо, що зараз все працює – бо хоч Authenticated Origin Pulls на Cloudflare ми включили, але на ALB ще ніяких перевірок не виконується:

І запит на URL самого ALB все ще працює, тільки з помилкою SSL:

Налаштування Application Load Balancer

Тепер нам треба налаштувати перевірку клієнтського сертифіката від Cloudflare на нашому ALB.

Отримання Cloudflare Certificate Authority

ALB для перевірки буде використовувати публічний Certificate Authority сертифікат від Cloudflare, який нам треба додати в Trust Store самого Load Balancer, а потім підключити його до ALB Listener.

Майте на увазі, що використання Trust Store теж платне – “$0.0056 per hour per Trust Store Associated with Application Load Balancer when using Mutual TLS“, тобто ~ 4 USD на місяць.

Завантажуємо сертифікат:

$ curl -o cloudflare-origin-pull-ca.pem https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem

Сертифікат загальний для всього Cloudflare – хоча є можливість додати свій власний і проходити перевірку тільки з ним.

Але для випадку, коли нам просто треба закрити доступ всяким китайським ботам – сертифіката самого Cloudflare вистачить.

Перевіряємо скачаний файл:

$ file cloudflare-origin-pull-ca.pem
cloudflare-origin-pull-ca.pem: PEM certificate

Або можна глянути шо в ньому:

$ openssl x509 -in cloudflare-origin-pull-ca.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 6310029703491235425 (0x5791ba9556c22e61)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net
        Validity
            Not Before: Oct 10 18:45:00 2019 GMT
            Not After : Nov  1 17:00:00 2029 GMT
        Subject: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net
...

З важливого тут – Not After : Nov 1 17:00:00 2029:

  • можна в налаштуваннях ALB для mTLS включити опцію “Allow expired client certificates”
  • або просто в кінці 2029 року додати в Trust Store новий сертифікат та видалити старий

Створення Trust Store

Для додавання сертифіката до ALB нам треба буде створити Trust Store, але в Trust Store додати сертифікат можна тільки з AWS S3 – тому спочатку завантажуємо в якусь свою корзину:

Переходимо да EC2 > Load Balancing > Trust Stores, створюємо новий:

Вказуємо шлях до файлу сертифіката в S3:

Налаштування mTLS для ALB Listener

Редагуємо HTTPS Listener – бо mTLS, логічно, відбувається на ньому:

Включаємо опцію mTLS, і маємо дві опції:

  • Passthrough: передаємо сертифікат на таргети в Target Group – валідацію виконує бекенд
  • Verify with trust store: виконуємо перевірку на самого ALB з сертифікатом із Trust Store – наш випадок

Переключаємо на “Verify with trust store”:

Тепер пробуємо пряме підключення до ALB URL:

$ curl -kI https://rtfm-alb-1984146384.eu-west-1.elb.amazonaws.com
curl: (56) Recv failure: Connection reset by peer

І в браузері:

Але через Cloudflare все працює:

$ curl -I https://test-tls.setevoy.kiev.ua
HTTP/2 200

 

Ну і сам rtfm.co.ua вже теж на цьому ALB і має включений mTLS.

Захист з AWS Security Group та Cloudflare IP ranges

Але тільки mTLS для повноцінного захисту ALB все ж недостатньо:

  1. при DDoS ми все-одно будемо витрачати LCU на підключення
  2. запити, які пройдуть не через Cloudflare треба фільтрувати на бекенді – в моєму випадку NGINX

Тому самий надійний спосіб – це обмежити трафік на рівні AWS Security Group, яка підключена до Load Balancer: тоді ніяких LCU не буде витрачатись, бо пакети до ALB не дійдуть взагалі.

Є готові Terraform-модулі, наприклад – cloudflare-security-group, але він працює тільки з одною Security Group, є більш просунутий варіант – cloudflare-sg-updater, який шукає SG по тегам.

Втім, особисто я не дуже люблю AWS Lambda і, принаймні поки що, для RTFM не використовую Terraform – то зробив просто з shell-скриптом, який використовує AWS CLI.

Як раз до цього було багато практики, аби згадати як писати скрипти – див. FreeBSD: Home NAS, part 15: автоматизація бекапів – скрипти, rsync, rclone.

AWS CLI на Amazon Linux вже йде в комплекті, треба тільки або налаштувати AWS Profile – або підключити EC2 Instance Profile з IAM-політикою, яка дасть дозвіл на внесення змін в Security Group.

Єдиний нюанс, який треба мати на увазі, якщо пишете скрипт самі: IP іноді змінюються, хоч і рідко, і їх треба видаляти з Security Group. Але при цьому просто видалити всі старі записи, а потім додати нові – не варіант, бо в цей момент ALB втратить підключення до Cloudflare.

Тому в скрипті виконується порівняння старих і нових адрес, і видаляються тільки ті, яких нема в останньому апдейті від Cloudflare.

Створення IAM Role для EC2 Instance profile

Рано чи пізно все одно треба буде додавати IAM Role для інстансу, тому замість використання AWS CLI профайл з ключами краще відразу робити доступ до редагування Security Group через EC2 Instance profile.

Для ролі потрібно дати права на виконання ec2:DescribeSecurityGroups, ec2:AuthorizeSecurityGroupIngress та ec2:RevokeSecurityGroupIngress – створимо окрему IAM Policy.

Створюємо нову Security Group для ALB – правила Inbound залишаємо пустими, в Outbound – залишаємо дефолтний “All to All”:

Створюємо нову IAM Policy, обмежуємо доступ тільки цією Security Group.

Для ec2:DescribeSecurityGroups задати обмеження не можна, бо правило глобальне для всіх SG, тому описуємо двома окремими Statement – один з  Resource": "*", другий вже з конкретною SG.

Робимо з обмеженням по Resource: вказуємо обмеження доступу конкретною групою, бо скрипт буде виконувати destructive дії – видалення правил, і для таких дій на випадок “щось пішло не так” завжди краще обмежувати blust radius:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeSecurityGroups",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": "arn:aws:ec2:eu-west-1:ACCOUNT_ID:security-group/sg-029ffc5f56be700ea"
        }
    ]
}

 

Задаємо ім’я політики, зберігаємо:

Створюємо IAM Role, в Use Case вибираємо EC2:

Підключаємо політику, яку створили вище:

Задаємо ім’я нової ролі, зберігаємо (хоча ім’я тут вибрав не найкраще – бо 100% якісь ще доступу в цю роль додаватись будуть):

В Security > Modify IAM Role підключаємо роль до EC2 – можна робити без зупинки інстансу:

Підключаємо нашу нову роль:

Підключаємось по SSH до інстансу, перевіряємо, що в нього тепер є доступ до, наприклад, ec2:DescribeSecurityGroups:

[ec2-user@ip-10-0-3-146 ~]$ aws ec2 describe-security-groups --group-ids sg-029ffc5f56be700ea
{
    "SecurityGroups": [
        {
            "GroupId": "sg-029ffc5f56be700ea",
            "IpPermissionsEgress": [
                {
                    "IpProtocol": "-1",
                    "UserIdGroupPairs": [],
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": []
                }
            ],
...

Shell script для оновлення ALB Security Group

Додаємо скрипт, який буде:

  1. отримувати список актуальних Cloudflare IP CIDR (і IPv4, і IPv6)
  2. з AWS CLI отримувати список поточних правил в Security Group
  3. порівнювати нові адреси і значення з Security Group і додавати тільки ті адреси, яких зараз нема
  4. і аналогічна перевірка для видалення адрес з Security Group – якщо IP нема в актуальному списку від Cloudflare, то видаляємо

Security Group одна, тому її просто задаємо на початку в змінну $SG_ID:

#!/bin/bash

# Update ALB security group with current Cloudflare IP ranges
SG_ID="sg-029ffc5f56be700ea"
PORT=443

# Fetch current Cloudflare IP ranges
CF_IPS=$(curl -s https://www.cloudflare.com/ips-v4; echo; curl -s https://www.cloudflare.com/ips-v6)

# Get current IPs from security group
CURRENT_IPS=$(aws ec2 describe-security-groups --group-ids $SG_ID --query "SecurityGroups[0].IpPermissions[?FromPort==\`$PORT\`].[IpRanges[].CidrIp, Ipv6Ranges[].CidrIpv6]" --output text)

# Add missing IPs
for IP in $CF_IPS; do
    if ! echo "$CURRENT_IPS" | grep -q "$IP"; then
        echo "Adding $IP"
        if echo "$IP" | grep -q ":"; then
            aws ec2 authorize-security-group-ingress --group-id $SG_ID --ip-permissions "[{\"IpProtocol\":\"tcp\",\"FromPort\":$PORT,\"ToPort\":$PORT,\"Ipv6Ranges\":[{\"CidrIpv6\":\"$IP\"}]}]"
        else
            aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port $PORT --cidr $IP
        fi
    fi
done

# Remove IPs no longer in Cloudflare list
for IP in $CURRENT_IPS; do
    if ! echo "$CF_IPS" | grep -q "$IP"; then
        echo "Removing $IP"
        if echo "$IP" | grep -q ":"; then
            aws ec2 revoke-security-group-ingress --group-id $SG_ID --ip-permissions "[{\"IpProtocol\":\"tcp\",\"FromPort\":$PORT,\"ToPort\":$PORT,\"Ipv6Ranges\":[{\"CidrIpv6\":\"$IP\"}]}]"
        else
            aws ec2 revoke-security-group-ingress --group-id $SG_ID --protocol tcp --port $PORT --cidr $IP
        fi
    fi
done

echo "Done"

Задаємо права на запуск скрипта:

[root@ip-10-0-3-146 ~]# chmod +x /opt/update-alb-sg/update-alb-sg-from-cloudflare.sh

Запускаємо вручну для перевірки:

Перевіряємо в Security Group:

Заміна Security Group на ALB

Редагуємо список Security Groups для Load Balancer:

Додаємо нову, видаляємо стару:

Перевіряємо, що все працює:

$ curl -I https://rtfm.co.ua/
HTTP/2 200 

Але з дому напряму до ALB тепер доступу нема:

$ curl --connect-timeout 5 -kI rtfm-alb-1984146384.eu-west-1.elb.amazonaws.com
curl: (28) Connection timed out after 5002 milliseconds

Додаємо в cron.

Запуск з crontab

В Amazon Linux версії AL2023 стандартний cron випиляли – встановлюємо його:

# yum install -y cronie
# systemctl enable crond
# systemctl start crond

Запускаємо, наприклад, раз на добу в 3 ночі:

0 3 * * * /opt/update-alb-sg/update-alb-sg-from-cloudflare.sh >> /var/log/update-cf-sg.log 2>&1

Можна ще підписатись на Cloudflare changelog/RSS, аби отримувати нотифікації коли щось міняється в IP діапазонах.

Але зміна IP ranges прям дуже рідко буває – останній раз у 2023 році, див IP Ranges:

Власне, на цьому все.

Тепер можна спокійно спати: “frontend” у нас Cloudflare, де є Security rules, WAF і захист від DoS/DDoS, а на “бекенді” ми захищені на рівні мережі, L3/L4 – і Load Balancer та EC2 живуть спокійно.

Див. мій пост по TCP/IP: моделі OSI та TCP/IP, TCP-пакети, Linux sockets і порти та документацію самого AWS – Network security.

P.S. Документація AWS – одна з тих речей, за що його дійсно люблю. Колись довелось багато працювати з Azure – і там це дійсно велика проблема. До речі, див. Azure: почему никогда.

Loading