连上隧道,打开同一个书签,被允许的网站给出的回应与在办公室时一模一样。这是大多数远程访问方案悄悄依赖的假设,而它只在三件事同时成立时才站得住。这三件事没有一件是 VPN 客户端的功能,也没有任何 DNS 记录能提供其中任何一件。当书签在家里失败时,浏览器只会给出同一个笼统错误,无论原因是缺少路由、未获批准的出口地址,还是替换了它所上报来源的代理。把这三者当成一个问题,正是小改动变成整整一周扩白名单的原因。

经得起真实办公网络考验的设计,比这个假设更窄:一张明确的三行决策表、一个无法绕过的执行点,以及一项能证明点什么的拒绝测试。先拆掉这个假设,因为其后的一切都取决于知道是哪一层在错误地表述来源。

神话:连上不等于走对路由

隧道建立起来,客户端为某些前缀安装了路由。连接能做的就这些。一个不相关的 HTTPS 请求是否经过隧道、到达时看起来来自哪个公网地址,是之后由其他组件做出的决定:路由表选路径,出口选来源。

所以,同一台笔记本、同一份浏览器配置、同一个书签,在办公室和在餐桌上结果不同。在办公室,请求经由网站运营方认可的、有人维护的公网地址发出。走全隧道时,它可能从 VPN 出口出去,那是另一个地址,在没人把它加进去之前属于未获批准。走拆分隧道时,它可能根本不进隧道。这三条路径里只有两条曾是站点所有者约定的一部分,第三条不是靠多加一条 allow 就能补上的缺陷。

三种请求状态,两个获批出口

动防火墙之前先把矩阵写下来,因为表才是设计,规则只是它的一种实现。下面这些地址属于文档保留段;请换成你自己维护的值,并放在站点所有者能审计的地方。

Request pathSource the site seesIntended decision
Office, no tunnelOffice public exitAdmit to the application login
Remote, approved full tunnelApproved VPN egressAdmit to the same login
Remote, ordinary connectionNeither approved addressDeny at the network boundary

恰恰是中间那一行让这些方案大多失效。算数的是执行设备看到的地址,不是笔记本在隧道内持有的私有地址,也不是它的局域网地址。10.8.0.6 这样的值对公网站点毫无意义,192.168.1.44 也一样。隧道地址只是隧道编址的细节,不是站点能验证的凭据。

如果你的 VPN 提供不了稳定且有记录可查的出口,矩阵就没有有效的中间一行,再多白名单也造不出来。此时诚实的选项是:经由明确的私有路由才能到达的私有站点,或者在应用前面加一道识别身份的门禁。两者与公网白名单是另一种约定,也都比一份长到碰巧能用的名单更容易向审计者解释。

在做出决定的地方读取来源

把限制放在终结请求的那台设备上,并确认没有任何备用源路径绕过它。Nginx 访问模块按顺序评估 allow 与 deny 指令,第一条匹配者胜出,而从外围块继承的规则在 location 内依然生效。同一个位置里窄 allow 挨着宽 allow,通常意味着由宽的那条决定。改动其中哪怕一行之前,先读完整份生效配置。

server {
    listen 443 ssl;
    server_name intranet.example.com;

    set_real_ip_from 10.20.0.10;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    location /app/ {
        allow 203.0.113.24;    # office public exit
        allow 198.51.100.77;   # approved VPN egress
        deny  all;
        proxy_pass http://app_backend;
    }
}

在负载均衡器或反向代理后面,这条规则看到的是代理,除非你另有说明。只有通过受支持的受信任代理机制、并附一份明确的代理地址清单,才能还原调用方地址。声称某个获批地址的请求头并不是调用方拥有它的证据,而在公网监听上,未经校验的转发头会让白名单沦为装饰。

接着检查另一个地址族。只覆盖 IPv4 的规则集,对一个同样在 AAAA 上应答的主机名来说是不完整的决定;有可用 IPv6 的客户端会沿一条你从未审阅过的路径到达同一个应用。

拆分隧道:根本进不来的请求

拆分隧道只承载它清单上的前缀。办公内网网段通常在清单上,公网站点不在。浏览器解析域名,把选中的地址与路由表比对,仍然发现本地默认网关挡在前面。

于是请求从用户家里的线路进来、被拒绝,报告说 VPN 坏了,而隧道其实完全按配置在做事。两种修法都正当:把该目的地导进隧道,让获批出口承载它;或者承认站点是直连的,给远程用户另一条获批路径,比如门禁。把站点的公网白名单扩到某个家庭地址碰巧被包含进来,两者都不是,而且它抹掉了设计本该守住的边界。前两种让矩阵保持诚实;第三种则悄悄删掉其中一行。

先证明拒绝,再证明恢复

站在表格每一行所代表的客户端上,用真正触发该决定的请求去测。对某个不相关的查地址页面做可达性检查证明不了什么,因为那个目的地可能走与应用不同的路径。

curl -sS -o /dev/null -w 'status=%{http_code}
' https://intranet.example.com/app/login
# repeat with the tunnel up, then with it down
tail -f /var/log/nginx/access.log | grep ' /app/'

把边界上的访问记录与表格的预判对照。然后把隧道降下来,发一条理应被拒绝的新请求;已经加载的页面或已经打开的连接什么都测不出来。如果没出现拒绝,执行点就不在你以为的位置。

在网络准入之后仍要保留应用认证,因为办公出口和 VPN 出口各自代表许多人,都不识别具体个人。给用户的说明保持简短:先连上获批隧道,再重试。这正是让一个 URL 保持便利、同时让来源规则仍是判断而非猜测的原因。

相关文章