1. 先认清元数据服务:实例上的“身份证窗口”,也是攻击的“秘密仓库”
1.1 元数据服务平常是怎么工作的
我做安全巡检时发现一个有意思的现象:很多运维同事能熟练使用gcloud命令管理GCE实例,但对实例内部的169.254.169.254这个地址几乎一无所知。他们不清楚每个GCE实例里都常驻着一个元数据服务(Metadata Server),也不了解这个服务在默认配置下只对实例内部开放——凡是能从这个虚拟机内发起网络请求的进程,都有机会直接访问它。
元数据服务的访问方式非常简单,在实例的Shell里执行下面这条命令就能看到它的根目录:
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/"你也可以把域名换成链路本地地址:
curl -H "Metadata-Flavor: Google" "http://169.254.169.254/computeMetadata/v1/"169.254.169.254是所有主流云平台共用的链路本地元数据地址,AWS、GCE、阿里云都遵循这个约定。区别于常规HTTP服务,GCE元数据服务要求你在请求时带上Metadata-Flavor: Google这个请求头,没有它,面对普通浏览器式请求时服务会直接拒绝。这个头的作用相当于“说明自己是合法客户端”,但这种校验非常初级——它只能拦住误操作,拦不住有意构造的请求。
元数据服务解决的问题,是让虚拟机在开机阶段“认识自己”。实例ID、机器类型、所在区域、启动脚本、SSH公钥、自定义标签、服务账号信息、临时OAuth Token……这些实例运行期需要的数据,都通过元数据服务暴露给虚拟机内部的进程。启动脚本最常见的写法,就是在实例初始化时用一行curl把配置参数从元数据服务里拉出来,再写入本地配置文件。
我用一个生活化的类比描述过这个机制:元数据服务就像酒店房间床头柜里的服务手册,告诉你WiFi密码、餐厅开放时间、退房流程和紧急联系电话。对有权限住进房间的人来说,这本手册很有用;问题是这本手册放在房间里任何人都能翻到的位置,而且酒店管理员不会检查翻手册的人拿了房卡还是撬了锁。
1.2 攻击者在这里会盯上什么
攻击者的视角和运维完全不同。拿到一台虚拟机后,他们通常不会急着翻磁盘和内存,而是先探测元数据服务。原因很简单:这里可能藏着“云平台级别的钥匙”,而不是这台机器上的普通文件。
对GCE来说,最诱人的资源位于下面这条路径:
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"返回的JSON里包含access_token和expires_in两个关键字段,令牌有效期通常是一小时。这个令牌对应用户身份是这台VM绑定的服务账号。在默认配置下,新创建的GCE实例会自动绑定项目默认的Compute Engine服务账号,这个默认服务账号往往拥有项目级别的Editor角色——注意是“项目级别”而不是“实例级别”。攻击者一旦成功读取这个令牌,拿到的可能是整个项目的写权限。
项目级Editor可以创建虚拟机、删除已有资源、修改IAM策略、读取云存储对象、调用各类管理API,覆盖面非常广。攻击者拿到令牌后,甚至不需要对GCP API有多熟悉,直接把令牌交给gcloud命令行工具即可完成身份切换:
gcloud --oauth2-access-token=<获取到的token> compute instances list这条命令一旦执行成功,项目里的资产清单基本就暴露了。如果服务账号权限没做收敛,后续还可以做更多危险操作。
除了令牌,元数据里还可能有自定义键值对。开发团队为了省事,把数据库地址、API Key、甚至明文密码写进自定义元数据的例子我见过不少。这部分内容风险同样极高,我放在第二部分专门展开。
1.3 为什么“网络隔离”不总是能保护它
很多读者第一反应是:元数据服务只能从实例内部访问,外部攻击者连不通169.254.169.254,有什么好担心的?
这个想法不能算错,但它忽略了一个关键事实:外部攻击者通常不会直接连元数据服务,而是先攻破一个能访问元数据服务的“中间载体”。这个载体可以是带SSRF漏洞的Web应用,可以是失陷的第三方组件,也可以是攻击者在VPC内部横向移动后控制的另一台实例。安全圈常说的“跳板”和“横移”,在云环境里最常见的目标之一就是169.254.169.254。
GCE默认防火墙规则虽然限制了来自公网的访问,但VPC内部默认允许实例访问元数据服务。如果你习惯把所有内网资源放在同一个VPC或子网里,又没有设置更严格的东西向边界规则,那么VPC内任何一台机器被攻破后,攻击者就可以从这台机器出发,探测其他实例的元数据服务。元数据服务本身不校验请求者的身份,它只关心请求是不是从“这台VM自己的网络栈”里发出的。VM A的元数据服务不会响应VM B的请求,但攻击者控制了VM B之后,可以利用VM B自身的网络身份请求VM B的元数据,再以此为跳板获取云API权限,进而操作整个项目。
所以元数据服务的威胁从来不是“公网能不能直连”,而是“内部任意一个能发起HTTP请求的角落,是否可能成为传递令牌的载体”。位置隔离在单台VM的场景下是有效的,但在复杂内网拓扑、容器网络和代理链面前,它远没有大多数人想象的那么可靠。
2. 自查后发现的最常见隐患:把密码和Token塞进了自定义元数据
2.1 这种现象为何高发
有一回,我给一个创业团队做GCE环境安全体检,项目不算大,三个开发环境,十几台VM,我原本以为半小时能看完。结果在检查其中一台Web服务器实例的启动元数据时,发现自定义属性里存着一整段明文配置,里面包含生产数据库的Host、端口、账号、密码,还有一个第三方支付网关的API Secret。
我当时愣住了,问团队负责人为什么要这么放。对方很坦率地说:“图方便。创建实例时加一句--metadata startup-script,脚本里启动时把这些变量读出来,比搞一套配置分发系统快多了。”
这个回答我听过太多次。项目早期团队规模小,把配置文件塞进启动脚本和自定义元数据,确实是最快的做法。gcloud一行命令就能带上几十个键值对,实例启动时自动注入,省去维护配置文件的心智负担。但你付出的代价是:这些键值对被放进了元数据服务的公共目录里,任何能访问元数据服务的进程、任何能通过启动日志读到命令的平台工具、任何能触发SSRF的Web应用,都可能把它们捞出来。
更值得警惕的是,元数据服务的读取没有IAM鉴权。你设置再精细的IAM策略,都不会影响元数据服务内部的读取逻辑。它只认“请求是否从实例内部发起”这一条,接下来就是对整个目录开放。只要攻击者控制了这个VM上任何一个普通权限的进程,拉取完整键值清单只需要一条命令:
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/attributes/"这条命令返回所有自定义键名,攻击者再逐一把键值取回即可。整个过程不需要sudo、不需要提权、不需要扫描端口。站在攻击者角度看,这是“进来就有”的资源,通常比翻找磁盘上的配置文件更快更稳。
2.2 这类信息泄露为什么难以快速察觉
自定义元数据泄露最麻烦的地方在于,它不像数据库拖库那样有异常流量可以观察。攻击者只是对VM发起HTTP请求,读取一个名为metadata.google.internal的地址。这种请求在多数IDS和网络监控系统里都被视为正常的内部流量,不会触发告警,也不会留下特别显眼的异常特征。
项目级别的自定义元数据(Project Attributes)则更隐蔽,它会在项目内的每一台VM上都可见。如果一个团队在项目级设置某个通用API Token,项目下的所有实例都能读取。我曾见过一个客户,把一套内部监控系统的API Token放在Project元数据里,想着“反正是内部工具,项目就一个”。结果项目里一台临时测试机器被挖矿程序入侵,Token被顺走,攻击者用这个Token调了企业内部多个管理接口,最后是靠账单异常才发现的。
很多人觉得自己的环境小,不在乎这类问题。但从审计角度看,任何明文存储在元数据里的机密,都相当于把保险柜钥匙贴在柜门上。风险大小不取决于团队规模,取决于这份机密能打开多少扇门。一个几十人的团队可能只有几十个VM,但如果数据库密码泄露,损失的是整个业务的数据。
2.3 正确做法:把机密迁到Secret Manager
解决这个问题,标准路径是用GCP的Secret Manager服务,把密码、API Key、数据库连接串这类敏感值从元数据里迁出来。整体流程可以这样做:
- 在Secret Manager里创建对应密钥条目,把原明文值导入。这里注意给密钥启用版本管理,后续轮换只更新新版本。
- 给VM绑定的服务账号授予
roles/secretmanager.secretAccessor权限,且在IAM条件里限定到具体密钥ID,不要给整个项目的授权。 - 修改实例启动脚本,通过Secret Manager API在启动阶段拉取密钥值,写入本地环境文件或直接作为环境变量使用。
- 删除自定义元数据里的相关键值对,同时检查项目属性(Project Attributes)里有没有残留敏感项。
Secret Manager的访问接入了Cloud IAM,可以做到按服务账号、按密钥、按条件精确授权。而元数据服务的读取完全不经过IAM,两者的安全模型有本质区别。这也是我推行“元数据零敏感信息”原则的根源——不是说你永远不能用元数据传参数,而是不能把元数据当成保存凭据的地方。非敏感的配置项,比如环境标识、实例角色标签、启动时需要的普通参数,留在元数据里没有问题;但凡是能用来“登录”“授权”“解密”的信息,一律走Secret Manager加IAM路线。
3. 最经典的一条攻击链路:SSRF直取元数据令牌
3.1 SSRF为什么恰好打中元数据服务的痛处
SSRF(服务端请求伪造,Server-Side Request Forgery)是Web应用漏洞里和元数据服务关系最紧密的一个。它的成因很直接:应用程序接收用户提供的URL或目标地址,然后在服务器端代用户发起请求,再把响应返回给用户。
常见功能包括网页预览卡片、远程图片抓取、URL健康检查、Webhook回调、文件下载代理等。开发者在实现时,往往只校验用户提交的URL是不是http/https开头,却没有限制请求的目标IP,也没有设置出站网络边界。此时攻击者只要把这个URL改成169.254.169.254,应用服务器就会替他去访问元数据服务。
为什么这类漏洞对元数据服务特别致命?核心在于SSRF请求是从应用服务器所在的那台VM的网络栈出去的。元数据服务不看请求的业务逻辑,只看“请求是不是从本机内网发出”。应用服务器运行在GCE实例上时,SSRF请求到达元数据服务的来源IP就是这台VM的内网地址,元数据服务会把它当作合法本地请求处理并返回数据。传统安全设备对公网入站流量严防死守,但SSRF把“内部可信网络发起请求”这个前提直接架空了一半。
3.2 完整利用流程:从发现参数到读取令牌
我把这个利用过程拆成几步,方便读者对照检查自己的应用。
第一步,攻击者发现某个URL参数可以被服务端回显。比如链接预览接口,输入url=https://example.com,服务器抓取页面并返回标题和图片,这个接口没有限制请求内网地址,也没有对响应体做裁剪。
第二步,攻击者把url改成http://169.254.169.254/computeMetadata/v1/。如果应用没有强制添加Metadata-Flavor: Google请求头,GCE元数据服务会拒绝请求。实际攻防中,有些应用会透传用户提供的Header,这就给了攻击者在请求头里携带Metadata-Flavor: Google的机会。如果应用把用户可控参数拼进HTTP请求头,整个读取流程就和在服务器本地敲curl几乎没区别。
第三步,应用把收到的响应原样返回给攻击者。攻击者这时拿到的是元数据目录页,下一步直接访问目标路径:
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token响应里包含access_token、expires_in、token_type三个字段。攻击者保存access_token,用官方工具一测,就能以这台VM的服务账号身份调用GCP API。
需要说明的是,GCE元数据服务在部分路径上对请求头有要求,不同攻击路径的细节会有些差异。但实际项目中,我见过不少SSRF漏洞恰好出现在应用可以控制请求头的场景——代码直接用用户输入拼装HTTP请求,或通过URL解析透传参数。与其在根因分析时纠结“到底要带几个头”,不如把防线前移到“SSRF根本不要发生”的位置:代码层面过滤私网地址,出口层面掐断到元数据服务的路由。
3.3 拿到令牌之后还能做什么
攻击者拿到令牌后的行为,取决于这个VM服务账号被授予了什么权限。如果是默认服务账号且没有收敛角色,大概率是项目级别的Editor权限,这意味着能列举项目里所有资源、修改大部分配置,甚至创建新的高权限VM。
我从防御视角模拟一下最典型的后续动作。攻击者先用token列出所有实例:
curl -s -H "Authorization: Bearer <access_token>" \ "https://compute.googleapis.com/compute/v1/projects/<project-id>/aggregated/instances"如果项目ID未知,可以从元数据服务直接读取:
curl -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/project/project-id"拿到实例清单后,攻击者会继续探测项目里的存储桶:
curl -s -H "Authorization: Bearer <access_token>" \ "https://storage.googleapis.com/storage/v1/b?project=<project-id>"这种“从令牌到资产枚举”的过程速度极快,几十秒就能完成。如果服务账号权限更大,攻击者还可能修改IAM策略、导出密钥、在内部网络建立持久化通道。整个过程在Cloud Audit Logs里有记录,但很多团队不会实时盯审计日志,等发现问题时往往是几天之后了。
一次SSRF的成功,相当于把VM的云身份直接交到攻击者手上。防御SSRF本质上是在保护云平台的“身份证系统”,这也是我为什么反复强调要同时处理“漏洞本身”和“漏洞被利用后的凭证价值”。
4. 防守配置四层加固:从身份到网络的纵深防御
4.1 服务账号最小化:降低被盗令牌的权限价值
如果攻击者拿到的token只有极小权限,SSRF的成功也只能造成有限损失。服务账号最小化是这里的第一道防线,也是最基础却最容易被忽略的工作。
具体操作可以这样展开:
不要在GCE实例上使用项目默认服务账号。默认服务账号在项目里通常有较广泛的角色,建议专门创建服务账号,只授予实例实际需要的权限。一台只跑Nginx的Web服务器,服务账号只需要访问指定存储桶或Secret Manager里某个密钥,那就只授予这些具体资源上的最小权限角色。
按工作负载拆分服务账号。数据库实例、应用实例、CI构建实例分别用不同服务账号,不要共用一个“万金油”账号。这既降低单点风险,也让审计日志里的身份归属更清晰。
定期review服务账号的角色绑定。通过
gcloud projects get-iam-policy或Console的IAM页面查看每个服务账号绑定的角色,凡是没见过、已停用、权限过大的绑定关系,立即清理。
一个常见的误区是认为把VM的API访问范围(scopes)限制住就安全了。API范围只是在VM内部限制了部分调用的“明文能力”,不代表服务账号在IAM层面的实际权限。过度依赖scopes而不去精简IAM角色,在SSRF场景里仍然可能暴露不必要的能力。比如一个VM限制了storage只读范围,但服务账号本身有项目级Editor角色,攻击者拿到token后依然可以通过IAM API给自己授权更高权限,或者调用其他未受scope限制的接口。所以重点还是收敛服务账号在IAM层的角色绑定。
4.2 网络层拦截:压缩元数据服务的暴露面
GCE元数据服务默认只开放给实例内部,公网被防火墙挡住。但这不意味着可以什么都不做。在复杂网络里,VPC内部的跳板机、代理网关、容器隔离失效的Pod,都可能成为访问元数据服务的入口。
我建议在网络层做两件事。
第一,梳理VPC的防火墙规则,避免“全网段开放”。很多项目为了省事,在防火墙里放一条允许公网/0访问实例的规则,这在云环境里风险极高。应该按源IP分段控制,特别是不要把生产VM暴露到任意来源,只放行必要的运维跳板机或负载均衡器的健康检查来源。
第二,在高安全等级环境里,收缩实例对元数据服务的访问口径。GCE允许通过防火墙规则和网络标签组合,限制哪些实例可以访问169.254.169.254。我操作过的一种做法是,给需要访问元数据服务的实例打特定标签,创建一条允许该标签实例访问169.254.169.254的规则;对不需要访问元数据的实例,用更高优先级的规则显式拒绝对该地址范围的访问。这里必须谨慎验证,因为元数据服务同时承载实例启动期间的网络配置信息,误伤会导致实例初始化异常。
容器场景还要额外注意Host网络模式。如果Pod直接使用GCE VM的宿主网络,容器内进程和VM进程共享同一个网络栈,元数据服务对容器同样可见。Kubernetes节点上的Pod如果配置了hostNetwork: true,或者CNI实现允许Pod直接访问链路本地网关地址,都需要单独评估隔离策略。最理想的情况是,普通工作负载的Pod不分配HostNetwork权限,集群内通过NetworkPolicy限制Pod到元数据服务地址的访问。
4.3 应用层防护:出站代理与请求校验
应用层是决定SSRF能否落地的关键防线。一个Web应用理论上很难过滤掉所有恶意请求,但可以通过网络出口做统一收口,把被封堵的部分放到代理层解决。
常见的做法是:把VM的默认出站流量引导到显式代理或专用出口网关,然后在代理层设置域名/IP白名单。应用进程不直连外网,全部经过代理转发。代理只允许访问业务所需的域名,比如特定API域名、特定对象存储bucket域名,其他全部拒绝。这样一来,即使应用存在SSRF漏洞,攻击者试图访问169.254.169.254时,请求会在代理策略中被拦截。
这个方案在大型项目里很常见,但实施时要注意几个细节。代理自身不能部署在被审计的同一台VM上,否则攻击者拿到VM权限后可以改写代理配置。代理的日志要完整保存,便于事后审计和追踪异常出站请求。代理规则变更要有审批流程和变更记录,防止运维人员为了排查问题临时放开全部出站规则又忘记回收。
如果暂时没条件上代理,至少在应用代码里增加一层过滤:在发起外呼请求之前,解析目标URL的IP,如果是私网地址段或链路本地地址段,直接拒绝。这个逻辑要覆盖IPv4和IPv6的所有保留地址范围,包括127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16、::1、fc00::/7等。同时要注意DNS rebinding问题,解析后拿到的IP和实际连接时的IP可能不同,更稳妥的做法是在建立连接前对解析结果做二次校验,或者直接使用能强制指定IP连接的网络库。
4.4 监控与响应:如何发现异常引用
最后一道防线是“看得见”。元数据服务内部的HTTP访问,云平台默认不会为这种内部流量强制打审计日志,但GCP管理面上的API调用记录是有的。你需要关注的是:哪些服务账号、哪些时间、从哪些来源IP调用了GCP管理API。
打开Cloud Audit Logs,为compute.googleapis.com、storage.googleapis.com、iam.googleapis.com等关键管理面配置导出规则,投递到日志收集系统。为非典型的API调用设置告警基线。比如一个平时只处理Web请求的实例服务账号,突然开始批量列举实例列表或下载大对象,这极可能是令牌被盗的信号。
也可以借助GCP的Security Command Center这类安全托管服务,它自带基础威胁监测能力,能识别常见的可疑行为模式。但它只是一个辅助工具,真正决定安全性的,仍然是你账号权限的收敛程度和出站网络的控制力度。
另外,我建议把针对元数据服务地址的访问行为纳入Web应用日志。很多Web框架不会为这种内部链接的访问单独记录,但如果应用因SSRF产生过外呼,日志里通常能看到url参数异常或响应体长度异常。在WAF层,可以对解析目标IP落在169.254.0.0/16范围内的请求添加告警和阻断规则。我们团队就在网关层加了一条规则:凡是业务请求URL解析结果为169.254.0.0/16的,直接打安全事件并隔离会话,这条规则拦截过好几次来自内部应用的恶意探测。
5. 安全自检清单与长期维护建议
5.1 快速自查清单
如果你接手了一个GCE项目,不确定之前有没有踩过这些坑,可以从下面几项快速自查:
- 登录一台实例,在Shell里执行
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/attributes/",检查自定义元数据键值里有没有密码、Token、数据库连接串等高敏感信息,有则需要迁移到Secret Manager。 - 查看项目属性里的自定义键值对:
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/project/attributes/",同样检查是否有敏感项残留。 - 用同一个VM的服务账号,打开IAM页面查看它绑定的项目级角色。如果发现是Editor或Owner级别,而VM实际只是普通业务机器,说明权限严重过大,立即收敛。
- 检查防火墙规则里有没有过于宽松的“允许公网访问实例”和“允许非必要端口对外开放”的规则。GCE默认规则是合理的最小化,很多团队后来会越加越宽。
- 排查Web应用里是否存在用户可控URL且响应可回显的功能,主要关注远程图片抓取、网页预览、URL检测类接口。如果存在,至少要加私网地址段过滤和Host校验。
对照这张表可以快速定位当前状态:
| 检查项 | 高风险状态 | 建议处理 |
|---|---|---|
| 自定义元数据 | 含密码/Token/连接串 | 迁至Secret Manager并清理 |
| 服务账号角色 | 项目级Editor/大量权限 | 收敛到具体资源的最小权限 |
| 防火墙规则 | 存在公网全段访问 | 按源IP分段最小化 |
| Web应用出站 | 用户可控制URL且无过滤 | 加IP段过滤或上代理收口 |
| 审计监控 | 未导出管理面日志 | 配置Audit Logs导出与告警 |
5.2 维护周期与个人建议
安全配置不是一次性工作,元数据服务的问题尤其如此。我建议至少每季度做一次服务账号权限review,每次实例模板或启动脚本变更时,顺便检查有没有新增敏感信息被塞进元数据。很多问题不是一开始就有的,而是某次上线时顺手加了一个--metadata db_password=xxx,之后就再也没人管过。
如果条件允许,建议在CI/CD流程里加一条静态扫描规则。凡是出现在云计算metadata字段里的键名,只要带password、secret、token、key、credential这些关键字,直接打回并提示使用Secret Manager。这条规则实现成本很低,收益却很高,比任何后期审计都更早拦截问题。
我在实际排查中最大的体会是:GCE元数据服务本身是一个设计高效的功能,问题不出在“它存在”这件事上,而出在我们对它的依赖方式。把实例的云身份凭证、把业务的关键密钥,都放在一个“无IAM鉴权、只靠网络位置隔离”的HTTP接口里,本质上是用安全纵深换开发便利。只要接受这个前提,防线就很清晰——先收敛服务账号权限,再控制网络出口,最后做好审计监控。三层都做扎实,元数据服务的安全风险基本能压到可控范围。
这几个月里,凡是找我审过的GCE环境,有一半以上或多或少存在元数据里塞敏感值的情况。这也从侧面说明,元数据服务配置风险是真的容易被忽略,但它一旦被利用,影响又往往是整个项目级别的。希望这篇内容能帮你在自己的环境里提前做一次排查,别等到SSRF被利用、令牌被偷走之后再来复盘。