国内网站全站 HTTPS 逐渐流行

刚才浏览豆瓣的时候,发现从 Google 结果页跳的豆瓣音乐页面已经是 https 开头了,于是再试了一次它的主站点(https://www.douban.com)也已经可以访问,支持了 TLS 协议,终于不是再像不久前那样跳回到未加密的 http 页面。

用浏览器控制台稍微看了下豆瓣的资源加载情况(https://music.douban.com/subject/26651208/):
(图片可以右键打开看原图)

对比通过 http 协议请求的时候:

两者的区别其实有些有意思的地方。

现如今一个 web 页面已经不太可能只是引用单一域名下面资源(因为性能、扩展等诸多方面的原因),而这点恰好成为现实中部署 https 的最大阻碍之一——你需要将页面包含的所有资源都走 https 才行!在 CDN 时代,这意味着你需要 CDN 的支持才行。举例来说就是 http://music.douban.com/subject/26651208/ 这样的 url,如果你想将它部署到 https,那么页面里面加载的 img#.douban.com 域名也同样要部署 https!

看起来走 https 的豆瓣的部署是通过将js, css, 图片等静态资源放到一个新的域名(doubanio.com)下面来提供的,而不是像一般的做法那样——直接在原来的 img#.douban.com 静态资源域名下面加 https。这在实现上需要后端根据不同协议在页面插入不同域名。这点来说,推测应该是这样:豆瓣为了尽量不影响 http 协议的访问,引入了新的 doubanio.com 域名来发布静态资源。实际上通过查看 doubanio.com 的 DNS CNAME 记录,可以看到这个域名是指向了腾讯云的 CDN,豆瓣静态资源站点是通过腾讯云来支持 CDN 的。说实话,看到国内 CDN 终于也开始支持 https,其实挺意外的(当然,也许是我在这点的认知已经太落伍了,12306 的“证书污染”可能是过去式了吧)。如果没有记错的话,以前 img#.douban.com 这些域名是在用蓝汛的 CDN,至于为啥不用了,本人当然是不知道的:)。目测豆瓣在 https 部署稳定后,也会将 http 页面的静态资源转向 doubanio.com (其实目前观察来看已经有这个趋势了)。这样,整个 https 化的过程就是平滑而用户感知不到的了。

腾讯云CDN 用了 spdy/3.1 协议,这点对于性能来说还是挺不错的。不过也许为了安全性,豆瓣正式推出后应该加个 HSTS 响应头的,毕竟,这么做的本意也就是为了用户的安全,默认的 http 还是会让用户被运营商劫持个遍。

另外一个观测到也正在部署 https 的站点是京东。

已经使用了通配证书,但是静态资源站点却并没有走 https,因此浏览器拒绝加载了。看了下,仍然是 CDN 不支持,直接访问 https 的话,是 12306 的证书。

这么多年以来,国内的一些大网站终于开始部署 https,此前有百度、阿里,现在豆瓣、京东正在部署。对于用户来说这是个好事,至少可以少很多中间人攻击/劫持,运营商插入广告等行为(这个应该才是电商网站部署的初衷)。但是更重要的也许还是数据库里面用户信息的保护。

Varnish: How do You Know Your Backend Responses Properly?

You might use varnishstat to monitor backend healthy status, but that is not so directly, instead you can do this:

# varnishlog -g session -C -q 'ReqHeader:Host ~ "example.com"' | grep Status
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
...

Convenient, right? you can tail and grep whatever you want of varnishlog’s output.

Whitelisting IPs in Limiting Request Rate of Nginx and Varnish

Sometimes we want to exclude IP blocks from limited request rate zone of web servers, here is how we can do it in Nginx and Varnish, the Nginx way needs crappy hacks, on the other hand, Varnish handles it really elegant.

The Nginx way:

# in the http block, we define a zone, and use the geoip
# module to map IP addresses to variable
http {
    ...
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    # geo directive maps $remote_addr to $block variable.
    # limited as default
    geo $block {
        default          limited;
        10.10.10.0/24    blacklist;
        192.168.1.0/24   whitelist;
        include more_geoip_map.conf;
    }
}

# server block
server {
    ...
    location /wherever {
        if ($block = whitelist) { return 600; }
        if ($block = limited)   { return 601; }
        if ($block = blacklist) { return 403; }
        # error code 600 and 601 goes to each's internal location.
        error_page 600 = @whitelist;
        error_page 601 = @limited;
    }

    # @whitelist have no limiting, it just passes
    # the requests to backend.
    location @whitelist {
        proxy_pass http://backend;
        # feel free to log into other file.
        #access_log /var/log/nginx/$host_whitelist.access.log;
    }

    # insert limit_req here.
    location @limited {
        limit_req zone=one burst=1 nodelay;
        proxy_pass http://backend;
        # feel free to log into other file.
        #access_log /var/log/nginx/$host_limited.access.log;
    }
    ...
}

The Varnish way:

vcl 4.0;

import vsthrottle;

acl blacklist {
    "10.10.10.0/24";
}

acl whitelist {
    192.168.1.0/24;
}

sub vcl_recv {
    # take client.ip as identify to distinguish clients.
    set client.identity = client.ip;
    if (client.ip ~ blacklist) {
        return (synth(403, "Forbidden"));
    }
    if ((client.ip !~ whitelist) && vsthrottle.is_denied(client.identity, 25, 10s)) {
        return (synth(429, "Too Many Requests"));
    }
}

As you can see, unlike Nginx, Varnish has the powerful if directive, it works just like you’d expect.

Advanced Limiting Request with Nginx (or Openresty)

Nginx had the ngx_http_limit_req_module which can be used to limit request processing rate, but most people seem to only used its basic feature: limiting request rate by the remote address, like this:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    ...
    server {
        ...
        location /search/ {
            limit_req zone=one burst=5;
        }

This is the example configuration taken from Nginx’s official document, the limit_req_zone directive takes variable $binary_remote_addr as a key to limit incoming request. The key fills a zone named one, which is defined by the zone parameter, it could use up to 10m memory. And the rate parameter says that the maximum request rate of each $binary_remote_addr is 1 per second. In the search location block, we can use limit_req directive to refer the one zone, with the bursts not exceeding 5 requests.

Everything seems great, we have configured Nginx against rogue bots/spiders, right?

No! That configuration won’t work in real life, it should never used in your production environments! Take these following circumstances as examples:

  • When users access your website behind a NAT, they share the same public IP, thus Nginx will use only one $binary_remote_addr to do limit request. Hundreds of users in total could only be able access your website 1 time per second!
  • A botnet is used to crawl your website, it use different IP addresses each time. Again, in this situation, limiting by $binary_remote_addr is totally useless.

So what configuration should we use then? We need to use a different variable as the key, or even multiple variables combined together (since version 1.7.6, limit_req_zone‘s key can take multiple variables). Instead of remote address, it is better to use request HTTP headers to distinguish users apart, like User-Agent, Referer, Cookie, etc. These headers are easy to access in Nginx, they are exposed as built in variables, like $http_user_agent, $http_referer, $cookie_name, etc.

For example, this is a better way to define a zone:

http {
    limit_req_zone $binary_remote_addr$http_user_agent zone=two:10m rate=90r/m;
}

It combines $binary_remote_addr and $http_user_agent together, so different user agent behind NATed network can be distinguished. But it is still not perfect, multiple users could use a same browser, same version, thus they send same User-Agent header! Another problem is that the length of $http_user_agent variable is not fixed (unlike $binary_remote_addr), a long header could use a lot of memory of the zone, may exceeds it.

To solve the first problem, we can use more variables there, cookies would be great, since different user sends their unique cookies, like $cookie_userid, but this still leaves us the second problem. The answer it to use the hashes of variables instead.

Thers is a third-party module called set-misc-nginx-module, we can use it to generate hashes from variables. If you are using Openresty, this moule is already included. So the configuration is like this:

http {
    ...
    limit_req_zone $binary_remote_addr$cookie_hash$ua_hash zone=three:10m rate=90r/m;
    ...
    server {
        ...
        set_md5 $cookie_hash $cookie_userid;
        set_md5 $ua_hash $http_user_agent;
        ...
    }
}

It is OK we used $cookie_hash and $ua_hash in the http block before they are defined in server block. This configuration is great now.

Let’s continue with the distributed botnet problem now, we need to take $binary_remote_addr out of the key, and since those bots usually don’t send Referer header (else you can found what unique about it by yourself), we can take advantage of it. This configuration should take care of it:

http {
    ...
    limit_req_zone $cookie_hash$referer_hash$ua_hash zone=three:10m rate=90r/m;
    ...
    server {
        ...
        set_md5 $cookie_hash $cookie_userid;
        set_md5 $referer_hash $http_referer;
        set_md5 $ua_hash $http_user_agent;
        ...
    }
}

Varnish 限制访问频率

Varnish 4 仍然没有内置的访问频率限制功能,要达到这一目的需要安装 vsthrottle VMOD。

这个 VMOD 可以从这里获取:https://github.com/varnish/libvmod-vsthrottle

可以使用源码目录中的 autogen.sh 利用 autotools 先生成 configure 脚本,然后再按照“./configure; make; make install”步骤安装好这个 VMOD(如果是使用官方 rpm 安装 Varnish 的话,还需要 varnish-libs-devel 这个包才能编译)。

安装好之后就可以在 vcl 配置里面使用它了。

vcl 4.0;

import vsthrottle;

sub vcl_recv {
        set client.identity = client.ip;
        if (vsthrottle.is_denied(client.identity, 15, 10s)) {
                # Client has exceeded 15 reqs per 10s
                return (synth(429, "Too Many Requests"));
        }
}

vsthrottle 的使用比较简单,只有一个“is_denied()”函数。函数原型为:

is_denied(STRING key, INT limit, DURATION period)

第一个 key 参数和 nginx 里面的 limit_req_zone 指令里面的 key 差不多,就是作为限制频率的依据。上面的配置里面我们按照客户端地址进行了访问限制,10s 内如果超过 15 次访问则给它返回 429 状态码——Too Many Requests。但是可以看到并没有直接将 client.ip 传递给 vsthrottle.is_denied,这是因为 key 参数必须是字符串类型,而 client.ip 则是个 IP 地址类型,只能将它先用 client.identity 保存起来后才能进行传递。key 的定义很灵活,我们可以把 req.http.User-Agent 等作为key,也可以利用类型转换,或者是 regsub 函数修改字符来作为 key。

Strict-Transport-Security HTTP 头造成的重定向

Strict-Transport-Security(HSTS) HTTP 头可以告诉浏览器使用强制的更高安全控制,比如当访问到的 SSL/TLS 证书异常时,浏览器会拒绝让用户添加例外(如下图,没有 HSTS 头的时候,Firefox 会有个让用户添加例外的选项,用户可以忽略这个证书异常,继续访问网站。如果是下图的情况,用户登录了这个伪装为 onedrive 的网站,恐怕后果就很严重了),或者是让浏览器以后都默认使用 TLS 协议访问这个网站,而不用再通过 80 端口去重定向(这个时候也是很容易被攻击的)。

但有时候 HSTS 头的不当使用也会造成问题,比如一些站点的莫名无法访问——明明在地址栏输入的是 http:// 但是却老是会自动跳转到 https://,这个时候我们往往会以为在服务器端有什么重定向,但是经过检查却没有!
经过检查,Chrome 或者 Firefox 都有此问题,但是 IE 却没有,所以可以怀疑是浏览器这头做的跳转。
其实就是 HSTS 的 includeSubDomains 这段区域造成的,这段参数使得浏览器在访问每个子站点都会走 TLS 协议

解决办法:服务器上面去掉这个参数,并且在浏览器中也需要清除这个记录,Chrome 可以访问 chrome://net-internal/#hsts 这个地址,查看和删除已经存在的站点,Firefox 则只能选择忘记整个站点的记录来清除。IE 目前还不支持 HSTS(Windows 10 会支持,但还在开发当中),所以没有此问题。

Varnish: 让客户端强制刷新更新服务器缓存

从 RFC2616 的要求来看,缓存服务器是需要对客户端发过来的”Cache-Control”头作出回应的

Sometimes a user agent might want or need to insist that a cache revalidate its cache entry with the origin server (and not just with the next cache along the path to the origin server), or to reload its cache entry from the origin server. End-to-end revalidation might be necessary if either the cache or the origin server has overestimated the expiration time of the cached response. End-to-end reload may be necessary if the cache entry has become corrupted for some reason.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
其中说到了一种叫做”End-to-end reload”的:

End-to-end reload
The request includes a “no-cache” cache-control directive or, for compatibility with HTTP/1.0 clients, “Pragma: no-cache”. Field names MUST NOT be included with the no-cache directive in a request. The server MUST NOT use a cached copy when responding to such a request.

也就是说,当客户端(通常来说是浏览器)发过来”Cache-Control: no-cache”头(通常当我们按 Ctrl+f5 或者 Shift+鼠标点击刷新按钮的时候就会发出这个 HTTP 头)的时候,缓存服务器绝不能响应之前被缓存的拷贝,而是要重新去从源服务器获取一份发送给客户端。
但是在现实中往往不是这样:当我们在浏览器进行强制刷新的时候,只是将浏览器本地的缓存给清除掉了,缓存服务器的可能并没有更新。造成这个现象的原因主要有两个:

  1. 缓存服务器的性能可能满足不了这样的要求。
    在一个繁忙的缓存服务器,尤其是以磁盘作为主要缓存存储设备的服务器上面,限于磁盘IO,如果每次都必须将缓存对象彻底清除(实际上应该是可以讨巧的不去碰旧的缓存对象,然后去后端请求一个新的,然后替代旧的),即使再好的算法恐怕也无法应对大量的刷新缓存需求。
  2. 忽略客户端发过来的”Cache-Control”头,可以达到更高的缓存命中率。
    之所以要做缓存服务器,就是要让缓存率越高越好,开放让用户端去刷新缓存会降低缓存命中率。

但是现在也许该用新一点的思维来思考这个问题了:如果我们的服务器可以承受这样的刷新量,我们也许就可以让用户拥有刷新缓存服务器的能力。毕竟这是个信息迅速过期的时代,”Cache-Control: max-age=31536000″ 的作用也许已并不重要了。
我们可以用 Varnish 缓存服务器来做到这点:
前提要求是最好用纯内存作为缓存。添加如下的配置

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_recv {
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        # Force a cache miss
        set req.hash_always_miss = true;
    }
}

req.hash_always_miss 让 Varnish 去后端取新的内容,在 hash 里面替代旧的缓存对象,但是旧的并不会被立即清除,要等待它的 ttl 到了或者其它方法才会清除掉。这个做法的缺点是会在内存里面留下大量的旧的无用的缓存对象拷贝。
另一个方法是使用 ban 规则去清除缓存:

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_backend_response {
    # 后端响应添加到缓存时,把 Host 和 Url 添加到缓存里面,以方便后续可以根据 obj. 对象进行缓存清除。
    set beresp.http.X-Cache-Host = bereq.http.Host;
    set beresp.http.X-Cache-Url = bereq.url;
}

sub vcl_recv {
    # purge 地址内的客户端发过来"Cache-Control: no-cache"头,则添加一条 ban 规则,清除缓存
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        ban ("obj.http.X-Cache-Host == " + req.http.Host + " && obj.http.X-Cache-Url == " + req.url);
    }
}

sub vcl_backend_response {
        # 设置 Varnish 最长缓存保留时间为一天
        if (beresp.ttl > 1d) {
                set beresp.ttl = 1d;
        }
}

sub vcl_deliver {
    # 在发送给客户端之前,我们可以将只用于清除缓存的头去掉,没必要发给客户。
    unset resp.http.X-Cache-Host;
    unset resp.http.X-Cache-Url;
}

可以根据需求修改要不要对可以进行刷缓存的客户端 IP 进行限制。
最后,为什么要设置 Varnish 的最长保留缓存时间为一天?这是因为如果刷新请求量一旦比较大的话,会累积大量的 ban 规则(ban 规则只有当缓存里面最老的缓存对象都比他要新的时候才会被删除,否则就会一直累积着。因此,即使只有一个缓存对象保存时间为一年的话,都会导致 Varnish 累积一年的 ban 规则),这仍然还是会对性能产生很大的影响,所以为了不至于累积过多的 ban 规则,拖累 ban lurker 线程和整个 Varnish 的性能,直接设置最多保存一天就够了。(实际上,需要被缓存一天以上的东西本来就不多,可以通过 varnishtop -i TTL 查看到。)
更新:后面使用 ban 规则的方法似乎设置过最长 ttl 为一天后,还是会出现大量 ban 规则累积的情况,暂时没时间研究了。。。 🙁

少用 if … rewrite,多用 try_files

前段时间配置 Nginx 时候碰到了一个问题:Nginx 模块内部暴露出来的资源无法访问。具体来说就是比如某个模块导出了一个资源(比如 /channels-stats)提供统计信息,如下:

location /channels-stats {
# activate channels statistics mode for this location
push_stream_channels_statistics;

# query string based channel id
push_stream_channels_path $arg_id;
}

而在配置文件中还有一下配置,如果访问的文件不存在,则重定向到使用 index.php 去进行访问

if (!-e $request_filename) {
rewrite ^/(.*)$ /index.php/?s=$1 last;
break;
}

这个时候,/channels-stats 是无法被访问到的,原因应该是 /channels-stats 这样的有模块导出的资源并不属于 $request_filename 这个变量的范畴。导致实际访问的其实是 /index.php/?s=channels-stats 。

这个时候也不能用 $request_uri, $uri 等变量去替换 $request_filename 的位置,因为 if 能测试的就只是文件是否存在而已。

想了想,发现这段时间都忘了 Nginx 的 try_files 指令了,以及 if is evil 这条 Nginx 金句了,用下面这条可以替代上面的,并且可以保证  /channels-stats 仍然可以访问

try_files $uri $uri/ /index.php?s=$request_uri;

这里讲了不该用 if 的:
http://wiki.nginx.org/Pitfalls#Check_IF_File_Exists
http://wiki.nginx.org/IfIsEvil
更新:这里的问题其实是把”if (!-e $request_filename)”测试放到 location / 下面就很好解决了的,不知道当时为什么脑子短路了,没有发现。。。

不负责任的豆瓣安全提醒

因为最近的 heartbleed 漏洞,邮箱里面收到了不少网站发过来的提示修改密码的邮件。而如以往,国内网站很少发。但是今天豆瓣网发了一封提示邮件

但是这个邮件未免发得太不负责。第一,OpenSSL 本身不是网络安全标准协议;第二,“你在其他时间的登录行为不会受到影响”,真的吗?豆瓣你真的这么确定?如果有人很久以前就已经发现和利用 heartbleed 漏洞,你也敢打包票漏洞在大规模曝光之前的登录是完全不受影响,未免也太轻浮。安全问题这东西,还是宁可信其有吧

The pain of using random generated complex passwords

Nowadays all major internet browsers come with password auto-sync feature, some even had random complex password generator included. that is great! You finally can feel “secured” about your password, until it actually comes with a pain in the ass.

When you use the same password to login some website’s mobile app, if the password just happened to be a random complex password, it’s diffcult to input it. On all the touch screen keyboard, when you have to input a string like “KU=u3B:Kt_mo”, you get mad. So it is easily you miss typed a charactor or you type something wrong. Oops! can’t let you login.

It gets worse, some website decide to have aggressive protection against brutal force login try to login users’ account, so when you typed wrong password multiple times, you got banned or worst: force you to reset password!

Thank you instagram for force me to reset my password