gRPC 服务安全加固是微服务架构里最常被跳过的一步:接口默认以明文 h2c 暴露、Server Reflection 把全部方法签名公开、metadata 里没有凭证校验、单条流不限长度。攻击者拿到内网一个 IP 和端口,就能用 grpcurl 列出所有服务方法并直接调用。本文给出一套完整的 gRPC 服务安全加固配置,覆盖服务端拦截器、Nginx 网关、mTLS 双向认证与限流。
适用场景
- 微服务之间使用 gRPC 通信,服务端口在宿主机
0.0.0.0:50051上监听,内网或公网可达。 - 后端为 App、小程序、前端 BFF 提供 gRPC / gRPC-Web 接口。
- 服务使用 protobuf 定义接口,但缺乏接口鉴权、访问控制与调用频次限制。
- 需要满足等保或安全基线的南北向接口管控要求,为 gRPC 入口补上网关层审计与限流。
前置条件
- gRPC 服务端(Go / Java / Python 均可),已定义
.proto文件。 - Nginx ≥ 1.13.10(
grpc_pass指令自该版本起支持)或 Envoy 作为网关。 - 可用的域名与证书,私有 CA 亦可(用于 mTLS 的客户端证书签发)。
- 调试机安装
grpcurl、openssl,用于暴露面自查与验证。
原理说明
gRPC 建立在 HTTP/2 之上,使用长度前缀的二进制帧传输 protobuf 消息。这套设计带来三个默认不安全的行为:
- 明文传输:Go 的
grpc.NewServer()默认不启用 TLS,即 h2c 明文模式。内网嗅探即可读取全部请求体,包含业务数据与 token。 - 接口自描述暴露:服务端若注册了
reflection.Register(s),任何人都可以像使用本地 proto 一样列出服务、方法、字段类型,并直接构造请求调用,攻击门槛极低。 - 无连接态鉴权:gRPC 没有 Cookie 会话概念,鉴权依赖 metadata(等价于 HTTP 头)。若服务端只做
context透传而不校验,任何持有内网可达地址的人都能调用。
因此加固思路是分层的:先关掉自描述能力,再强制加密与身份校验,最后在网关上做访问控制与限流。三层中任意一层单独实施都不足以防御内网横向移动。
操作步骤
步骤一:以攻击者视角自查暴露面
先确认当前风险,再动手改配置:
# 探测是否明文可达并列出服务
grpcurl -plaintext 10.0.0.10:50051 list
# 列出方法签名(Reflection 开启时可读)
grpcurl -plaintext 10.0.0.10:50051 list myapp.UserService
# 直接调用,验证是否存在无鉴权访问
grpcurl -plaintext -d '{"user_id":"1"}' 10.0.0.10:50051 myapp.UserService/GetUser
若上述命令在非授权机器上返回服务列表或业务数据,说明服务缺少加密、反射与鉴权三层防护。同时检查监听地址:
ss -lntp | grep -E '50051|50052'
# 期望只看到 127.0.0.1 或内网网卡,不应出现 0.0.0.0
步骤二:服务端强制 TLS 并关闭反射
Go 服务端的核心改动是:注册 TLS 凭证、移除 reflection、挂载鉴权拦截器,并限制单条消息大小:
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
)
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 || !verifyToken(tokens[0]) { // verifyToken 校验签名与有效期
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
func main() {
cert, _ := tls.LoadX509KeyPair("/etc/grpc/server.crt", "/etc/grpc/server.key")
// 强制校验客户端证书,实现 mTLS
pool := x509.NewCertPool()
ca, _ := os.ReadFile("/etc/grpc/ca.crt")
pool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
})
s := grpc.NewServer(
grpc.Creds(creds),
grpc.UnaryInterceptor(authInterceptor),
grpc.MaxRecvMsgSize(4 << 20), // 单条消息上限 4MB
grpc.MaxConcurrentStreams(128),
)
registerServices(s)
// 生产环境不要注册 reflection:
// reflection.Register(s)
lis, _ := net.Listen("tcp", "127.0.0.1:50051") // 只监听本机,由网关转发
s.Serve(lis)
}
要点:ClientAuth 必须设为 RequireAndVerifyClientCert 而非 VerifyClientCertIfGiven,后者在客户端不提供证书时仍会放行;监听地址改为 127.0.0.1,让外部流量必须经过网关。
步骤三:Nginx 网关做 TLS 终结、路径白名单与限流
# /etc/nginx/conf.d/grpc-gateway.conf
limit_req_zone $binary_remote_addr zone=grpc:10m rate=50r/s;
limit_conn_zone $binary_remote_addr zone=grpcconn:10m;
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client on; # 强制 mTLS
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 4m;
# 拒绝反射服务:这是攻击者列举接口的入口
location /grpc.reflection. { deny all; }
# 健康检查单独放行,不暴露业务方法
location /grpc.health.v1.Health/ {
grpc_pass grpcs://127.0.0.1:50051;
}
# 业务方法白名单:只允许显式声明的方法通过
location ~ ^/myapp\.(User|Order)Service/ {
limit_req zone=grpc burst=100 nodelay;
limit_conn grpcconn 20;
grpc_pass grpcs://127.0.0.1:50051;
grpc_set_header X-Real-IP $remote_addr;
grpc_read_timeout 30s;
grpc_send_timeout 30s;
grpc_buffer_size 8k;
}
# 其余 gRPC 路径一律拒绝
location / {
return 404;
}
}
路径白名单比黑名单更安全:只允许业务实际使用的方法前缀,未列出的服务即使被部署进进程也无法从外部调用,等于把暴露面收敛到最小集合。
步骤四:网络层收敛端口与出向访问
# 只允许网关节点访问后端 gRPC 端口(iptables 示例)
iptables -A INPUT -p tcp --dport 50051 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 50051 -j DROP
# 云服务器同步收紧安全组:50051 不放行公网,仅内网网段
# 服务端自身出向也做限制,降低被入侵后的横向移动能力
iptables -A OUTPUT -p tcp --dport 50051 -d 10.0.0.0/24 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 50051 -j DROP
步骤五:为调用方签发客户端证书
# 自建 CA(若已有企业 CA 可直接签发)
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
-keyout /etc/grpc/ca.key -out /etc/grpc/ca.crt -subj "/CN=grpc-internal-ca"
# 为每个调用方签发独立客户端证书,CN 用于标识调用方身份
openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=order-service"
openssl x509 -req -in client.csr -CA /etc/grpc/ca.crt -CAkey /etc/grpc/ca.key \
-CAcreateserial -out client.crt -days 365 -sha256
# 服务端可基于 CN 做细粒度授权
openssl x509 -in client.crt -noout -subject
配置验证
- 明文调用应失败:
grpcurl -plaintext api.example.com:443 list应返回tls: first record does not look like a TLS handshake或握手失败。 - 反射应被禁用:带证书调用
grpcurl -cacert ca.crt -cert client.crt -key client.key api.example.com:443 list,应返回server does not support the reflection API。 - 不带客户端证书应握手失败:
openssl s_client -connect api.example.com:443 -alpn h2 -servername api.example.com,输出中不应出现Verify return code: 0 (ok);应看到服务端要求客户端证书。 - 协议协商正确:上述 openssl 输出应包含
ALPN protocol: h2;若为http/1.1,说明http2 on未生效,gRPC 调用会失败。 - 限流生效:
for i in $(seq 1 200); do grpcurl -d '{}' ... myapp.UserService/GetUser >/dev/null; done,观察出现Unavailable或 Nginx 返回 503,且/var/log/nginx/error.log中记录limiting requests。 - 未授权方法被拒:调用白名单外的方法(如
myapp.AdminService/List),应返回 404 而非Unimplemented,确认请求在网关层即被丢弃。
常见问题(FAQ)
Q1:服务只在 Kubernetes 内网通信,还需要 mTLS 吗?
需要。集群内网络默认是扁平可达的,任何被入侵的 Pod 都能直接访问其他服务的 50051 端口;容器逃逸或配置错误的 Service 也可能把 gRPC 端口暴露出去。mTLS 的价值不只是加密,更在于把「能连通」转化为「有身份」——没有合法客户端证书的调用在 TLS 握手阶段就被拒绝,无需业务代码参与。若集群已有服务网格,可用其自动注入的 mTLS,但要确认 PeerAuthentication 处于 STRICT 模式而非默认的 PERMISSIVE。
Q2:禁用 Server Reflection 之后,开发和排障怎么做?
三种做法并存:开发环境保留 reflection,生产环境移除,靠配置开关控制;调试时用 grpcurl -proto ./proto/user.proto 直接在本地加载 proto 文件,不依赖服务端反射;排障优先使用网关访问日志与 OpenTelemetry 链路数据,而不是列表接口。凡是能在生产环境列出全部方法签名的能力,对攻击者同样好用,属于必须关闭的信息暴露项。
Q3:gRPC 流量能交给传统 WAF 检查吗?
能力有限。传统七层 WAF 主要解析 HTTP/1.1 与部分 HTTP/2 帧,而 gRPC 的 protobuf 消息体是二进制,字段没有明文键名,规则匹配几乎无效。可行方案是在网关侧做协议转换(gRPC-Web / REST 转码)后再交给 WAF,或把 gRPC 的访问控制、限流、mTLS 与审计放在 Envoy / Nginx 这一层实现,WAF 只负责南北向的 HTTP 入口。
Q4:限流应该按连接还是按请求?
两者都要。limit_conn 限制并发流数量,防止单客户端开大量长连接或服务端流占据连接池;limit_req 限制请求速率,防止单连接高频调用。对于服务端流(server streaming)接口,还需单独设置 grpc_read_timeout 与消息大小上限,避免单条流长时间占用资源。
总结
gRPC 服务安全加固的优先级是:先关闭反射、再开 mTLS、然后加网关白名单与限流、最后收敛网络可达性。反射关闭解决接口枚举,mTLS 解决身份伪造与内网嗅探,路径白名单解决未授权方法调用,端口收敛解决横向移动。四层中任何一层缺失,都会让其余三层形同虚设——例如只开 mTLS 但保留反射,攻击者仍可借助合法客户端证书完成完整接口测绘。把这些配置写进部署模板并纳入安全基线检查,才能避免每次发版后防护回退。