使用OpenSSL为Nginx签发自签名证书

本文介绍使用openssl命令为nginx生成自签名的证书。注:仅用于本地测试使用,自签名证书不被浏览器认可,不能应用于生产环境。

一、生成RSA私钥
openssl genrsa -out local.lyz810.com.key 2048
上述命令在当前目录下使用RSA2048算法生成一个文件名为local.lyz810.com.key的pem格式的私钥。

二、使用生成好的私钥签发证书
openssl req -new -x509 -days 3650 -key local.lyz810.com.key -out lyz810.com.crt
输入命令后,会提示输入一些信息,由于是测试使用,可以任意填写。
配置好后添加到nginx上,浏览器打开会提示证书错误,选择继续访问即可。

三、openssl genrsa用法说明
genrsa的语法如下:

openssl genrsa [-help] [-out filename] [-passout arg] [-aes128] [-aes192] [-aes256] [-camellia128] [-camellia192] [-camellia256] [-des] [-des3] [-idea] [-f4] [-3] [-rand file(s)] [-engine id] [numbits]

参数详解:
-help:打印帮助信息
-out filename:生成文件的名称
-passout arg:生成的文件的短语密码源
-aes128|-aes192|-aes256|-camellia128|-camellia192|-camellia256|-des|-des3|-idea:这些选项会在输出私钥文件前使用该参数的加密算法将私钥加密。如果没有这些参数中的一个,则生成的私钥没有密码保护。如果没有通过passout指定短语密码,则会提示输入短语密码用于加密。
-F4|-3:使用的公开指数,值为65537或3,默认为65537
-rand files(s):一个或多个包含用于随机数发生器播种的随机数据的文件或EGD socket。多个文件之间的分隔符根据系统不同而不同,Windows中是分号“;”,OpenVMS中是逗号“,”,其他系统中是冒号“:”。
-engine id:通过唯一的id指定一个引擎。
-numbits:私钥长度,默认是512位,处于安全建议1024以上,不过测试环境可以忽略。

四、openssl req用法说明
req的语法如下:

openssl req [-help] [-inform PEM|DER] [-outform PEM|DER] [-in filename] [-passin arg] [-out filename] [-passout arg] [-text] [-pubkey] [-noout] [-verify] [-modulus] [-new] [-rand file(s)] [-newkey rsa:bits] [-newkey alg:file] [-nodes] [-key filename] [-keyform PEM|DER] [-keyout filename] [-keygen_engine id] [-[digest]] [-config filename] [-multivalue-rdn] [-x509] [-days n] [-set_serial n] [-newhdr] [-extensions section] [-reqexts section] [-utf8] [-nameopt] [-reqopt] [-subject] [-subj arg] [-batch] [-verbose] [-engine id]

参数详解:
-help:打印帮助信息
-inform PEM|DER:私钥的格式,默认为PEM,第一步到处的证书即PEM格式,因此此参数省略。
-outform PEM|DER:输出的格式,含义同上,默认PEM。
-in filename:指定读取请求的文件名,只有在没有指定-new和-newkey参数时有效。如果没有指定文件名,则从标准输入中获取。
-passin arg:输入文件的密码源
-out filename:输出文件名
-passout arg:输出文件的密码源
-text:打印请求文件的文本格式
-subject:打印请求文件的主题(如果有-x509则为证书的主题)
-pubkey:输出公钥
-noout:禁止输出经过编码版本的请求
-modulus:打印请求中包含的公钥系数值
-verify:验证请求的签名
-new:该选项生成一个新的证书请求。它会让用户输入相关域的信息。如果没有key选项则会根据配置文件生成一个私钥。
-rand file(s):参见genrsa中该参数的说明
-newkey arg:该选项创建一个新证书请求和一个新的私钥。参数使用以下几种形式之一,rsa:nbits,nbits为比特位数,用于生成RSA密钥长度,如果忽略nbits,则使用配置文件中的默认大小。
其他所有算法支持-newkey alg:file格式,file是算法参数文件,通过genpkey -genparam命令或X.509证书适当的秘钥算法。
param:file:使用参数文件或证书文件生成的密钥文件,算法取决于参数。
algname:file:使用algname算法和file参数文件。
dsa:filename:使用filename文件中的参数生成DSA密钥。
-pkeyopt opt:value:设置公钥的选项opt的值value。
-key filename:指定私钥的文件名,私钥格式默认为PEM。
-keyform PEM|DER:指定私钥的格式
-keyout filename:在指定文件中写入新创建的私钥,如未指定,则使用配置文件中的设置。
-nodes:该参数指定后,如果生成私钥则不加密。
-[digest]:用于签名请求信息的摘要
-config filename:配置文件的位置,可选项。
-subj arg:设置主题,格式/type0=value0/type1=value1/type2=…,字符可以用\转义,没有空格会被跳过。
-multivalue-rdn:该选项会使-subj参数解释为完全支持多值RDN,例如/DC=org/DC=OpenSSL/DC=users/UID=123456+CN=John Doe,如果没有使用该参数,则UID的值是123456+CN=John Doe
-x509:该参数说明输出是一个自签名证书而不是证书请求。
-days n:有效期天数,默认为30天。
-set_serial n:输出自签名证书用的序列号,可以是十进制的值或0x开头的十六进制的值。
其他参数略。

