使用NGINX条件日志对请求进行采样

NGINX | 2019年4月24日

NGINX可以详细记录其处理的每个事务。此类日志称为“访问日志”,您可以使用可自定义的日志文件格式,精细调整不同服务或位置所记录的详细信息。

默认情况下,NGINX会记录其处理的每笔事务。出于合规或安全目的,这可能是必要的,但对于繁忙的网站,生成的数据量可能会大到难以处理。本文将介绍如何根据各种条件有选择地记录事务,以及如何运用这些知识,以快速、轻量的方式对请求数据点进行采样。

除非另有说明,本文同时适用于NGINX Open Source和NGINX Plus。为便于阅读,下文将统一简称为“NGINX”。

背景:NGINX访问日志配置概览

NGINX访问日志使用log_format指令定义。可以定义多种不同的命名日志格式,例如,名为main的完整日志格式,以及名为notes的简化日志格式,用于记录请求的3个数据点:

log_format main  '$remote_addr - $remote_user [$time_local] "$request" '
                 '$status $body_bytes_sent "$http_referer" '
                 '"$http_user_agent" "$http_x_forwarded_for"';

log_format notes '$remote_addr "$request" $status';

日志格式可以引用NGINX变量以及在记录日志时计算的其他值。

然后,使用access_log指令让NGINX在事务完成后记录日志。该指令用于指定日志文件的位置和要使用的日志格式:

access_log  /var/log/nginx/access.log main;

默认情况下,NGINX使用以下配置记录所有事务:

access_log logs/access.log combined;

如果自行定义 access_log,它将覆盖(替换)默认访问日志。

条件日志记录

有时,您可能只想记录特定请求。为此,可按如下方式使用条件日志记录:

server {
    listen 80;

    set $logme 0;
    if ( $uri ~ ^/secure ) {
        set $logme 1;
    }

    # Auditors require an additional log for requests to /secure
    access_log  /var/log/nginx/secure.log notes if=$logme;

    # If we have a global access log, we need to re-declare it here
    access_log  /var/log/nginx/access.log main;

    location / {
        # ...
    }
}

访问日志不会被继承

访问日志设置不会叠加或继承;某一上下文中的access_log指令会覆盖(替换)父级上下文中声明的访问日志。

例如,如果要记录有关流向URI /secure 的流量的更多信息,可以在 location /secure {...} 块中定义访问日志。此访问日志会替换配置中其他位置定义的常规访问日志。

The example in the 上一节中的示例解决了此问题。该示例在同一上下文中使用两个访问日志,并通过条件日志记录将对/secure的请求写入专用日志文件。

访问日志面临的挑战

假设您希望获取有关网站流量的一些统计信息:

  • 用户通常分布在哪些地区?
  • 我的用户使用哪些SSL/TLS密码套件和协议?
  • 各类Web浏览器的占比分别是多少?

常规访问日志通常不适合记录此类信息。您可能不希望因添加研究所需的额外字段而使访问日志变得冗杂;对于繁忙的网站,记录所有事务所产生的开销也会过高。

在这种情况下,可以将有限的一组字段记录到专用日志中。为降低系统负载,还可以考虑对部分请求进行采样。

采样技术

对1%的请求进行采样

以下配置使用$request_id变量作为每个请求的唯一标识符。该配置使用split_clients块对数据进行采样,仅记录1%的请求:

split_clients $request_id $logme {
    1%     1;
    *      0;
}

server {
    listen 80;

    access_log  /var/log/nginx/secure.log notes if=$logme;

    # ...
}

从1%的唯一用户中采样

假设我们希望从每位用户(或1%的用户)中抽取一个数据点,例如User-Agent标头。不能直接从所有请求中抽样,因为发出大量请求的用户会在数据中占比过高。

我们使用map块检测是否存在会话Cookie,以判断请求来自新用户还是之前访问过的用户。然后,我们仅对来自新用户的请求进行采样:

map $cookie_SESSION $logme {
    "" $perhaps;  # If the cookie is missing, we log if $perhaps
    default   0;
}

split_clients $request_id $perhaps {
    1%        1;  # $perhaps is true 1% of the time
    *         0;
}

server {
    listen 80;

    access_log  /var/log/nginx/secure.log notes if=$logme;
  
    # Optional: If the application does not generate a session cookie, we 
    # generate our own
    add_header Set-Cookie SESSION=1;

    # ...
}

对唯一项进行采样

不过,并非所有客户端都会遵循会话 Cookie。例如,网络爬虫可能会忽略 Cookie,导致其发出的每个请求都被识别为来自新用户,从而使结果出现偏差。

如果能在首次发现新的“对象”时从请求中采样,岂不是很好?这个“对象”可以是新的IP地址、新的会话Cookie值、新的“User-Agent”标头、此前未见过的Host标头,甚至是这些项的组合。这样,每个“对象”只需采样一次数据。

