Nginx安全配置
禁用不需要的 Nginx 模块
自动安装的 Nginx 会内置很多模块,并不是所有的模块都需要,对于非必须的模块可以禁用,
禁用autoindex
如 autoindex module ,下面展示如何禁用
# ./configure --without-http_autoindex_module # make # make install
不展示 server tokens
默认情况下,Nginx 的 server tokens 会在错误页面显示 Nginx 的版本号,
这可能会导致信息泄露,未经授权的用户可能会了解你使用的nginx版本。
应该在nginx.conf
通过设置server_tokens off
来禁用
控制资源和限制
为了防止对 Nginx 进行潜在的 DOS 攻击,可以为所有客户端设置缓冲区大小限制,配置如下:
-
client_body_buffer_size
指定客户端请求主体缓冲区的大小。默认值为8k或16k, 但建议将此值设置为低至1k:client_body_buffer_size 1k
-
client_header_buffer_size
为客户端请求标头指定标头缓冲区大小。 设置为 1k 足以应付大多数请求。 -
client_max_body_size
为客户端请求指定可接受的最大正文大小。 设置为 1k 应该足够了,但是如果通过 POST方法接收文件上传,则需要增加它。 -
large_client_header_buffers
指定用于读取大型客户端请求标头的缓冲区的最大数量和大小。 将最大缓冲区数设置为 2,每个缓冲区的最大大小为 1k。该指令将接受 2 kB 数据,large_client_header_buffers 2 1k
禁用所有不需要的 HTTP 方法
禁用所有不需要的 HTTP 方法,下面设置意思是只允许GET、HEAD、POST
方法,
过滤掉DELETE
和TRACE
等方法。
location / { limit_except GET HEAD POST { deny all; } }
另一种方法是在server
块 设置,不过这样是全局设置的,要注意评估影响
if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; }
监控访问日志和错误日志
持续监控和管理 Nginx 的错误日志,就能更好的了解对 web 服务器的请求,注意到任何遇到的错误,有助于发现任何攻击尝试,并确定您可以执行哪些操作来优化服务器性能。
可以使用日志管理工具(例如 logrotate )来旋转和压缩旧日志并释放磁盘空间。
同样,ngx_http_stub_status_module
模块提供对基本状态信息的访问。
合理配置响应头
为了进一步加强 Nginx web 的性能,可以添加几个不同的响应头,推荐
X-Frame-Options
可以使用 X-Frame-Options HTTP 响应头指示是否应允许浏览器在 <frame> 或 <iframe> 中呈现页面。 这样可以防止点击劫持攻击。
配置文件中添加:
add_header X-Frame-Options "SAMEORIGIN";
Strict-Transport-Security
HTTP Strict Transport Security,简称为 HSTS。它允许一个 HTTPS 网站,要求浏览器总是通过 HTTPS 来访问它,同时会拒绝来自 HTTP 的请求,操作如下:
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
CSP
Content Security Policy (CSP) 保护你的网站避免被使用如 XSS,SQL注入等手段进行攻击,操作如下:
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
配置 SSL 和 cipher suites
Nginx 默认允许使用不安全的旧 SSL 协议,ssl_protocols TLSv1 TLSv1.1 TLSv1.2
,建议做如下修改:
ssl_protocols TLSv1.2 TLSv1.3;
此外要指定cipher suites
,可以确保在 TLSv1 握手时,使用服务端的配置项,以增强安全性。
ssl_prefer_server_ciphers on
SSL 证书
全站SSL
全站做ssl是最常见的一个使用场景,默认端口443,而且一般是单向认证。
server { listen 443; server_name example.com; root /apps/www; index index.html index.htm; ssl on; ssl_certificate ../SSL/ittest.pem; ssl_certificate_key ../SSL/ittest.key; # ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; # ssl_prefer_server_ciphers on; }
如果想把http的请求强制转到https的话:
server { listen 80; server_name example.me; rewrite ^ https://$server_name$request_uri? permanent; ### 使用return的效率会更高 # return 301 https://$server_name$request_uri; }
ssl_certificate
证书其实是个公钥,它会被发送到连接服务器的每个客户端,
ssl_certificate_key
私钥是用来解密的,所以它的权限要得到保护但nginx的主进程能够读取。
当然私钥和证书可以放在一个证书文件中,这种方式也只有公钥证书才发送到client。
ssl_protocols
指令用于启动特定的加密协议,
nginx在1.1.13和1.0.12版本后默认是ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2
,
TLSv1.1与TLSv1.2要确保OpenSSL >= 1.0.1
,SSLv3 现在还有很多地方在用但有不少被攻击的漏洞。
ssl_ciphers
选择加密套件,不同的浏览器所支持的套件(和顺序)可能会不同。
这里指定的是OpenSSL库能够识别的写法,你可以通过
openssl -v cipher 'RC4:HIGH:!aNULL:!MD5'
(后面是你所指定的套件加密算法) 来看所支持算法。
ssl_prefer_server_ciphers on
设置协商加密算法时,优先使用我们服务端的加密套件,
而不是客户端浏览器的加密套件。
部分页面ssl
但是请注意不要理解错了,是对页面加密而不能针对某个请求加密, 一个页面或地址栏的URL一般会发起许多请求的, 包括css/png/js等静态文件和动态的java或php请求, 所以要加密的内容包含页面内的其它资源文件,否则就会出现http与https内容混合的问题。
在http页面混有https内容时,页面排版不会发生乱排现象; 在https页面中包含以http方式引入的图片、js等资源时,浏览器为了安全起见会阻止加载。
下面是只对example.com/account/login
登录页面进行加密的栗子:
root /apps/www; index index.html index.htm; server { listen 80; server_name example.com; location ^~ /account/login { rewrite ^ https://$server_name:443$request_uri? permanent; } location / { proxy_pass http://localhost:8080; ### Set headers #### proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; } } server { listen 443 ssl; server_name example.com; ssl on; ssl_certificate ../SSL/ittest.pem; ssl_certificate_key ../SSL/ittest.key; ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; location ^~ /account/login { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; ### Most PHP, Python, Rails, Java App can use this header -> https ### proxy_set_header X-Forwarded-Proto $scheme; } location / { rewrite ^ http://$server_name$request_uri? permanent; } }
当浏览器访问http://example.com/account/login.xx
时,
被301到https://example.com/account/login.xx
,
在这个ssl加密的虚拟主机里也匹配到/account/login
,反向代理到后端服务器,
后面的传输过程是没有https的。这个login.xx
页面下的其它资源也是经过https请求nginx的,
登录成功后跳转到首页时的链接使用http,这个可能需要开发代码里面控制。
-
上面配置中使用了
proxy_set_header X-Forwarded-Proto $scheme
, 在jsp页面使用request.getScheme()
得到的是https 。如果不把请求的scheme协议设置在header里, 后端jsp页面会一直认为是http,将导致响应异常。 -
ssl配置块还有个与不加密的80端口类似的
location /
, 它的作用是当用户直接通过https访问首页时,自动跳转到不加密端口, 你可以去掉它允许用户这样做。
实现双向SSL认证
上面的两种配置都是去认证被访问的站点域名是否真实可信,并对传输过程加密, 但服务器端并没有认证客户端是否可信。(实际上除非特别重要的场景, 也没必要去认证访问者,除非像银行U盾这样的情况)
要实现双向认证HTTPS,nginx服务器上必须导入CA证书(根证书/中间级证书), 因为现在是由服务器端通过CA去验证客户端的信息。还有必须在申请服务器证书的同时, 用同样的方法生成客户证书。取得客户证书后,还要将它转换成浏览器识别的格式 (大部分浏览器都认识PKCS12格式):
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
然后把这个client.p12
发给你相信的人,让它导入到浏览器中,
访问站点建立连接的时候nginx会要求客户端把这个证书发给自己验证,如果没有这个证书就拒绝访问。
同时别忘了在nginx.conf
里配置信任的CA:(如果是二级CA,请把根CA放在后面,形成CA证书链)
proxy_ignore_client_abort on; ssl on; ... ssl_verify_client on; ssl_verify_depth 2; ssl_client_certificate ../SSL/ca-chain.pem; #在双向location下加入: proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
nginx默认安装了一个ngx_http_geo_module
,这个geo模块可以根据客户端IP来创建变量的值,
用在如来自172.29.73.0/24
段的IP访问login时使用双向认证,其它段使用一般的单向认证。
geo $duplexing_user { default 1; include geo.conf; # 注意在0.6.7版本以后,include是相对于nginx.conf所在目录而言的 }
语法:
geo [$address] $variable { … }
位于http段,默认地址是$reoute_addr
,假设conf/geo.conf
内容:
127.0.0.1/32 LOCAL; # 本地 172.29.73.23/32 SEAN; # 某个IP 172.29.73.0/24 1; # IP段,可以按国家或地域定义后面的不同的值
需要配置另外一个虚拟主机server{ssl 445}
,里面使用上面双向认证的写法,
然后在80或443里使用变量$duplexing_user
去判断,如果为1就rewrite到445,
否则rewrite到443。具体用法可以参考nginx geo使用方法。
https优化参数
- ssl_session_cache shared:SSL:10m; : 设置ssl/tls会话缓存的类型和大小。 如果设置了这个参数一般是shared,buildin可能会参数内存碎片,默认是none, 和off差不多,停用缓存。如shared:SSL:10m表示我所有的nginx工作进程共享ssl会话缓存, 官网介绍说1M可以存放约4000个sessions。 详细参考serverfault上的问答ssl_session_cache。
- ssl_session_timeout : 客户端可以重用会话缓存中ssl参数的过期时间, 内网系统默认5分钟太短了,可以设成30m即30分钟甚至4h。
设置较长的keepalive_timeout也可以减少请求ssl会话协商的开销,但同时得考虑线程的并发数了。
提示:在生成证书请求csr文件时,如果输入了密码,nginx每次启动时都会提示输入这个密码, 可以使用私钥来生成解密后的key来代替,效果是一样的,达到免密码重启的效果:
openssl rsa -in ittest.key -out ittest_unsecure.key
结合Nginx与证书例子
生成签名请求
生成私钥jade_dungeon.key
和证书请求jade_dungeon.csr
:
openssl req -new -newkey rsa:2048 -sha256 -nodes \ -out jade_dungeon.csr -keyout jade_dungeon.key \ -subj "/C=CN/ST=Shanghai/L=Shanghai/O=Jade Dungeon/OU=Jade Dungeon/CN=*.jade-dungeon.net"
自签署CA证书
自己玩网站玩玩的话就自己签名,如果是商业的网站,花大价钱买商业证书吧。
用openssl自签名:
openssl x509 -req -days 365 -in jade_dungeon.csr -signkey jade_dungeon.key -out jade_dungeon.crt
购买CA证书
VeriSign、GlobaSign、Visa、Microsoft等,免费的只有StartSSL一家。
一般的流程都是把证书请求jade_dungeon.csr
给供应商,他们发证书jade_dungeon.crt
回来。
众所周知,前段时间某 NIC 机构爆出过针对 Google 域名的证书签发的丑闻, 所以可见选择一家靠谱的第三方 SSL 签发机构是多么的重要。
目前一般市面上针对中小站长和企业的 SSL 证书颁发机构有:
- StartSSL
- Comodo / 子品牌 Positive SSL
- GlobalSign / 子品牌 AlphaSSL
- GeoTrust / 子品牌 RapidSSL
其中 Postivie SSL、AlphaSSL、RapidSSL 等都是子品牌,一般都是三级四级证书, 所以你会需要增加 CA 证书链到你的 CRT 文件里。
以 Comodo Positive SSL 为例,需要串联 CA 证书,假设你的域名是example.com
。
那么,串联的命令是:
cat example_com.crt COMODORSADomainValidationSecureServerCA.crt COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > example_com.signed.crt
在 Nginx 配置里使用example_com.signed.crt
即可
如果是一般常见的 AplhaSSL 泛域名证书,他们是不会发给你 CA 证书链的, 那么在你的 CRT 文件后面需要加入 AlphaSSL 的 CA 证书链
针对企业的 EV SSL
EV SSL,是 Extended Validation 的简称,更注重于对企业网站的安全保护以及严格的认证。
最明显的区别就是,通常 EV SSL 显示都是绿色的条,比如本站的 SSL 证书就是 EV SSL。
如果贵公司想获取专业的 EV SSL,可以随时联系我们 info at cat dot net
保存证书
为了方便管理,把得到的三个文件都移动到/etc/ssl/private/
目录。
配置证书
server { listen 80; listen [::]:80 ssl ipv6only=on; listen 443 ssl; listen [::]:443 ssl ipv6only=on; server_name www.jade-dungeon.net; ssl on; ssl_certificate /etc/ssl/private/example_com.crt; ssl_certificate_key /etc/ssl/private/example_com.key; }
默认的SHA-1形式不够安全,而现在主流的方案应该都避免SHA-1,为了确保更强的安全性, 我们可以采取迪菲-赫尔曼密钥交换。
首先,进入/etc/ssl/certs
目录并生成一个jade_dungeon.pem
:
openssl dhparam -out dhparam.pem 2048 # 如果你的机器性能足够强大,可以用 4096 位加密
生成完毕后,在 Nginx 的 SSL 配置后面加入:
ssl_prefer_server_ciphers on; ssl_dhparam /etc/ssl/certs/dhparam.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4"; keepalive_timeout 70; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;
全站HTTPS
同时,如果是全站 HTTPS 并且不考虑 HTTP 的话, 可以加入 HSTS 告诉你的浏览器本网站全站加密,并且强制用 HTTPS 访问:
add_header Strict-Transport-Security max-age=63072000; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff;
同时也可以单独开一个 Nginx 配置,把 HTTP 的访问请求都用301
跳转到 HTTPS:
server { listen 80; listen [::]:80 ssl ipv6only=on; server_name example.com; return 301 https://example.com$request_uri; }
只针对注册、登陆进行https加密处理
既然HTTPS能保证安全,为什么全世界大部分网站都仍旧在使用HTTP呢?使用HTTPS协议, 对服务器来说是很大的负载开销。从性能上考虑, 我们无法做到对于每个用户的每个访问请求都进行安全加密 (当然,Google这种大神除外)。作为一个普通网站,我们所追求的只是在进行交易、 密码登陆等操作时的安全。通过配置Nginx服务器,可以使用rewrite来做到这一点。
在https server下加入如下配置:
if ($uri !~* "/logging.php$") { rewrite ^/(.*)$ http://$host/$1 redirect; }
在http server下加入如下配置:
if ($uri ~* "/logging.php$") { rewrite ^/(.*)$ https://$host/$1 redirect; }
这样一来,用户会且只会在访问logging.php
的情况下,才会通过https访问。
更新:有一些开发框架会根据$_SERVER[‘HTTPS’]
这个 PHP 变量是否为on
来判断当前的访问请求是否是使用 https。
为此我们需要在 Nginx 配置文件中添加一句来设置这个变量。
遇到 https 链接重定向后会自动跳到 http 问题的同学可以参考一下。
server { ... listen 443; location \.php$ { ... include fastcgi_params; fastcgi_param HTTPS on; # 多加这一句 } } server { ... listen 80; location \.php$ { ... include fastcgi_params; } }