nginx代理入门

本文通过几个的实例,介绍nginx配置代理的几种方式,以及全局代理的配置。

注:本文中所有实例均可以直接使用,不需要改变任何配置(包括域名,域名解析到127.0.0.1,注意:由于https证书自动续期的需要,仅国内线路的*.local.lyz810.com指向127.0.0.1,国外线路已变更为服务器的IP地址
原始站点(被代理站点)
测试后面所有实例时,请确保该源站配置存在

server {
    listen 80;
    server_name origin.local.lyz810.com;
    default_type text/plain;
    location / {
        return 200 "This is origin.local.lyz810.com$request_uri";
    }

    location =/setcookie/ {
        add_header Set-Cookie "testcookie=123; domain=origin.local.lyz810.com; path=/";
        return 200 "Please find result in response header";
    }

    location =/showcookie/ {
        return 200 $http_cookie;
    }
}

当访问origin.local.lyz810.com的任意页面,都可以看到This is origin.local.lyz810.com后面跟着url

实例一:
通过其他域名进行代理(如目前线上访问google.lyz810.com即可实现代理google.com.hk)
代理配置:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location / {
        proxy_pass http://origin.local.lyz810.com/;
    }
}

访问proxy.local.lyz810.com的任意页面,会对应访问origin.local.lyz810.com的页面
如访问proxy.local.lyz810.com/test,返回This is origin.local.lyz810.com/test

实例二:
代理服务器上的url与源站url存在差异,访问proxy.local.lyz810/test/下的所有页面,要求返回origin.local.lyz810.com/hello/对应页面(test改为源站的hello)
代理配置:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location /test/ {
        proxy_pass http://origin.local.lyz810.com/hello/;
    }
}

请注意hello后面的/,这个是必须有的,nginx在处理url时,会将location匹配的路径从访问url中去掉后拼接到proxy_pass指令后面
例如访问proxy.local.lyz810.com/test/abc.html,匹配location为/test/,把匹配的部分从url中去掉,还剩abc.html,直接拼接到proxy_pass后面,即http://origin.local.lyz810.com/hello/abc.html
一种典型的错误写法:

    ...
    location /test {
        proxy_pass http://origin.local.lyz810.com/hello/;
    }
    ...

上面的写法语法上没有问题,但是跟需求不匹配。也许你会发现,这个错误的写法在访问proxy.local.lyz810.com/test/abc.html时仍然返回This is origin.local.lyz810.com/hello//abc.html(nginx会对url进行标准化处理,两个/跟一个/访问的是同一个资源)
这是由于hello以/结尾,nginx会将/test后面的内容直接接到proxy_pass最后,造成2个/
但如果访问proxy.local.lyz810.com/test123/abc.html则会返回http://origin.local.lyz810.com/hello/123/abc.html,这个与预期不符。

实例三:
源站要设cookie,由于域名不同,跨域无法设置cookie,通过nginx可以解决这个问题。如访问origin.local.lyz810.com/setcookie/后会在origin.local.lyz810.com域下根目录种一个cookie,现在希望通过代理,访问源站,并在proxy.local.lyz810.com的/test上种上相同的cookie,配置如下:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location / {
        proxy_pass http://origin.local.lyz810.com/;
        proxy_cookie_domain origin.local.lyz810.com $host;
        proxy_cookie_path / /test;
    }
}

访问proxy.local.lyz810.com/setcookie/,查看响应头(Set-Cookie:testcookie=123; domain=proxy.local.lyz810.com; path=/test)。
这种使用场景是使用一个域名代理另一个域名的页面,并可以代理认证信息。
我们知道,浏览器是按照域名携带cookie的,所以访问proxy.local.lyz810.com的时候只能带着proxy.local.lyz810.com的cookie访问源站,显然用户不可能自己在proxy.local.lyz810.com上添加cookie,所以我们需要把访问源站登录时,设置的cookie转换为代理域下的cookie,这样访问代理域就和访问源站域完全一样了,相当于镜像站。
通常做完整镜像站时,只需要将cookie的domain修改为代理域即可,path保持一致不需要配置。