显然,我们需要存储状态(即已见过的事物列表),为此可以使用NGINX Plus的键值存储。键值存储维护一个内存键值数据库,可在NGINX Plus配置中通过变量访问;该数据库还可选择支持条目自动过期(timeout参数)、持久化存储(state)和集群同步(sync)。对于存储中尚不存在的每个事物,我们会记录相应请求,并将该事物添加到存储中,以免再次记录。

在NGINX Plus R18及更高版本中,可以非常轻松地在处理事务时设置键值对:

# Define a keyval zone with appropriate parameterskeyval_zone zone=clients:80m timeout=3600s;

# Create a variable $seen for each unique $remote_addr
keyval $remote_addr $seen zone=clients;

log_format notes '$remote_addr "$request" $status';

server {
    listen 80;

    # if $seen is empty, update the keyval (set $seen 1;) and log this 
    # request (set $logme 1;)
    # Otherwise, $logme is not set and we don't log the request
    # Note that $seen resets to "" after the configured timeout
    if ($seen = "") {
       set $seen 1;
       set $logme 1;
    }
    access_log  /var/log/nginx/secure.log notes if=$logme;

    location / {
        return 200 "All OK: -$seen-$logme-\n";
    }

    location /api {
        api;
    }
}

实际应用示例:对TLS参数进行采样

本文源于一个现实问题:如何按照良好实践配置TLS,同时又不将使用旧设备的用户拒之门外?

TLS最佳实践在不断变化。TLS 1.3于一年前正式获批,但许多客户端仅支持早期TLS版本;有些密码套件被认定为“不安全”并停用,但较旧的实现仍依赖这些密码套件;ECC证书的性能优于RSA证书,但并非所有客户端都支持ECC。许多TLS攻击依赖“中间人”拦截密码套件协商握手,并迫使客户端和服务器选择安全性较低的密码套件。因此,应将NGINX Plus配置为不支持安全性较弱或旧版密码套件,但这样做可能会导致旧版客户端无法访问。

在以下配置示例中,我们对每个TLS客户端进行采样,并记录SSL协议、密码套件和User-Agent标头。假设每个客户端都会选择其支持的最新协议和安全性较高的密码套件,我们便可评估采样数据,并确定停止支持旧版协议和密码套件后会有多大比例的客户端无法访问。

我们通过IP地址和User-Agent的唯一组合来识别每个客户端,但使用会话Cookie或其他方法识别客户端也同样有效。

log_format sslparams '$ssl_protocol $ssl_cipher '
                     '$remote_addr "$http_user_agent"';

# Define a keyval zone with appropriate parameters
keyval_zone zone=clients:80m timeout=3600s;

# Create a variable $seen for each unique combination of $remote_addr and 
# 'User-Agent' header
keyval $remote_addr:$http_user_agent $seen zone=clients;

server {
    listen 443 ssl;

    # default NGINX SSL configuration
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    if ($seen = "") {
       set $seen 1;
       set $logme 1;
    }
    access_log  /tmp/sslparams.log sslparams if=$logme;

    # ...
}

这会生成一个日志文件,其中包含如下所示的条目:

TLSv1.2 AES128-SHA 1.1.1.1 "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 2.2.2.2 "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 4.4.4.4 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
TLSv1 AES128-SHA 5.5.5.5 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
TLSv1.2 ECDHE-RSA-CHACHA20-POLY1305 6.6.6.6 "Mozilla/5.0 (Linux; U; Android 5.0.2; en-US; XT1068 Build/LXB22.46-28) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36"

然后,我们可以使用多种方法处理该文件,以确定数据的分布情况:

$ cat /tmp/sslparams.log | cut -d ' ' -f 2,2 | sort | uniq -c | sort -rn | perl -ane 'printf "%30s %s\n", $F[1], "="x$F[0];'    ECDHE-RSA-AES128-GCM-SHA256 =========================
    ECDHE-RSA-AES256-GCM-SHA384 ========
                     AES128-SHA ====
    ECDHE-RSA-CHACHA20-POLY1305 ==
        ECDHE-RSA-AES256-SHA384 ==

我们先识别使用量较低、安全性较弱的密码套件,再检查日志以确定哪些客户端正在使用这些套件,然后据此决定是否从NGINX Plus配置中移除这些密码套件。

结论

NGINX的条件日志功能可用于从NGINX管理的请求中抽取一部分样本,并写入标准日志或专用日志。如果需要快速抽取流量样本进行统计分析(例如确定SSL参数的分布情况),此方法会很有帮助。

需要仔细考虑数据采样方式,以免活跃用户或网络爬虫在样本中占比过高。可以使用NGINX配置中的变量,并结合map和split_clients指令来选择和筛选请求。

对于决策更复杂或需要较高准确度的情况,可以在NGINX配置中构建高级选择器。借助NGINX Plus键值存储,可以累积状态,并根据需要在集群中的多个NGINX Plus实例之间共享状态。

亲自体验NGINX Plus的请求采样功能,立即开始30天免费试用,或联系我们讨论您的用例。


分享
标签: SSL/TLS, F5 NGINX, Tech, 2016

关于作者

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。