1、基于 IP 地址的访问
根据客户端的 IP 地址控制访问。
使用 HTTP 或 stream 访问模块控制对受保护资源的访问:
location /admin/ { deny 10.0.0.1; allow 10.0.0.0/20;allow 2001:0db8::/32; deny all;}
给定的 location 代码块允许来自 10.0.0.0/20 中的任何 IPv4 地址访问(10.0.0.1 除外),允许来自 2001:0db8::/32 子网中的 IPv6 地址访问,并在收到来自其他任何地址的请求后返回 403。allow 和 deny 指令在 http、server 和 location 上下文以及 TCP/UDP 的 stream、server 上下文中有效。按顺序检查规则,直到找到与远程地址匹配的规则为止。
详解
互联网上的宝贵资源和服务必须要进行多层保护,NGINX 就是其中一层的安全卫士。deny 指令可阻止对给定上下文的访问,而 allow 指令可用来允许被阻止访问的子集。您可以使用 IP 地址、IPv4 或 IPv6、无类别域间路由(CIDR)块范围,关键字 all 和Unix 套接字。在保护资源时,通常会允许一个内部 IP 地址块,并拒绝所有访问请求。
2、允许跨域资源共享
您提供了来自另一个域的资源,需要允许跨域资源共享(CORS),以便浏览器能够利用这些资源。
根据 request 方法更改请求头,启用 CORS:
map $request_method $cors_method {OPTIONS 11;GET 1;POST 1;default 0;}server {# ...location / { if ($cors_method ~ '1') { add_header 'Access-Control-Allow-Methods''GET,POST,OPTIONS'; add_header 'Access-Control-Allow-Origin''*.example.com'; add_header 'Access-Control-Allow-Headers' 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; }if ($cors_method = '11') { add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=UTF-8'; add_header 'Content-Length' 0; return 204;} }}
这个示例中的内容很多,但通过使用 map 将 GET 和 POST 方法进行分组,简化了示例中的内容。OPTIONS 请求方法向客户端返回有关该服务器 CORS 规则的 preflight 请求。在 CORS 下允许使用 OPTIONS、GET 和 POST 方法。设置 Access-ControlAllow-Origin 标头,允许该服务器提供的内容也可以在与此请求头匹配的原始页面上使用。preflight 请求可以在客户端缓存 1,728,000 秒,相当于 20 天。
详解
当 JavaScript 等资源所请求的资源属于其他域时,就会发生 CORS。当请求被视为跨域的时候,浏览器就要遵守 CORS 规则。如果浏览器没有专门允许其使用资源的请求头,那么它将不会使用该资源。为了允许其他子域使用我们的资源,我们必须设置CORS 请求头,这可通过 add_header 指令完成。如果请求方法是具有标准内容类型的 GET、HEAD 或 POST,并且没有特殊的请求头,则浏览器将发出请求并且只检查来源。其他请求方法将导致浏览器发出 preflight 请求,以检查它将遵守的关于该资源的服务器条款。如果您没有正确设置这些请求头,那么浏览器会在尝试利用这些资源时出错。
3、客户端加密
在 NGINX 服务器和客户端之间加密流量。
使用一个 SSL 模块加密流量,例如 ngx_http_ssl_module 或 ngx_stream_ssl_module:
http{ # All directives used below are also valid in stream server {listen 8443 ssl;ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key;}}
此配置将服务器设置为侦听使用 SSL/TLS 加密的端口 8443。指令 ssl_certificate 定义了提供给客户端的证书和可选的证书链。ssl_certificate_key 指令定义了 NGINX 用来解密请求和加密响应的密钥。许多 SSL/TLS 协商配置默认为 NGINX 版本的缺省配置。
详解
安全传输层是最常见的加密传输中信息的方法。截至本文撰写之时,TLS 协议比 SSL 协议更受欢迎。这是因为现在人们认为 SSL 版本 1 至 3 不安全。尽管协议名称可能不同,但 TLS 仍然建立了安全套接字层。NGINX 能够让您的服务保护您和客户端之间的信息,进而保护客户端和您的业务。在使用 CA 签名的证书时,需要将证书与证书颁发机构链连接起来。在连接证书和证书颁发机构链时,证书应位于证书颁发机构链文件之上。如果证书颁发机构提供了多个文件作为证书颁发机构链的中间证书,则它们会按顺序进行分层。有关排序问题,请参阅证书提供商的文档。
参考资料
Mozilla SSL 配置生成器
SSL Labs 的 SSL 服务器测试
4、高级客户端加密
具有高级客户端-服务器加密配置需求。
NGINX 的 http 和 stream 模块可完全控制接受的 SSL/TLS 握手。证书和密钥可通过文件路径或可变值的方式提供给 NGINX。NGINX 根据其配置为客户端提供可接受的协议、密码和密钥类型列表。客户端与 NGINX 服务器之间的最高标准是经过协商的。NGINX 可以将客户端-服务器 SSL/TLS 的协商结果缓存一段时间。
下面的示例同时展示了许多选项,意在说明客户端-服务器协商的复杂性:
http { # All directives used below are also valid in stream server {listen 8443 ssl;# Set accepted protocol and cipherssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers HIGH:!aNULL:!MD5;# RSA certificate chain loaded from filessl_certificate /etc/nginx/ssl/example.crt;# RSA encryption key loaded from filessl_certificate_key /etc/nginx/ssl/example.pem;# Elliptic curve cert from variable valuessl_certificate $ecdsa_cert;# Elliptic curve key as file path variablessl_certificate_key data:$ecdsa_key_path;# Client-Server negotiation cachingssl_session_cache shared:SSL:10m;ssl_session_timeout 10m;}}
服务器接受 SSL 协议版本 TLSv1.2 和 TLSv1.3。接受的密码设置为 HIGH,这是最高标准的宏;对于 aNULL 和 MD5,显式拒绝是通过 ! 符号来表示的。使用了两组证书密钥对。传递给 NGINX 指令的值展示了提供 NGINX 证书密钥值的不同方式。变量被解释为文件的途径。当带有前缀 data: 时,变量值被解释为直接值。可提供多种证书密钥格式,以实现与客户端的反向兼容性。客户端支持的、服务器接受的最强标准将是协商的结果。
如果 SSL/TLS 密钥作为直接值变量暴露,则有可能会被配置记录或暴露。如果将密钥值暴露为变量,请确保您拥有严格的变更和访问控制措施。
SSL 会话缓存和超时允许 NGINX worker 进程在给定的时间内缓存和存储会话参数。作为单个实例化中的进程,NGINX worker 进程会相互共享此缓存(而非在机器之间共享)。还有许多其他会话缓存选项可以帮助提高所有类型的用例的性能或安全性。您可以结合使用不同的会话缓存选项。但是,指定没有默认值的缓存选项后,默认的内置会话缓存将会关闭。
详解
在上面的高级配置示例中,NGINX 为客户端提供了备受推重的密码算法 —— TLS 版本1.2 或 1.3 的 SSL/TLS 选项,以及使用 RSA 或椭圆曲线密码(ECC)格式密钥的能力。客户端所能实现的最强协议、密码和关键格式是协商的结果。该配置指示 NGINX 将协商结果缓存 10 分钟,可用内存分配为 10MB。
测试发现,ECC 证书的速度比同等强度的 RSA 证书快。密钥占用空间较小,可以提供更多的 SSL/TLS 连接及更快的握手速度。NGINX 允许您配置多个证书和密钥,然后为客户端浏览器提供最佳证书。您可以利用更新的技术,但仍然服务旧客户端。在此示例中,NGINX 正在加密它与客户端之间的流量。但是,与上游(upstream)服务器的连接也可能进行了加密。在本文第五章Upstream 加密中演示了 NGINX 与上游服务器之间的协商。
参考资料
Mozilla SSL 配置生成器
SSL Labs 的 SSL 服务器测试
5、Upstream 加密
需要加密 NGINX 和上游服务之间的流量,并为合规性法规设置特定的协商规则;或者上游(upstream)服务位于有安全防护措施的网络之外。
使用 HTTP 代理模块的 SSL 指令来指定 SSL 规则:
location / {proxy_pass https://upstream.example.com; proxy_ssl_verify on; proxy_ssl_verify_depth 2; proxy_ssl_protocols TLSv1.2;}
这些代理指令为 NGINX 设定了需要遵守的特定 SSL 规则。配置的指令可确保 NGINX 验证上游服务上的证书和证书链是否有效,验证深度最多为两个证书。proxy_ssl_protocols 指令指定 NGINX 只使用 TLS 版本 1.2。默认情况下,NGINX 不会验证上游证书并接受所有 TLS 版本。
详解
HTTP 代理模块的配置指令非常庞大,如果您需要加密上游流量,那么至少应该启用验证。您只需更改传递给 proxy_pass 指令的值对应的协议,即可通过 HTTPS 进行代理。但是,这不会验证上游证书。其他指令(如 proxy_ssl_certificate 和 proxy_ssl_ssl_certificate_key)允许您锁定上游加密,以增强安全性。您还可以指定 proxy_ssl_crl 或证书吊销列表,该列表列出了被认为无效的证书。这些 SSL 代理指令有助于增强系统在自己的网络或跨公共互联网的通信通道。
6、保护位置
使用 secret 保护 location 代码块。
通过 secure link 模块和 secure_link_secret 指令,仅允许拥有安全链接的用户访问
资源:
location /resources {secure_link_secret mySecret;if ($secure_link = "") { return 403; }rewrite ^ /secured/$secure_link;}location /secured/ {internal;root /var/www;}
这种配置创建了一个内部和公共 location 代码块。请求 URI 需包含 md5 哈希字符串(可通过提供给 secure_link_secret 指令的 secret 进行验证),否则公共 location 代码块 /resources 将返回 403 Forbidden。URI 中的哈希值需进行验证,否则 $ secure_link 变量将是一个空字符串。
详解
使用 secret 保护资源是一种有效的文件保护方法。结合使用 secret 与 URI,然后对该字符串进行 md5 哈希计算,并在 URI 中使用这个 md5 哈希算法的十六进制摘要。将哈希值放置在链接中,并通过 NGINX 进行评估。NGINX 知道要请求的文件的路径,因为它位于哈希算法之后的 URI 中。NGINX 还知道您的 secret,因为它通过secure_link_secret 指令提供。NGINX 能够快速验证 md5 哈希算法,并将 URI 存储在$secure_link 变量中。如果无法验证哈希值,则将变量设置为空字符串。需要注意的是,传递给 secure_link_secret 的参数必须是静态字符串,不能是变量。
7、使用 secret 生成安全链接
需要使用 secret 从应用中生成安全链接。
NGINX 中的 secure link 模块接受 md5 哈希字符串的十六进制摘要,其中字符串由URI 路径和 secret 组成。在上一节的6、保护位置基础上,我们将创建一个安全链接,
location /resources { secure_link_secret mySecret; if ($secure_link = "") { return 403; } rewrite ^ /secured/$secure_link; } location /secured/ { internal; root /var/www; }
与前面的配置示例搭配使用,因为 /var/www/secured/index.html 上有一个文件。要生成 md5 哈希算法的十六进制接要,我们可以使用 Unix openssl 命令:
$ echo -n 'index.htmlmySecret' | openssl md5 -hex (stdin)= a53bee08a4bf0bbea978ddf736363a12
此处展示了我们要保护的 URI index.html,它与我们的 secret mySecret 拼接在一起。该字符串被传递到 openssl 命令,输出 md5 十六进制摘要。
下面是使用 Python Standard Library 中包含的 hashlib 库在 Python 中生成相同哈希摘
要的示例:
import hashlibhashlib.md5.(b'index.htmlmySecret').hexdigest()'a53bee08a4bf0bbea978ddf736363a12'
现在我们得到了这个哈希摘要,并可以在 URL 中使用它了。我们的示例是 www.example.com 通过我们的 /resources 位置请求 /var/www/secured/index.html 上的文
件。我们的完整 URL 将是:
www.example.com/resources/a53bee08a4bf0bbea978ddf736363a12/index.html
详解
我们可以通过多种方式,用多种语言生成哈希摘要。请记住:URI 路径在 secret 之前;字符串中没有回车;使用 md5 哈希算法的十六进制摘要。
8、保护过期的位置
需要保护一个位置,该位置的链接将在未来某个时间过期,并且特定于某个客户端。
利用 secure link 模块中的其他指令设置过期时间,并在 secure link 中使用变量:
location /resources { root /var/www;secure_link $arg_md5,$arg_expires;secure_link_md5 "$secure_link_expires$uri$remote_addrmySecret"; if ($secure_link = "") { return 403; }if ($secure_link = "0") { return 410; }}
secure_link 指令带两个参数,这两个参数用两个逗号隔开。第一个参数是保存 md5 哈希值的变量。本例中使用了 md5 的 HTTP 参数。第二个参数是一个变量,该变量保存了以 Unix 时间戳格式显示的链接过期时间。secure_link_md5 指令带一个参数,该参数声明了用于生成 md5 哈希值的字符串的格式。与其他配置一样,如果哈希值未经验证,则 $secure_link 变量将设置为空字符串。但是,使用这种用法,如果哈希值匹配,但时间已过期,则 $secure_link 变量将设置为 0。
详解
这种保护链接的用法比上上章节6、保护位置 中所示的 secure_link_secret 更灵活、更简洁。通过这些指令,您可以在哈希字符串中使用 NGINX 可用的任意数量的变量。在哈希字符串中使用特定于用户的变量可增强安全性,因为用户无法交换到受保护资源的链接。建议使用类似于 $remote_addr 或 $http_x_forwarded_for 的变量,或者由应用生成的会话 cookie 请求头。secure_link 的参数可以来自您喜欢的任何变量,并且能够以最合适的方式命名。问题是:您拥有访问权限吗?您是否在链接有效期内访问?如果您没有访问权限,将会收到消息 Forbidden。如果您拥有访问权限,但链接却过期了,将会收到消息 Gone。HTTP 410 Gone 非常适合过期的链接,因为该条件应被视为永久性的。
9、生成过期链接
需要生成一个过期的链接。
为过期的时间生成一个 Unix 时间戳格式的时间戳。在 Unix 系统上,您可以使用如下所示的日期进行测试:
$ date -d "2030-12-31 00:00" +%s --utc 1924905600
接下来,您需要连接哈希字符串,以匹配使用 secure_link_md5 指令配置的字符串。在本例中,我们要使用的字符串将是 1924905600/resources/index.html127.0.0.1 mySecret。md5 哈希值与十六进制摘要稍有不同。它是二进制格式的 md5 哈希值,base64 编码,加号(+)转换成了连字符(-),斜杠(/)转换成了下划线(_),删除了等号(=)。以下是在 Unix 系统上操作的一个示例:
$ echo -n '1924905600/resources/index.html127.0.0.1 mySecret' \| openssl md5 -binary \| openssl base64 \| tr +/ -_ \| tr -d = sqysOw5kMvQBL3j9ODCyoQ
现在我们得到了哈希值,我们可以将它和过期日期用作参数:/resources/index.html?md5=sqysOw5kMvQBL3j9ODCyoQ&expires=1924905600下面是 Python 中使用相对过期时间的更实际的示例,它将链接设置为生成后一小时过期。在撰写本文之时,此示例适用于 Python 2.7 和使用 Python 标准库的 3.x:
from datetime import datetime, timedelta from base64 import b64encode import hashlib # 设置环境变量 resource = b'/resources/index.html' remote_addr = b'127.0.0.1' host = b'www.example.com' mysecret = b'mySecret' # 生成过期时间戳 now = datetime.utcnow() expire_dt = now + timedelta(hours=1) expire_epoch = str.encode(expire_dt.strftime('%s')) # 计算字符串的 md5 哈希值 uncoded = expire_epoch + resource + remote_addr + mysecret md5hashed = hashlib.md5(uncoded).digest() # 对字符串进行 base64 编码和转换 b64 = b64encode(md5hashed) unpadded_b64url = b64.replace(b'+', b'-')\ .replace(b'/', b'_')\ .replace(b'=', b'') # 格式化并生成链接 linkformat = "{}{}?md5={}?expires={}" securelink = linkformat.format( host.decode(), resource.decode(), unpadded_b64url.decode(), expire_epoch.decode() ) print(securelink)
详解
有了这种模式,我们能够生成具有特殊格式并能在 URL 中使用的安全链接。secret 通过使用从未发送给客户端的变量来提供安全性。您可以使用尽可能多的其他变量来保护位置的安全。md5 哈希计算和 base64 编码是通用的、轻量级的,并且支持几乎任何语言。