实例四:
有时候,页面上的js会对域名做判断,上面所有的方法只能骗过源站的服务器,而不能骗过浏览器的location.hostname,这种情况下,nginx是无法完美解决,如果是开发调试,可以通过配置host来实现。
例如,我的项目地址是demo.lyz810.com,我负责前端开发,需要调用后端接口(后端接口都在/fetch/下)demo.lyz810.com/fetch/api.php?action=getJson(完全可以通过实例三的方法设置另一个域,然后做cookie共享,但我不喜欢开发和线上访问的不一样),那么可以通过下面的方式来实现:
1.设置host 127.0.0.1 demo.lyz810.com
2.nginx配置如下:

server {
    listen 80;
    server_name demo.lyz810.com;
    default_type text/html;
    location /fetch/ {
        proxy_pass http://133.130.97.238/fetch/;
        proxy_set_header Host $host;
    }

    location / {
        return 200 "Please open console to see result<script src='/fetch/api.php?action=getJson'></script>";
    }
}

访问demo.lyz810.com,打开浏览器的控制台,看到发送请求/fetch/api.php返回了信息(这个是线上真实的接口数据),本例中使用了return指令直接返回了一段html代码,正常开发时,此处一般是由root指令指定的静态文件目录,也就是我们的工程目录。
几点说明:
1.proxy_pass需要填写服务器的IP地址,因为本机设置了host,并且nginx在本机部署,所以会受hosts的影响
2.proxy_set_header是nginx访问线上服务器时携带的请求头,因为上面写的是IP,而服务器不能直接通过IP访问(服务器上挂了那么多站点,你用IP访问,它也不知道返回哪个站点),加上Host这个请求头,服务器就知道返回哪个站点下的资源。

实例五:
终极代理大法一,访问本机8888端口,代理任何网站(需要配hosts文件)
代理配置:

server {
    listen 8888;
    server_name _ default;
    resolver 119.29.29.29;
    location / {
        proxy_pass https://$host$request_uri$is_args$args;
    }
}

绑定hosts:127.0.0.1 www.sogou.com,访问http://www.sogou.com:8888/web?query=lyz810&_asf=www.sogou.com可以看到结果,这里用80端口也是完全可以的,hosts文件只要指向本机就可以,如果是局域网内其他机器访问,可以直接配置hosts文件指向nginx服务器的IP,可以通过nginx代理任意站点。
注意:server_name 后面的default表示如果没有匹配到其他的域名,就用当前server,nginx配置文件中同一个端口最多有1个default的server,否则后面设置的default会无效。proxy_pass中域名使用了变量,所以需要配置resolver,即dns服务器地址,这里用的是腾讯云提供的公共dns。此例中代理走https协议,因为目前大部分网站支持https,这里暂且用https,后面有更灵活配置方式。
这种配置的一种应用是共享账号,可以登录某个网站后,将cookie记下来,配置在服务端的proxy_set_header Cookie …中,其他人访问该代理服务器,不需要登录就可以访问公共账号登录的内容。

实例六:
全局代理,无需配置hosts

server {
    listen 80;
    server_name proxy.local.lyz810.com;
    resolver 119.29.29.29;

    location ~ ^/([^/]*)(.*)$ {
        proxy_pass $scheme://$1$2$is_args$args;
    }
}

注意:这种方法只适合于代理任意已知地址,由于网页中很少使用相对地址,所以一般不能直接通过这种代理访问网站,仅限开发调试使用。
这里给出的例子并不完美,转发使用了$scheme根据访问的协议进行同协议转发,但只监听了80端口,所以此处一定为http,可以同时监听443并开启ssl,同时设置证书,已超出本文的内容,不再赘述。
这种配置也有应用场景,目前用于统一代理某些已知接口,不需要每次修改配置文件,只需要写好正确的地址,即可拿到数据。
线上应用在proxy.lyz810.com上,通过此服务器进行跨域管理,所有需要跨域访问的内容交给服务器去访问,返回的结果通过add_header增加跨域头,这样就可以让应用轻松的跨域,具体即实现方法以后介绍。

Nginx配置WordPress与HSTS

本文介绍HSTS基本概念,以及在nginx上配置HSTS的方法,配置站点基于WordPress。

