使用PrivateLink减少非必要的公网通信

在跨VPC访问服务、跨账号访问服务的场景中,可以使用PrivateLink通过私网访问服务替代通过互联网(公网)访问,缩小公网暴露面,更好地保障网络安全。使用LB服务面向私网提供服务时可以启用ProxyProtocol v2获取请求真实来源VPC ID、PrivateLink终端节点ID,有助于更好地实现网络访问控制。

安全风险

在云环境中,服务(如自建的数据库中台、AI推理应用)或云产品(如对象存储OSS)为了便于访问,常常会配置公网IP(EIP)或公网端点(Endpoint)。这种做法虽然便捷,但本质上是将服务直接暴露在开放的互联网上。这会带来严峻的安全风险:

  1. 公网攻击风险:任何暴露在公网的服务都可能成为攻击目标,面临DDoS攻击、应用漏洞扫描与利用、密码暴力破解等威胁。

  2. 身份冒用风险:传统的互联网服务主要依赖账号密码进行认证。一旦凭证泄露,攻击者可以从全球任何地方冒用合法身份进行非法访问,窃取或篡改数据。公网访问模式无法对请求的“网络来源”进行有效限制。

  3. 数据外泄通道:内部员工可能会利用个人云账号,通过云服务(如OSS)的公网端点,将企业核心数据从内部网络上传并外泄。由于通信发生在公网,企业侧的网络策略难以有效管控。

最佳实践

使用PrivateLink实现同可用区跨VPC访问

image

说明:在VPC2中使用ECS部署某网络服务(如Web服务),在VPC1VPC2间创建PrivateLink,使得在VPC1中可以通过内网地址(终端节点)访问网络服务。

注意:使用PrivateLink的前提是终端节点(服务使用方)与终端节点服务(服务提供方)必须同一可用区,或者终端节点部署的可用区是终端节点服务资源部署可用区的子集(如终端节点服务部署在可用区A、B,终端节点只能选择AB)

核心步骤

  1. 服务端(服务提供方)配置

    1. 创建负载均衡:在VPC2中创建一个支持PrivateLink的负载均衡实例(如私网类型的CLBNLB),并配置好后端服务器组和监听。

    2. 创建终端节点服务:进入私网连接控制台,创建一个终端节点服务,并将其关联到上一步创建的负载均衡实例。

    3. 配置服务白名单(仅跨账号需要):如果VPC1属于不同的阿里云主账号,必须将该账号的UID添加到终端节点服务的白名单中。

    4. 接受连接请求:等待客户端发起连接后,在终端节点服务的“连接”标签页中,找到待处理的连接请求并“允许”它。(也可在创建服务时设置为自动接受连接)。

  2. 客户端(服务使用方)配置

    1. 创建终端节点:在VPC1中,创建一个终端节点。在创建过程中,选择服务端创建的终端节点服务。

    2. 配置网络:为终端节点选择位于VPC1内的交换机和安全组,以控制VPC1内哪些资源可以访问该终端节点。

    3. 获取访问地址:创建成功后,终端节点会生成一个私网域名和IP地址。

    4. 发起访问:等待服务端接受连接后,VPC1内的应用即可使用此域名或IP来访问VPC2中的服务。

详细操作,请参见通过私网访问云服务

使用Proxy Protocol 控制可访问的VPC及其内网IP

通过PrivateLink提供服务,服务端能够识别网络请求的真实来源(具体来自哪个终端节点或私有网络VPC)。因此服务可以设置安全规则,限制某用户账号仅能在某合法私网中使用(即仅接受合法VPC的访问),即使账号不慎泄露,也可以杜绝从互联网或非法网络使用该账号访问服务。

