- 知道客户证书是什么,并了解为什么他需要在移动Safari上使用Websocket;
- 希望将网络服务发布给少数人或只发布给自己;
- 认为一切都已经由某人完成,并希望使世界更加方便和安全。
Web套接字的历史始于大约8年前。以前,使用长http请求(实际上是答复)形式的方法:用户的浏览器向服务器发送了一个请求,并等待他回答一些,然后再次连接并等待。但是随后出现了网络套接字。
几年前,我们开发了自己的纯PHP实现,因为这是数据链接层,所以不知道如何使用https请求。不久前,几乎所有Web服务器都学会了通过https代理请求并支持连接:升级。
发生这种情况时,Web套接字几乎成为SPA应用程序的默认服务,因为它可以方便地向用户提供服务器主动性的内容(发送来自其他用户的消息或下载其他人正在编辑的图像,文档,演示文稿的新版本) ...
尽管Clientert已经存在了很长时间,但它仍然受支持不佳,因为尝试绕过它会产生很多问题。并且(可能是:稍微_smiling_face :),因此IOS浏览器(除Safari之外的所有浏览器)都不想使用它并询问本地证书存储。与登录/通过或ssh密钥或对正确的端口进行防火墙相比,证书具有许多优点。但这不是重点。
在iOS上,安装证书的过程非常简单(并非没有具体说明),但通常是根据说明完成的,该说明在网络上非常多,并且仅适用于Safari浏览器。不幸的是,Safari不知道如何将客户端证书用于Web套接字,但是Internet上有很多说明如何制作这样的证书,但是实际上这是无法实现的。
为了理解websocket,我们使用以下概述:问题/假设/解决方案。
问题:在 IOS和其他包含证书支持的应用程序的移动Safari浏览器上将请求代理到受客户端证书保护的资源时,不支持websocket。
假设:
- 可以配置这样的例外,以对内部/外部代理资源的Web套接字使用证书(知道它们将不可用)。
- 对于Web套接字,您可以使用在常规(非Web套接字)浏览器请求期间生成的临时会话建立唯一的安全连接。
- 可以使用一台代理Web服务器(仅内置模块和功能)实现临时会话。
- 临时会话令牌已作为现成的Apache模块实现。
- 可以通过逻辑设计交互结构来实现临时会话令牌。
部署后可见状态。
工作目的:应该可以通过IOS上的移动电话进行服务和基础架构的管理,而无需附加程序(例如VPN),并且统一而安全。
附加目标:节省时间和资源/电话流量(某些没有Web套接字的服务会生成不必要的请求),同时加快移动Internet上的内容交付。
怎么检查?
1.打开页面:
— , https://teamcity.yourdomain.com Safari ( ) — -.
— , https://teamcity.yourdomain.com/admin/admin.html?item=diagnostics&tab=webS…— ping/pong.
— , https://rancher.yourdomain.com/p/c-84bnv:p-vkszd/workload/deployment:danidb:ph…-> viewlogs — .
2.或在开发人员的控制台中:
假设测试:
1.可以为内部/外部代理资源的Web套接字配置使用证书的此类例外(知道它们将不存在)。
在这里找到2个解决方案:
a)在一级
<Location sock*> SSLVerifyClient optional </Location>
<Location /> SSLVerifyClient require </Location>
更改访问级别。
此方法具有以下细微差别:
- 在对代理资源的请求(即请求后握手)之后检查证书。这意味着代理将首先加载,然后切断对受保护服务的请求。这是不好的,但不是关键。
- 在http2中。它仍处于草稿中,浏览器制造商不知道如何实现有关tls1.3 http2 post握手的#info(现在无法正常工作)实施RFC 8740“在HTTP / 2中使用TLS 1.3”;
- 目前尚不清楚如何统一此处理。
b)在基本级别上,允许没有证书的ssl。
SSLVerifyClient require => SSLVerifyClient可选,但这会降低代理服务器的保护级别,因为这样的连接将在没有证书的情况下进行处理。但是,您可以使用以下指令进一步拒绝对代理服务的访问:
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteRule .? - [F]
ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"
有关更多信息,请参见有关ssl的文章:Apache服务器客户端证书身份验证
这两个选项都经过测试,选择了选项“ b”是为了实现与http2协议的通用性和兼容性。
为了完成对该假设的验证,需要进行大量配置实验,并检查了结构:
if = require = rewrite
我们得到以下基本构造:
SSLVerifyClient optional
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule .? - [F]
#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"
#websocket for safari without cert auth
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
...
#
SSLUserName SSl_PROTOCOL
</If>
</If>
给定证书持有者的现有授权,但证书丢失,我不得不添加一个不存在的证书持有者作为可用的SSl_PROTOCOL变量之一(而不是SSL_CLIENT_S_DN_CN),有关更多详细信息,请参阅文档:
Apache Module mod_ssl2
。对于Web套接字,您可以建立一个唯一的安全连接使用在常规(不是Web套接字)浏览器请求期间生成的临时会话。
根据以前的经验,您需要在配置中添加一个附加部分,以便在常规(而非Web套接字)请求期间,为Web套接字连接准备临时令牌。
# ookie
<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
Header set Set-Cookie "websocket-allowed=true; path=/; Max-Age=100"
</If>
</If>
# Cookie -
<source lang="javascript">
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
#check for exists cookie
#get and check
SetEnvIf Cookie "websocket-allowed=(.*)" env-var-name=$1
#or rewrite rule
RewriteCond %{HTTP_COOKIE} !^.*mycookie.*$
#or if
<If "%{HTTP_COOKIE} =~ /(^|; )cookie-name\s*=\s*some-val(;|$)/ >
</If
</If>
</If>
测试表明它有效。可以通过用户浏览器传输cookie。
3.临时会话可以使用一台代理Web服务器(仅内置模块和功能)实现。
如前所述,Apache具有很多核心功能,可让您创建条件语句。但是,我们需要一种在用户浏览器中保护我们的信息的方法,因此我们设置要存储的内容和存储的内容以及将使用的内置函数:
- 我们需要一个无法进行简单解码的令牌。
- 我们需要一个令牌,在令牌中可以保护服务器的过时和检查过时的能力。
- 您需要一个与证书所有者关联的令牌。
这需要散列函数,盐和令牌过期的日期。根据Apache HTTP Server文档中的表达式,我们提供了现成的sha1和%{TIME}。
结果是以下构造:
# , websocket
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
SetEnvIf Cookie "zt-cert-sha1=([^;]+)" zt-cert-sha1=$1
SetEnvIf Cookie "zt-cert-uid=([^;]+)" zt-cert-uid=$1
SetEnvIf Cookie "zt-cert-date=([^;]+)" zt-cert-date=$1
# , env- , ( , , )
<RequireAll>
Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1}
Require expr %{env:zt-cert-sha1} =~ /^.{40}$/
</RequireAll>
</If>
</If>
# , websocket
<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
SetEnvIf Cookie "zt-cert-sha1=([^;]+)" HAVE_zt-cert-sha1=$1
SetEnv zt_cert "path=/; HttpOnly;Secure;SameSite=Strict"
# ,
Header add Set-Cookie "expr=zt-cert-sha1=%{sha1:salt1%{TIME}salt3%{SSL_CLIENT_S_DN_CN}salt2};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
Header add Set-Cookie "expr=zt-cert-uid=%{SSL_CLIENT_S_DN_CN};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
Header add Set-Cookie "expr=zt-cert-date=%{TIME};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
</If>
</If>
该目标已经实现,但是服务器过时了(您可以使用一年前的cookie),这意味着令牌虽然对于内部使用是安全的,但对于工业(批量)使用却是不安全的。
4.临时会话令牌已经作为现成的Apache模块实现。
从上一次迭代开始,仍然存在一个重要的问题-无法控制令牌过时。
我们正在寻找一个可以做到这一点的现成模块,根据:apache token json two factor auth
是的,有现成的模块,但是所有模块都与特定操作相关,并且具有会话开始和其他cookie形式的工件。也就是说,暂时不会。
我们花了五个小时进行搜索,但未给出具体结果。
5.临时会话令牌可以通过逻辑设计交互结构来实现。
现成的模块太复杂了,因为我们只需要几个功能。
同时,日期的问题是Apache内置函数不允许从将来生成日期,并且在检查内置函数中的过时值时,没有数学上的加减法。
也就是说,您不能写:
(%{env:zt-cert-date} + 30) > %{DATE}
只能比较两个数字。
在寻找Safari问题的解决方法时,发现了一篇有趣的文章:使用客户端证书保护HomeAssistant(可与Safari / iOS一起使用)
它描述了Nginx的Lua代码示例,事实证明,它非常重复了我们之前已经实现的那部分配置的逻辑。除了使用安排盐用于哈希的hmac方法外(在Apache中找不到)。
显而易见,Lua是一种逻辑清晰的语言,可以为Apache做一些简单的事情:
研究了Nginx和Apache的区别:
以及Lua语言制造商提供的可用功能:
22.1-日期和时间
找到了一种在小型Lua文件中设置env变量的方法,以便设置从未来开始检查当前日期的日期。
这是一个简单的Lua脚本的样子:
require 'apache2'
function handler(r)
local fmt = '%Y%m%d%H%M%S'
local timeout = 3600 -- 1 hour
r.notes['zt-cert-timeout'] = timeout
r.notes['zt-cert-date-next'] = os.date(fmt,os.time()+timeout)
r.notes['zt-cert-date-halfnext'] = os.date(fmt,os.time()+ (timeout/2))
r.notes['zt-cert-date-now'] = os.date(fmt,os.time())
return apache2.OK
end
这样一来,所有这些工作总可以实现,可以优化cookie的数量,并在旧的cookie(令牌)过期之前经过一半的时间后替换令牌:
SSLVerifyClient optional
#LuaScope thread
#generate event variables zt-cert-date-next
LuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early
# - , webscoket
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule .? - [F]
#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"
#websocket for safari without certauth
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
SetEnvIf Cookie "zt-cert=([^,;]+),([^,;]+),[^,;]+,([^,;]+)" zt-cert-sha1=$1 zt-cert-date=$2 zt-cert-uid=$3
<RequireAll>
Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1}
Require expr %{env:zt-cert-sha1} =~ /^.{40}$/
Require expr %{env:zt-cert-date} -ge %{env:zt-cert-date-now}
</RequireAll>
#
SSLUserName SSl_PROTOCOL
SSLOptions -FakeBasicAuth
</If>
</If>
<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
SetEnvIf Cookie "zt-cert=([^,;]+),[^,;]+,([^,;]+)" HAVE_zt-cert-sha1=$1 HAVE_zt-cert-date-halfnow=$2
SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1
Define zt-cert "path=/;Max-Age=%{env:zt-cert-timeout};HttpOnly;Secure;SameSite=Strict"
Define dates_user "%{env:zt-cert-date-next},%{env:zt-cert-date-halfnext},%{SSL_CLIENT_S_DN_CN}"
Header set Set-Cookie "expr=zt-cert=%{sha1:salt1%{env:zt-cert-date-next}sal3%{SSL_CLIENT_S_DN_CN}salt2},${dates_user};${zt-cert}" env=!HAVE_zt-cert-sha1-found
</If>
</If>
SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1
,
SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge env('zt-cert-date-now') && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1
因为LuaHookAccessChecker仅在基于Nginx的信息进行访问检查后才被激活。
链接到图像源。
还有一点。
通常,在Apache配置(可能是Nginx)中编写指令的顺序无关紧要,因为最终所有内容都将根据用户的请求顺序进行排序,这与编制Lua脚本的方案相对应。
完成:
实施后可见的状态(目标):
可通过IOS上的移动电话进行服务和基础架构管理,而无需附加程序(VPN),统一和安全。
该目标得以实现,WebSocket可以正常工作,并且安全性不亚于证书。