本博客(https://blog.lyz810.com)已开启HSTS,读者可以尝试访问http协议,并观察浏览器的行为。
一、背景介绍
HSTS(HTTP Strict Transport Security)是HTTP严格传输安全,它是全站HTTPS时的一个更安全的策略,并且对网站性能有一定的优化。
全站HTTPS是一个必然的趋势,目前全站HTTPS通常的做法是同时开启80和443端口,当请求访问的是http协议时,通过rewrite将请求301重定向到https协议的对应URI。这么做的缺点是当用户访问http协议的页面时,总会有一次301重定向,增加网络请求以及服务器负担。
使用HSTS后,浏览器在第一次访问http协议时,会通过rewrite重定向到https协议,并根据https协议头中设定的相关响应头缓存下HSTS配置,下次用户再访问该站点下的任意http页面,浏览器会自动通过307跳转到https对应的URI上,不需要向服务器多发送一次请求。
HSTS配置方法如下:
1.当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。
2.Strict-Transport-Security有3个参数,max-age表示有效期(如max-age=31536000表示未来的一年内,访问该域名会强制使用https,其中数字表示一年的秒数);includeSubDomains表示子域名也强制使用https;preload主要用于加入preload列表使用。
preload列表是一个站点的列表,他将会被通过硬编码写入 Chrome 浏览器中,列表中的站点将会默认使用 HTTPS 进行访问,此外,Firefox 、Safari 、IE 11 和 Edge 也同样一份 HSTS 站点列表,其中包括了 Chrome 的列表
加入preload列表的条件:

  • 有一张有效的证书
  • 重定向所有的 HTTP 流量到 HTTPS ( HTTPS ONLY )
  • 全部子域名的流量均通过 HTTPS ,如果子域名的 www 存在的话也同样需要通过 HTTPS 传输。
  • 为https协议添加的响应头Strict-Transport-Securit内容必须满足:max-age必须大于18周,必须指定includeSubDomains和preload,如果从当前https站点重定向到其他的站点,那个站点也必须启用HSTS

加入HSTS preload list:https://hstspreload.appspot.com/
二、Nginx配置

server {
  ......
  if ( $https != "on" ) {#https变量的值在https协议下为on,否则为空字符串
     rewrite ^(.*) https://$host$1 permanent;#301定向到https协议的相同URI,主要是首次访问本站的请求使用
     break;
  }

  location / {
     try_files $uri $uri/ /index.php$args;#此行是WordPress使用的,它会将请求都交给/index.php处理
     add_header Strict-Transport-Security max-age=86400000;#添加响应头,过期时间1000天,即1000天之内,浏览器会自动将本站http协议转为https协议
  }

  location ~ \.php$ {
     fastcgi_pass localhost:9000;#fastcgi监听9000端口,其他fastcgi配置写在了公用配置文件中
     add_header Strict-Transport-Security max-age=86400000;#由于正则的优先级更高,所以所有动态的页面均会走这里,需要添加响应头
   }
}

三、使用HSTS的弊端
用户只要访问一次站点,就会在max-age指定的时间内,强制访问https协议,在此期间,如果网站https出现了问题(如证书配置错误),用户就无法进行访问,没有配置HSTS时,证书错误用户可以选择忽略错误继续访问。
如果想要在过期时间内再次开启http访问,是无法做到的(对没有访问过站点的用户可行,只要访问过,过期时间前用户不能访问http协议),如果加入到HSTS Preload List中,就更没有办法启用http访问。
四、浏览器支持度
Chromium和Google Chrome从4.0.211.0版本开始支持HSTS
Firefox 4及以上版本
Opera 12及以上版本
Safari从OS X Mavericks起
Internet Explorer和Microsoft Edge从Windows 10开始支持

解决openssl升级到1.1.0后shadowsocks服务报错问题

本文适用于解决openssl升级到1.1.0以上版本,导致shadowsocks2.8.2启动报undefined symbol: EVP_CIPHER_CTX_cleanup错误。

最近将openssl升级到了1.1.0b版本,编译之后shadowsocks无法启动,报错如下:

Traceback (most recent call last):
  File "/usr/bin/ssserver", line 9, in 
    load_entry_point('shadowsocks==2.8.2', 'console_scripts', 'ssserver')()
  File "/usr/lib/python2.7/site-packages/shadowsocks/server.py", line 34, in main
    config = shell.get_config(False)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 262, in get_config
    check_config(config, is_local)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 124, in check_config
    encrypt.try_cipher(config['password'], config['method'])
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 44, in try_cipher
    Encryptor(key, method)
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 83, in __init__
    random_string(self._method_info[1]))
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 109, in get_cipher
    return m[2](method, key, iv, op)
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 76, in __init__
    load_openssl()
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 52, in load_openssl
    libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 373, in __getattr__
    func = self.__getitem__(name)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 378, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /usr/local/ssl/lib/libcrypto.so.1.1: undefined symbol: EVP_CIPHER_CTX_cleanup
