Jade Dungeon

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方法, 过滤掉DELETETRACE等方法。

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;
    }
}