使用PrivateLink实现同可用区跨VPC访问的基础上,让服务端应用能够识别请求来自哪个VPC,并基于此实现访问控制。

  1. 开启Proxy Protocol v2:在服务端的NLB监听上开启Proxy Protocol v2,并订阅VPC ID和终端节点ID。

    # 使用阿里云CLI为指定监听开启Proxy Protocol v2并订阅VPC ID和终端节点ID
    aliyun nlb UpdateListenerAttribute \
         --ListenerId lsn-xxxxxxxxxxxxxxxx \
         --ProxyProtocolEnabled true \
         --ProxyProtocolV2Config '{"Ppv2VpcIdEnabled":true,"Ppv2PrivateLinkEpIdEnabled":true}' \
         --RegionId cn-hangzhou

    --ListenerId :是创建监听ID,即为例子中Web服务创建的监听。

    --ProxyProtocolEnabled:true表示开启ProxyProtocol协议。注意:该协议会在TCP握手后的第一个包中传递客户端连接信息,因此必须后端服务支持该协议(见步骤2配置),否则服务会中断。

    --ProxyProtocolV2Config:添加阿里云定义的TLV字段,这里订阅了来源网络的VPC ID、PrivateLink的终端节点ID。image.pngimage.png对应TLV解析如下:

    含义/内容

    原始内容

    协议头

    Proxy Protocol协议固定签名

    0d0a0d0a000d0a515549540a

    版本2, PROXY命令

    21

    TCP/IPv4

    11

    后面跟的数据长度为 84 字节

    0054

    地址信息

    IP: 10.0.0.14

    0a00000e

    目的IP: 10.0.0.15

    0a00000f

    源端口: 59074

    e6c2

    目的端口: 80

    0050

    TLV1

    03类型

    03

    4字节

    0004

    0764b56b

    0764b56b

    TLV2

    e1类型(自定义类型)

    e1

    24字节

    0018

    PrivateLink终端节点ID:

    ep-bp1i288487e586152d4b

    0265702d6270316932383834383765353836313532643462

    TLV3

    e1类型(自定义类型)

    e1

    26字节

    001a

    VPC ID:

    vpc-bp179qeke0wzo1mr8bxhl

    017670632d627031373971656b6530777a6f316d72386278686c

    TLV4

    04 (PP2_TYPE_NETNS),用于传递网络命名空间

    04

    6字节

    0006

    000000000000

    000000000000

  2. 假设Web服务使用Nginx提供Web服务,可以按如下配置支持Proxy Protocol:

    (Nginx Plus R16及以后版本或开源Nginx 1.13.11及以后版本支持Proxy Protocol v2)

    server {
        listen 80 proxy_protocol;   #启用proxy Protocol v1/v2协议
        # 其他配置...
    }

    以上方式,nginx会自动处理80端口的proxy Protocol协议解析TLV,但原始nginx仅支持从TLV中提取ip

    址,对于阿里自定义的VPC ID、PrivateLink终端节点ID需要lua扩展来自行处理。

    以下代码示例是在nginx.conf中编写Lua插件处理TCP连接,从socket中读取TCP连接后收包,并解析TLV

    存进请求的上下文中。在/login请求时判断PrivateLink终端节点ID是否符合预期。Lua代码示例如下:

    stream {
      server{
      listen 80;  #不能启用proxy_Protocol,需要交给下面lua处理
      preread_by_lua_block {
         -- 1. 尝试读取固定的16字节协议头
         local sock = ngx.req.socket(true)
         local header, err = sock:receive(16)
    
         if not header then
            ngx.log(ngx.ERR, "failed to receive proxy protocol header: ", err)
            return
         end
    
         -- 2. 检查协议签名 (12字节)
         if string.sub(header, 1, 12) ~= "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" then
             -- 如果不是Proxy Protocol,须将已读取的数据放回缓冲区,否则会破坏原始请求
              sock:setreused(header)
                    return
        end
    
        -- 3. 解析协议头剩余部分
        local ver_cmd = string.byte(header, 13) -- 版本和命令
        local family = string.byte(header, 14) -- 地址族和协议
        local len = string.byte(header, 15) * 256 + string.byte(header, 16) -- 可变部分的总长度
        if len == 0 then
           -- 没有地址和TLV信息,直接返回
           return
        end
    
        -- 4. 读取可变长度部分 (包含地址和TLV)
        local variable_part, err = sock:receive(len)
        if not variable_part or #variable_part < len then
           ngx.log(ngx.ERR, "failed to read variable part (address + tlvs): ", err)
           return
        end
    
        -- 5. 根据地址族(family)确定地址信息的长度
        local addr_len = 0
        local protocol = family & 0x0F
        local address_family = family >> 4
    
        if address_family == 1 then -- AF_INET (IPv4)
            addr_len = 12
        elseif address_family == 2 then -- AF_INET6 (IPv6)
            addr_len = 36
        elseif address_family == 3 then -- AF_UNIX
            addr_len = 216
        end
    
        -- 6. 分离地址信息和TLV数据
        local address_block = string.sub(variable_part, 1, addr_len) -- 如果需要,可以解析地址信息
        local tlv_string = string.sub(variable_part, addr_len + 1)
    
        -- 7. 解析TLV数据
        local tlvs = {}
        local pos = 1
        while pos <= #tlv_string do
            -- 确保至少有3个字节 (Type, Length)
            if pos + 2 > #tlv_string then
                ngx.log(ngx.ERR, "malformed TLV data: not enough bytes for type and length")
                break
            end
    
            local tlv_type = string.byte(tlv_string, pos)
            local tlv_length = string.byte(tlv_string, pos + 1) * 256 + string.byte(tlv_string, pos + 2)
        
            -- 确保value的长度不会越界
            if pos + 2 + tlv_length > #tlv_string then
                ngx.log(ngx.ERR, "malformed TLV data: length exceeds available data")
                break
            end
        
            local tlv_value = string.sub(tlv_string, pos + 3, pos + 2 + tlv_length)
                        
            table.insert(tlvs, {
                type = tlv_type,
                length = tlv_length,
                -- 将二进制的value编码为Base64,便于存储和查看
                value = ngx.encode_base64(tlv_value) 
             })
             pos = pos + 3 + tlv_length
       end
    
       -- 8. 将解析出的TLV缓存到ngx.ctx中
       -- ngx.ctx对于每个请求都是唯一的,是传递数据的标准方式
      if #tlvs > 0 then
          ngx.ctx.proxy_protocol_tlvs = tlvs
          -- 示例:如果你想在日志中记录它,可以在http部分的log_format中使用$proxy_protocol_tlvs变量
          -- 需要在http块中定义 `lua_set $proxy_protocol_tlvs 'return ngx.var.proxy_protocol_tlvs_json';`
          -- 和 `preread_by_lua_block` 中 `ngx.var.proxy_protocol_tlvs_json = cjson.encode(ngx.ctx.proxy_protocol_tlvs)`
      end
      }
      proxy_pass localhost:8080;   #后面的http请求给http server处理
      }
    }
    
    http {
        server {
            listen 8080;
    
            # 处理 /login 请求的专用 location
            location /login {
                access_by_lua_block {
                    local expected_value = "some value"
                    -- 从Nginx变量中获取stream层传递过来的第2TLV值 (Base64编码的)
                    local privateLinkEndId = ngx.var.pp_tlv2_value
                    
                    -- 如果不符合预期则拒绝
                    if received_value ~= expected_value then
                        ngx.log(ngx.ERR, "Access to /login denied: TLV value mismatch. Expected '", 
                                expected_value, "', got '", received_value, "'")
                        return ngx.exit(ngx.HTTP_FORBIDDEN)
                    end
                    -- 验证通过,请求将继续被处理
                }
                
                # 验证通过后,将请求转发给后端的Python程序
                proxy_pass http://your_python_backend;
            }
    
            # 处理其他所有请求
            location / {
                # 这里可以根据你的需求决定是否允许访问
                # 例如,直接转发给后端
                proxy_pass http://your_python_backend;
            }
        }
    
        # 你的Python后端服务 upstream 定义
        upstream your_python_backend {
            server 127.0.0.1:5000; # 假设你的后端程序运行在5000端口
        }
    }

