2025, Dec 31 00:08

Кастомный 403 для AWS WAF и CloudFront через Lambda@Edge: почему Viewer Response не срабатывает и рабочий вариант на Origin Request

Разбираем, почему при блокировке AWS WAF CloudFront не вызывает Viewer Response, и как отдать кастомный 403 HTML через Lambda@Edge на Origin Request — решение

Когда вы комбинируете AWS WAF с CloudFront и пытаетесь настроить видимый пользователю ответ через Lambda@Edge, легко захотеть подключиться к событию Viewer Response и переписать тело при 403. На практике это не сработает так, как вы рассчитываете. Если вы видите стандартный 403 от WAF, а ваша функция Lambda@Edge не запускается, вы упираетесь в то, как CloudFront вызывает события для ответов с ошибками.

Постановка задачи

Цель простая: когда запрос блокируется, показать небольшую HTML‑страницу с IP клиента. Функция привязана к Viewer Response и проверяет статус ответа, чтобы отдать собственный HTML.

def edge_entry(evt, ctx):
    ip_addr = evt['Records'][0]['cf']['request']['clientIp']
    resp_status = evt['Records'][0]['cf']['response']['status']
    resp_headers = evt['Records'][0]['cf']['response']['headers']
    passthrough = evt['Records'][0]['cf']['response']
    html_doc = f"""
    <!DOCTYPE html>
    <html>
    <head><title>Access Denied</title></head>
    <body>
        <h1>Access Denied</h1>
        <p>Your IP address is: {ip_addr}</p>
    </body>
    </html>
    """
    if resp_status == '403':
        crafted = {
            'status': '403',
            'statusDescription': 'Forbidden',
            'headers': resp_headers,
            'body': html_doc,
            'bodyEncoding': 'text',
        }
        return crafted
    else:
        return passthrough

Почему это не работает

Событие Viewer Response не срабатывает на ответах с кодами > 4xx. Поэтому ваша функция не запускается, когда WAF блокирует запрос. Более того, если вы не настроили собственный ответ, платформа вернет стандартную страницу блокировки. Как сказано:

WAF вернет клиенту стандартный ответ о блокировке, если ни в самом WAF, ни в защищаемом ресурсе (в данном случае CloudFront) не настроен пользовательский ответ.

Смена типа при проверке статуса (строка против числа) не поможет; это не ошибка сравнения, дело в том, что событие вообще не генерируется для ответов о блокировке.

Что делать вместо этого

Если вам нужна динамическая страница для заблокированных запросов, стройте решение вокруг обработки ошибок CloudFront и события Origin Request. Настройте в CloudFront пользовательский ответ для HTTP 403, создайте behavior для этого пути с отключенным кэшированием и привяжите вашу функцию Lambda@Edge к событию Origin Request для данного behavior. В таком потоке функция не получит ответ от origin для анализа, поэтому она должна всегда сама возвращать динамический 403.

Рабочий подход: всегда отдавайте 403 на Origin Request

Ниже функция, которая собирает HTML и возвращает 403 каждый раз, когда вызывается в рамках этого behavior. IP по‑прежнему читается из запроса, так что вы можете персонализировать страницу блокировки, не завися от ответа origin.

def edge_origin_request(ev, cx):
    requester_ip = ev['Records'][0]['cf']['request']['clientIp']
    content = f"""
    <!DOCTYPE html>
    <html>
    <head><title>Access Denied</title></head>
    <body>
        <h1>Access Denied</h1>
        <p>Your IP address is: {requester_ip}</p>
    </body>
    </html>
    """
    return {
        'status': '403',
        'statusDescription': 'Forbidden',
        'headers': {},
        'body': content,
        'bodyEncoding': 'text',
    }

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

Понимание того, какие события CloudFront генерирует в ошибочных сценариях, экономит время и избавляет от слепой отладки. Viewer Response — не то место, где стоит менять тела ответов для заблокированных запросов, когда такие ответы до этой стадии вообще не доходят. Используйте пользовательский обработчик ошибок CloudFront и перенесите логику в Origin Request — так вы сохраняете контроль, а AWS WAF продолжает применять блокировку.

Выводы

Если WAF возвращает 403 до того, как сработает Viewer Response, функция не запустится, даже будучи правильно привязанной. Настройте в CloudFront пользовательский ответ 403, направьте его через behavior с отключенным кэшированием и обработайте рендеринг в Lambda@Edge на Origin Request, которая всегда отдает страницу 403. Это сохраняет применение политики на периферии и дает предсказуемый, динамический результат для заблокированного трафика.