2016 年 10 月,欧盟法院裁定 IP 地址为“个人信息”,因此适用《数据保护指令》和《通用数据保护条例》(GDPR)。对于许多网站所有者而言,这意味着如果数据离开了欧盟,那么日志文件的归档和分析就会面临问题。进入美国的数据虽有欧盟-美国隐私护盾提供一定的保护,但仍面临着隐私组织和政府的法律质疑,他们认为目前的安全防护还不够。
然而,保护日志文件中的个人数据不仅仅是欧盟的问题。对于获得 ISO/ICE 27001 等安全认证的企业而言,将日志文件移出其原生安全区,例如从网络运维部门移至营销部门,就可能会影响其认证范围和合规性。
本文介绍了一些简单的解决方案,可用于清理 NGINX Plus 和 NGINX 日志文件,这样就能将其安全导出,而不会泄露“个人身份信息(PII)”。
[编者按 – 本文是探讨 NGINX JavaScript 模块(最初称为 nginxScript)用例的系列文章之一。查看完整列表,请参阅《NGINX JavaScript 模块的用例》。
本文已更新,在 NGINX Plus R23 及更高版本中,使用 js_import
指令取代 js_include
指令。更多信息,请参阅 NGINX JavaScript 模块的参考文档 – “示例配置”一节显示了 NGINX 配置和 JavaScript 文件的正确语法。]
简单方法行不通
保护个人数据的最简单方法是在导出日志之前将 IP 地址信息移除。使用标准 Linux 命令行工具很容易做到这一点,但日志分析系统可能需要日志文件采用标准格式,且无法导入缺少 IP 地址字段的日志。即使成功导入了日志,如果分析系统依靠 IP 地址来跟踪用户活动,日志处理的价值也会大大降低。
另一种可能的方法是用虚假值或随机值替代真实 IP 地址,这样得到的日志文件虽然看起来完整,但日志分析的质量却大打折扣,因为每个日志条目都显示为来自随机生成的不同 IP 地址。
脱敏客户端 IP 地址
最有效的解决方案是使用一项称为“数据脱敏”的技术,将真实 IP 地址转换为另一种 IP 地址,后者无法用于识别最终用户,但仍支持对特定用户的网站活动进行关联。数据脱敏算法始终为给定的输入值生成相同的伪随机值,同时可确保这些伪随机值无法被转换回原始输入值。每次出现的 IP 地址都会被转换为相同的伪随机值。
您可以使用 NGINX JavaScript 模块在 NGINX 和 NGINX Plus 中实现 IP 地址脱敏。该模块是一个面向 NGINX 和 NGINX Plus 的 JavaScript 实现,专为服务器端用例和按请求处理而设计。在本例中,我们会执行少量 JavaScript 代码,从而在记录每个请求时脱敏客户端 IP 地址。
文末提供了在 NGINX 和 NGINX Plus 上启用 NGINX JavaScript 模块的操作说明。
支持 IP 地址脱敏的 NGINX 和 NGINX Plus 配置
log_format
指令控制访问日志中显示的信息。NGINX 和 NGINX Plus 的默认日志格式为 combined,用它生成的日志文件可使用大多数日志处理工具进行处理。
对于此配置,我们创建了一种新的日志格式 – masked,它与 combined 格式差别不大,除了第一个字段 – 我们使用 $remote_addr_masked
替换了 $remote_addr
变量。这个新变量通过执行 JavaScript 代码进行计算,从而生成客户端 IP 地址的脱敏值。
为了让 NGINX 和 NGINX Plus 以这种格式编写访问日志,我们使用 access_log
指令配置 server
块,以指定 masked 格式。
因为我们打算使用 NGINX JavaScript 脱敏客户端 IP 地址,所以我们使用 js_import
指令来指定 JavaScript 代码的位置。该 js_set
指令指定了在计算 $remote_addr_masked
变量时要执行的 JavaScript 函数。
请注意,我们使用了两个 access_log
指令。第一个指令使用默认日志格式生成访问日志,可供管理员用于运维。第二个指令指定了 masked 日志格式。通过这种配置,我们为每个请求编写两个访问日志 — 一个供系统管理员和 DevOps 使用,另一个用于导出。
最后,location
块使用 return
指令定义了一个非常简单的响应,以确认数据脱敏正常运行。在生产环境中,这很可能会包含一个 proxy_pass
指令,用于将请求定向到后端服务器。
用于 IP 地址脱敏的 NGINX JavaScript 代码
我们使用三个简单的 JavaScript 函数构建脱敏的 IP 地址。因为依赖函数必须先出现,所以我们将按照它们出现的先后顺序进行说明。
数据脱敏解决方案的本质是使用单向哈希算法来转换客户端 IP 地址。在本例中,我们使用的是 FNV-1a 哈希算法。该算法不仅紧凑、快速,并具有良好的分布特性,而且还会返回 32 位正整数(与 IPv4 地址相同的大小),因此非常适合用于表示 IP 地址。fnv32a
函数是 FNV-1a 算法的 JavaScript 实现 。
i2ipv4
函数将 32 位整数转换为点分四进制记法的 Ipv4 地址。它从 fnv32a()
中获取哈希值,并提供在访问日志中“看起来正确”的表示形式。IPv6 地址和 IPv4 地址都用 IPv4 格式表示。.
最后,我们使用 maskRemoteAddress
函数。该函数由上述 NGINX 和 NGINX Plus 配置中的 js_set
指令引用。它带有一个参数 req
,即表示 HTTP 请求的 JavaScript 对象。remoteAddress
属性包含客户端 IP 地址的值(相当于 $remote_addr
变量)。
IP 地址脱敏示例
上述配置完成后,我们就可以向服务器发出一个简单的请求,并检查响应和生成的访问日志条目。
$ curl http://localhost/
127.0.0.1 -> 8.163.209.30
$ sudo tail --lines=1 /var/log/nginx/access*.log
==> /var/log/nginx/access.log <==
127.0.0.1 - - [16/Mar/2017:19:08:19 +0000] "GET / HTTP/1.1" 200 26 "-" "curl/7.47.0"
==> /var/log/nginx/access_masked.log <==
8.163.209.30 - - [16/Mar/2017:19:08:19 +0000] "GET / HTTP/1.1" 200 26 "-" "curl/7.47.0"
脱敏查询字符串中的个人数据
日志文件也可能包含除 IP 地址以外的个人数据。电子邮件地址、邮政地址及其他标识符均可作为查询字符串参数传递,因此被记录为请求 URI 的一部分。如果您的应用以这种方式传递个人数据,则可扩展 NGINX JavaScript IP 地址脱敏解决方案,以清理查询字符串中的个人数据。
支持查询字符串脱敏的 NGINX 和 NGINX Plus 配置
与默认 combined 格式一样,上面为 IP 地址脱敏定义的 masked 日志格式会记录 $request
变量。该变量捕获请求的三个组成部分:HTTP 方法、URI(包括查询字符串)及 HTTP 版本。我们只需要脱敏查询字符串,因此为了提高代码效率,我们分别对这三个组成部分使用单独的变量 — 仅使用 $request_uri_masked
变量转换请求 URI(第二个组成部分),并对第一个和第三个组成部分使用标准变量($request_method
和 $server_protocol
)。
server
块需要使用另一个 js_set
指令来定义如何计算 $request_uri_masked
变量。
用于查询字符串脱敏的 NGINX JavaScript 代码
我们将 maskRequestURI
函数添加到上述 mask_ip_uri.js 文件中。与 maskRemoteAddress()
一样,maskRequestURI()
依赖于 fnv32a
哈希函数,因此在文件中它位于该函数的下方。
maskRequestURI
函数会遍历查询字符串中的每个键值对,查找已知包含个人数据的特定键。这些键的值都会被转换为脱敏值。
根据要对 NGINX 和 NGINX Plus 日志文件执行的处理类型,脱敏后的查询字符串值可能需要与真实数据格式相同。在上面的示例中,我们将 zip
格式设置为五位数,并将 email
格式设置为符合 RFC 821。其他键可能需要更复杂的格式设置或专用函数来构建。
查询字符串脱敏实例
经过这些额外的配置,查询字符串脱敏实例如下所示。
$ curl "http://localhost/index.php?foo=bar&zip=90210&email=liam@nginx.com"
127.0.0.1 -> 8.163.209.30
$ sudo tail --lines=1 /var/log/nginx/access*.log
==> /var/log/nginx/access.log <==
127.0.0.1 - - [16/Mar/2017:20:05:55 +0000] "GET /index.php?foo=bar&zip=90210&email=liam@nginx.com HTTP/1.1" 200 26 "-" "curl/7.47.0"
==> /var/log/nginx/access_masked.log <==
8.163.209.30 - - [16/Mar/2017:20:05:55 +0000] "GET /index.php?foo=bar&zip=38643&email=2852675791@example.com HTTP/1.1" 200 26 "-" "curl/7.47.0"
总结
带有 NGINX JavaScript 模块的 NGINX 和 NGINX Plus 提供了简单而强大的解决方案来将自定义逻辑应用于请求处理。在本文中,我们演示了如何使用 NGINX JavaScript 脱敏个人数据,这样得到的日志文件便可在不违反数据保护要求的情况下进行离线分析。
我们希望了解您正如何使用 NGINX JavaScript 来解决业务问题;欢迎大家在下方评论区进行分享。
如欲试用 NGINX Plus,请下载 30 天免费试用版,或与我们联系以讨论您的用例。
为 NGINX 和 NGINX Plus 启用 NGINX JavaScript
- 为 NGINX Plus 加载 NGINX JavaScript 模块
- 为 NGINX 开源版加载 NGINX JavaScript 模块
- 将 NGINX JavaScript 编译为 NGINX 开源版的动态模块
为 NGINX Plus 加载 NGINX JavaScript 模块
NGINX JavaScript 可作为免费动态模块供 NGINX Plus 用户使用。有关加载说明,请参阅《NGINX Plus 管理指南》。
为 NGINX 开源版加载 NGINX JavaScript 模块
NGINX JavaScript 模块默认包含在 NGINX Docker 官方镜像中。如果您的系统配置为使用面向 NGINX 开源版的官方预构建包,并且您安装的版本是 1.9.11 或更高版本,则可以按预构建包的形式安装 NGINX JavaScript。
-
安装预构建包。
-
Ubuntu 和 Debian 系统:
$ sudo apt-get install nginx-module-njs
-
RedHat、CentOS 和 Oracle Linux 系统:
$ sudo yum install nginx-module-njs
-
-
在 nginx.conf 配置文件的顶层(“main”)上下文(而非
http
或stream
上下文)中添加一个load_module
指令,以启用该模块。本例面向 HTTP 和 TCP/UDP 流量加载 JavaScript 模块。load_module modules/ngx_http_js_module.so; load_module modules/ngx_stream_js_module.so;
-
重新加载 NGINX,以便将 NGINX JavaScript 模块加载到运行实例中。
$ sudo nginx -s reload
将 NGINX JavaScript 编译为 NGINX 开源版的动态模块
如果您更喜欢从源码编译 NGINX 模块:
- 按照说明从开源仓库构建 HTTP 和/或 TCP/UDP NGINX JavaScript 模块。
- 将模块二进制文件(ngx_http_js_module.so、 ngx_stream_js_module.so)复制到 NGINX 根目录的 modules 子目录(通常为 /etc/nginx/modules)。
- 执行 “为 NGINX 开源版加载 NGINX JavaScript 模块”的第 2 步和第 3 步。