使用PrivateLink访问云服务

目前部分云服务已经支持通过PrivateLink访问规避公网访问风险,1)配置私网中允许使用的合法账号,避免

使用个人账号外传企业数据的风险。2)配置企业账号只能从合法私网访问,避免账号泄露后在外部被利用的

风险。

  1. 为云服务创建终端节点:进入PrivateLink控制台,为需要访问的云服务(如OSS)创建一个网关终端节

    点。将其关联到企业内部的VPC和路由表。

  2. 配置终端节点访问策略:在终端节点的策略中,限制只允许来自企业主账号下的RAM身份(用户或角

    色)访问。这可以防止其他账号(如员工个人账号)通过这个私网通道访问OSS。

    在终端节点的访问策略中限制只允许企业主账号下的所有账号访问云服务。

    image.png

  3. 配置RAM策略(可选增强):为进一步加强管控,您可以在RAM中为子用户或角色配置策略,例如,强制

    要求他们对OSS的访问必须来自指定的VPC。

image.png

合规能力

检查:是否存在从VPC访问阿里云公网地址的情况

在进行配置前,首先需要排查环境中是否存在可以被PrivateLink优化的公网通信。核心思路是分析VPC流日志,找

出从VPC内部主动访问阿里云公网IP的流量。

执行步骤:

  1. 开启VPC流日志:为核心业务所在的VPC开启流日志功能,并将日志投递到日志服务SLS中。

  2. 分析出向流量:在日志服务中,查询VPC流日志,筛选出方向为out(出向)的流量。

  3. 过滤与定位

    • 过滤回包流量:为减少干扰,可以过滤掉目的端口小于1024的流量以及其他已知的正常服务回包流量。

    • 识别阿里公网IP:对日志中的目的IP地址(dstaddr)进行归属地查询(例如使用ipip.net等工具)。重点关

      注归属于阿里云的公网IP。

    • 定位源头ECS:根据流日志中的vm-id字段,可以精确定位到发起访问的ECS实例。

日志样例分析:

通过分析这类日志,可以识别出哪些实例正在通过公网访问其他云上资源。

image.png

结果判断:

  • 如果查询到的目的公网IP是某个阿里云服务(如OSS、ECS)的公网端点,强烈建议切换为使用PrivateLink终端

    节点访问。

  • 如果目的公网IP是自己账户下的另一个VPC中的服务(如EIP+SLB),这属于跨VPC通信,是使用PrivateLink

    进行优化的理想场景。