shadowsocks start failed

这个问题是由于在openssl1.1.0版本中,废弃了EVP_CIPHER_CTX_cleanup函数,如官网中所说:

EVP_CIPHER_CTX was made opaque in OpenSSL 1.1.0. As a result, EVP_CIPHER_CTX_reset() appeared and EVP_CIPHER_CTX_cleanup() disappeared. EVP_CIPHER_CTX_init() remains as an alias for EVP_CIPHER_CTX_reset().

EVP_CIPHER_CTX_reset函数替代了EVP_CIPHER_CTX_cleanup函数
EVP_CIPHER_CTX_reset函数说明:

EVP_CIPHER_CTX_reset() clears all information from a cipher context and free up any allocated memory associate with it, except the ctx itself. This function should be called anytime ctx is to be reused for another EVP_CipherInit() / EVP_CipherUpdate() / EVP_CipherFinal() series of calls.

EVP_CIPHER_CTX_cleanup函数说明:

EVP_CIPHER_CTX_cleanup() clears all information from a cipher context and free up any allocated memory associate with it. It should be called after all operations using a cipher are complete so sensitive information does not remain in memory.

可以看出,二者功能基本上相同,都是释放内存,只是应该调用的时机稍有不同,所以用reset代替cleanup问题不大。

修改方法:

  1. 用vi打开文件:vi /usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py
  2. 跳转到52行(shadowsocks2.8.2版本,其他版本搜索一下cleanup)
  3. 进入编辑模式
  4. 将第52行libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
    改为libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,)
  5. 再次搜索cleanup(全文件共2处,此处位于111行),将libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
    改为libcrypto.EVP_CIPHER_CTX_reset(self._ctx)
  6. 保存并推出
  7. 启动shadowsocks服务:service shadowsocks start

之后我们就可以继续愉快的、科学的畅游互联网了。
提示:openssl1.1.0目前兼容性很不好,大部分的软件都不支持
目前支持的有nginx-1.11.5、curl-7.50.3
不支持的有PHP-7.0.12、openssh-7.3p1
所以如果决定使用openssl1.1.0需要考虑很多兼容问题,必须保留1.0.2或1.0.1(不推荐,存在一些已知漏洞,最重要的是如果服务器要开http2,由于新版chrome必须使用ALPN的限制,只有1.0.2版本支持ALPN,所以必须升级到1.0.2)版本以便编译其他程序。

在CentOS7上的nginx中部署Let’s Encrypt免费证书

本文介绍如何在CentOS7上的nginx中部署Let‘s Encrypt免费证书。

之前一直使用的是沃通免费证书,最近看到网上的一些消息,担心Firefox和Chrome会取消它的根证书,所以准备逐步替换为Let’s Encrypt免费证书。目前已在https://demo.lyz810.com中使用,未来可能会逐步将其他域替换为Let’s Encrypt免费证书。
下面将操作步骤记录一下,以便后续替换时查阅。

一、安装certbot
文档:https://certbot.eff.org/#centosrhel7-nginx
$ sudo yum install epel-release
$ sudo yum install certbot

二、为域名申请一个证书
-w后面是站点根目录
-d后面是站点域名,如果多个域名,可以使用多个-d参数,每个-d参数跟一个域名,-d之间用空格分开
certbot certonly --webroot -w /website/lyz810-main/demo/ -d demo.lyz810.com

1.提示输入邮箱,用于紧急通知以及密钥恢复
2.阅读文档,选Agree即可

如果成功证书和私钥会保存在/etc/letsencrypt/live/demo.lyz810.com/中

三、nginx配置证书
ssl_certificate /etc/letsencrypt/live/demo.lyz810.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/demo.lyz810.com/privkey.pem;
重启nginx服务器

四、证书自动续期
证书有效期为90天,所以需要写一个定时任务

#minute   hour    day  month  week    command
0         0,12    *    *      *       certbot renew > /var/log/certbot.log & echo certbot last renew at `date` >> /var/log/certbot.log

在每天0点和12点会更新一次证书,并将结果保存到/var/log/certbot.log日志中。
注意:
如果设置cron或systemd任务,建议一天执行两次(如果没有到需要更新的时间段内,运行此命令不会更新证书)