feat: add authz-adapter service and Envoy ext_authz integration

- Implemented authz-adapter deployment and service for Envoy gRPC authorization.
- Created PowerShell script to generate JWK for JWT authentication.
- Documented the integration of ext_authz with user-rpc.ValidateToken in ENVOY_EXT_AUTHZ_ADAPTER.md.
- Added comprehensive Envoy Gateway configuration guide with JWT authentication and access control in ENVOY_GATEWAY_GUIDE.md.
This commit is contained in:
wwweww
2026-02-26 06:08:35 +08:00
parent 60b6f40f9f
commit 659168fe32
30 changed files with 2093 additions and 3527 deletions
+31
View File
@@ -0,0 +1,31 @@
# authz-adapter
Envoy `ext_authz` 适配服务,实现 `envoy.service.auth.v3.Authorization`,并调用 `user-rpc.ValidateToken`
## 环境变量
- `LISTEN_ON`:监听地址,默认 `0.0.0.0:9002`
- `USER_RPC_TARGET`user-rpc 地址,默认 `user-rpc-svc.juwan.svc.cluster.local:9001`
## 本地运行
```powershell
go run ./app/authz/adapter
```
## Docker 构建
在仓库根目录执行:
```powershell
docker build -f app/authz/adapter/Dockerfile -t authz-adapter:local .
docker run --rm -p 9002:9002 authz-adapter:local
```
## 说明
- 放行路径:`/healthz``/api/users/login``/api/users/register`
- 受保护路径:其余请求要求
- Cookie 中有 `JToken`
- Header 中有 `x-auth-user-id`(由 Envoy `jwt_authn` 注入)
- 鉴权通过后回传:`x-auth-user-id``x-auth-role-type`
+183
View File
@@ -0,0 +1,183 @@
package main
import (
"context"
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
userpb "juwan-backend/app/users/rpc/pb"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
codepb "google.golang.org/genproto/googleapis/rpc/code"
statuspb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
headerAuthUserID = "x-auth-user-id"
headerAuthRoleType = "x-auth-role-type"
headerAuthIsAdmin = "x-auth-is-admin"
headerCookie = "cookie"
cookieJToken = "JToken"
)
type authzServer struct {
authv3.UnimplementedAuthorizationServer
userRPC userpb.UsercenterClient
}
func (s *authzServer) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) {
httpReq := req.GetAttributes().GetRequest().GetHttp()
if httpReq == nil {
return deny(codepb.Code_INVALID_ARGUMENT, typev3.StatusCode_BadRequest, "missing http attributes"), nil
}
path := httpReq.GetPath()
if isPublicPath(path) {
return allow(nil), nil
}
token, ok := getCookieValue(httpReq.GetHeaders(), cookieJToken)
if !ok || token == "" {
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing JToken cookie"), nil
}
userIDHeader := getHeader(httpReq.GetHeaders(), headerAuthUserID)
if userIDHeader == "" {
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing x-auth-user-id header"), nil
}
userID, err := strconv.ParseInt(userIDHeader, 10, 64)
if err != nil || userID <= 0 {
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "invalid x-auth-user-id"), nil
}
rpcCtx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
defer cancel()
resp, err := s.userRPC.ValidateToken(rpcCtx, &userpb.ValidateTokenReq{
Token: token,
UserId: userID,
})
if err != nil {
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "validate token failed"), nil
}
if !resp.GetValid() {
return deny(codepb.Code_PERMISSION_DENIED, typev3.StatusCode_Forbidden, "token invalid"), nil
}
outHeaders := []*corev3.HeaderValueOption{
{Header: &corev3.HeaderValue{Key: headerAuthUserID, Value: strconv.FormatInt(resp.GetUserId(), 10)}},
{Header: &corev3.HeaderValue{Key: headerAuthRoleType, Value: strconv.FormatInt(resp.GetRoleType(), 10)}},
}
if getHeader(httpReq.GetHeaders(), headerAuthIsAdmin) != "" {
outHeaders = append(outHeaders, &corev3.HeaderValueOption{Header: &corev3.HeaderValue{Key: headerAuthIsAdmin, Value: getHeader(httpReq.GetHeaders(), headerAuthIsAdmin)}})
}
return allow(outHeaders), nil
}
func allow(headers []*corev3.HeaderValueOption) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &statuspb.Status{Code: int32(codepb.Code_OK)},
HttpResponse: &authv3.CheckResponse_OkResponse{
OkResponse: &authv3.OkHttpResponse{Headers: headers},
},
}
}
func deny(code codepb.Code, httpCode typev3.StatusCode, message string) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &statuspb.Status{Code: int32(code), Message: message},
HttpResponse: &authv3.CheckResponse_DeniedResponse{
DeniedResponse: &authv3.DeniedHttpResponse{
Status: &typev3.HttpStatus{Code: httpCode},
Body: fmt.Sprintf(`{"code":%d,"message":"%s"}`, httpCode, message),
Headers: []*corev3.HeaderValueOption{
{Header: &corev3.HeaderValue{Key: "content-type", Value: "application/json"}},
},
},
},
}
}
func isPublicPath(path string) bool {
if path == "/healthz" || path == "/api/users/login" || path == "/api/users/register" {
return true
}
return false
}
func getHeader(headers map[string]string, key string) string {
for k, v := range headers {
if strings.EqualFold(k, key) {
return v
}
}
return ""
}
func getCookieValue(headers map[string]string, name string) (string, bool) {
cookieHeader := getHeader(headers, headerCookie)
if cookieHeader == "" {
return "", false
}
parts := strings.Split(cookieHeader, ";")
for _, part := range parts {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
if kv[0] == name {
return kv[1], true
}
}
return "", false
}
func getEnvWithDefault(key, defaultValue string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return defaultValue
}
return value
}
func run() error {
listenOn := getEnvWithDefault("LISTEN_ON", "0.0.0.0:9002")
userRPCTarget := getEnvWithDefault("USER_RPC_TARGET", "user-rpc-svc.juwan.svc.cluster.local:9001")
conn, err := grpc.NewClient(userRPCTarget, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("dial user rpc failed: %w", err)
}
defer conn.Close()
lis, err := net.Listen("tcp", listenOn)
if err != nil {
return fmt.Errorf("listen failed: %w", err)
}
grpcServer := grpc.NewServer()
authv3.RegisterAuthorizationServer(grpcServer, &authzServer{userRPC: userpb.NewUsercenterClient(conn)})
fmt.Printf("authz-adapter listening on %s, user-rpc target %s\n", listenOn, userRPCTarget)
return grpcServer.Serve(lis)
}
func main() {
if err := run(); err != nil {
if !errors.Is(err, net.ErrClosed) {
panic(err)
}
}
}
@@ -23,25 +23,25 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
l := user.NewLoginLogic(r.Context(), svcCtx) l := user.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req) resp, err := l.Login(&req)
token := resp.Token
resp.Token = ""
http.SetCookie(w, &http.Cookie{
Name: "JToken",
Value: token,
Quoted: false,
Path: "/",
Domain: "",
RawExpires: "",
MaxAge: 691200,
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Partitioned: false,
})
if err != nil { if err != nil {
httpx.ErrorCtx(r.Context(), w, err) httpx.ErrorCtx(r.Context(), w, err)
} else { } else {
token := resp.Token
resp.Token = ""
http.SetCookie(w, &http.Cookie{
Name: "JToken",
Value: token,
Quoted: false,
Path: "/",
Domain: "",
RawExpires: "",
MaxAge: 691200,
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Partitioned: false,
})
httpx.OkJsonCtx(r.Context(), w, resp) httpx.OkJsonCtx(r.Context(), w, resp)
} }
} }
@@ -46,9 +46,9 @@ func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
resp, err := l.Register(&req) resp, err := l.Register(&req)
if err != nil { if err != nil {
httpx.ErrorCtx(r.Context(), w, err) httpx.ErrorCtx(r.Context(), w, utils.NewErrorResp(400, err))
} else { } else {
httpx.OkJsonCtx(r.Context(), w, utils.NewErrorResp(400, err)) httpx.OkJsonCtx(r.Context(), w, resp)
} }
} }
} }
@@ -38,11 +38,17 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
Username: req.Username, Username: req.Username,
Passwd: req.Password, Passwd: req.Password,
}) })
logx.Infof("res:%v", res)
if err != nil { if err != nil {
logx.Errorf("rpc login err: %v", err) logx.Errorf("rpc login err: %v", err)
return nil, errors.New("login fail") return nil, errors.New("login fail")
} }
if res == nil || res.Id <= 0 || res.Username == "" || res.Token == "" {
logx.Errorf("rpc login returned empty payload, username=%s, resp=%+v", req.Username, res)
return nil, errors.New("login fail")
}
return &types.LoginResp{ return &types.LoginResp{
UserId: res.Id, UserId: res.Id,
Username: res.Username, Username: res.Username,
@@ -59,7 +59,7 @@ func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterRe
requestId, err := contextx.RequestIdFrom(l.ctx) requestId, err := contextx.RequestIdFrom(l.ctx)
if err != nil { if err != nil {
logx.Errorf("contextx.RequestIdFrom failed: %v", errjA) logx.Errorf("contextx.RequestIdFrom failed: %v", err)
return nil, errors.New("contextx.RequestIdFrom failed") return nil, errors.New("contextx.RequestIdFrom failed")
} }
+3
View File
@@ -28,3 +28,6 @@ CacheConf:
Jwt: Jwt:
SecretKey: "${JWT_SECRET_KEY}" SecretKey: "${JWT_SECRET_KEY}"
Issuer: "juwan-user-rpc" Issuer: "juwan-user-rpc"
Log:
Level: info
@@ -31,6 +31,7 @@ func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
logx.WithContext(l.ctx).Errorf("LoginLogic.Login error:%v", err) logx.WithContext(l.ctx).Errorf("LoginLogic.Login error:%v", err)
return nil, err return nil, err
} }
logx.Infof("user:%v", user)
if !utils.VerifyPassword(user.Passwd, in.Passwd) { if !utils.VerifyPassword(user.Passwd, in.Passwd) {
logx.WithContext(l.ctx).Errorf("User %s Login failed", user.Username) logx.WithContext(l.ctx).Errorf("User %s Login failed", user.Username)
return nil, errors.New("incorrect password") return nil, errors.New("incorrect password")
@@ -2,7 +2,6 @@ package logic
import ( import (
"context" "context"
"fmt"
"juwan-backend/app/users/rpc/internal/svc" "juwan-backend/app/users/rpc/internal/svc"
"juwan-backend/app/users/rpc/pb" "juwan-backend/app/users/rpc/pb"
@@ -27,8 +26,8 @@ func NewValidateTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Val
} }
func (l *ValidateTokenLogic) ValidateToken(in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) { func (l *ValidateTokenLogic) ValidateToken(in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
redisKey := fmt.Sprintf(USER_TOKEN_TEMP, in.UserId)
_, err := l.svcCtx.JwtManager.Valid(l.ctx, redisKey) _, err := l.svcCtx.JwtManager.Valid(l.ctx, in.Token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+10
View File
@@ -1,10 +1,20 @@
package utils package utils
import "encoding/json"
type ErrorResponse struct { type ErrorResponse struct {
Code int `json:"code"` Code int `json:"code"`
Msg string `json:"msg"` Msg string `json:"msg"`
} }
func (e *ErrorResponse) Error() string {
marshal, err := json.Marshal(e)
if err != nil {
return err.Error()
}
return string(marshal)
}
func NewErrorResp(code int, msg error) *ErrorResponse { func NewErrorResp(code int, msg error) *ErrorResponse {
return &ErrorResponse{ return &ErrorResponse{
Code: code, Code: code,
+388
View File
@@ -0,0 +1,388 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-config
namespace: juwan
data:
envoy.yaml: |
static_resources:
listeners:
- name: ingress_http
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
generate_request_id: true
use_remote_address: true
internal_address_config:
cidr_ranges:
- address_prefix: 10.0.0.0
prefix_len: 8
- address_prefix: 172.16.0.0
prefix_len: 12
- address_prefix: 192.168.0.0
prefix_len: 16
- address_prefix: 127.0.0.0
prefix_len: 8
route_config:
name: local_route
virtual_hosts:
- name: juwan_services
domains: ["*"]
routes:
- match:
path: /healthz
direct_response:
status: 200
body:
inline_string: ok
typed_per_filter_config: &public_route_ext_authz_disabled
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match:
path: /api/users/login
route:
cluster: user_api_cluster
timeout: &default_route_timeout 30s
typed_per_filter_config: *public_route_ext_authz_disabled
- match:
path: /api/users/register
route:
cluster: user_api_cluster
timeout: *default_route_timeout
typed_per_filter_config: *public_route_ext_authz_disabled
- match:
prefix: /api/users
route:
cluster: user_api_cluster
timeout: *default_route_timeout
- match:
path: /api/email/verification-code/send
route:
cluster: email_api_cluster
timeout: *default_route_timeout
typed_per_filter_config: *public_route_ext_authz_disabled
- match:
prefix: /api/email
route:
cluster: email_api_cluster
timeout: *default_route_timeout
- match:
prefix: /
direct_response:
status: 404
body:
inline_string: "gateway route not found"
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
local TOKEN_HEADER = "xsrf-token"
local TOKEN_COOKIE = "__Host-XSRF-TOKEN"
local GUARD_COOKIE = "__Host-XSRF-GUARD"
local seeded = false
local function seed_random()
if seeded then
return
end
seeded = true
math.randomseed(os.time())
end
local function split_cookie(header)
local out = {}
if not header then
return out
end
for pair in string.gmatch(header, "([^;]+)") do
local key, value = string.match(pair, "^%s*([^=]+)=?(.*)$")
if key ~= nil and value ~= nil then
out[string.lower(key)] = value
end
end
return out
end
local function is_safe_method(method)
return method == "GET" or method == "HEAD" or method == "OPTIONS"
end
local function build_token(request_id)
seed_random()
local rnd = tostring(math.random(100000, 999999))
local rid = request_id or "rid"
return tostring(os.time()) .. "-" .. rid .. "-" .. rnd
end
function envoy_on_request(request_handle)
local headers = request_handle:headers()
local method = headers:get(":method")
local cookie_header = headers:get("cookie")
local cookies = split_cookie(cookie_header)
local token_cookie = cookies[string.lower(TOKEN_COOKIE)]
local guard_cookie = cookies[string.lower(GUARD_COOKIE)]
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", token_cookie == nil or token_cookie == "")
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", guard_cookie == nil or guard_cookie == "")
if token_cookie == nil or token_cookie == "" then
token_cookie = build_token(headers:get("x-request-id"))
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie)
else
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie)
end
if guard_cookie == nil or guard_cookie == "" then
guard_cookie = build_token(headers:get("x-request-id"))
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie)
else
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie)
end
if is_safe_method(method) then
return
end
local token_header = headers:get(TOKEN_HEADER)
if token_header == nil or token_header == "" then
request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"missing XSRF-TOKEN header"}'
)
return
end
if token_cookie == nil or token_cookie == "" or guard_cookie == nil or guard_cookie == "" then
request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"missing csrf cookies"}'
)
return
end
if token_header ~= token_cookie then
request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"xsrf token mismatch"}'
)
return
end
end
function envoy_on_response(response_handle)
local metadata = response_handle:streamInfo():dynamicMetadata():get("csrf")
if metadata == nil then
return
end
local token_value = metadata["token_value"]
local guard_value = metadata["guard_value"]
if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then
response_handle:headers():add(
"set-cookie",
TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure"
)
end
if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then
response_handle:headers():add(
"set-cookie",
GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure; HttpOnly"
)
end
end
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
juwan_user_jwt:
issuer: "juwan-user-rpc"
from_cookies:
- "JToken"
local_jwks:
inline_string: '{"keys":[{"kty":"oct","k":"MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ","alg":"HS256","use":"sig","kid":"juwan-hs256-1"}]}'
forward: false
claim_to_headers:
- header_name: "x-auth-user-id"
claim_name: "UserId"
- header_name: "x-auth-is-admin"
claim_name: "IsAdmin"
rules:
- match:
path: "/healthz"
- match:
path: "/api/users/login"
- match:
path: "/api/users/register"
- match:
path: "/api/email/verification-code/send"
- match:
prefix: "/api/users"
requires: &jwt_required
provider_name: juwan_user_jwt
- match:
prefix: "/api/email"
requires: *jwt_required
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
failure_mode_allow: false
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
grpc_service:
envoy_grpc:
cluster_name: authz_adapter_cluster
timeout: 0.5s
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: user_api_cluster
connect_timeout: 2s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: user_api_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: user-api-svc.juwan.svc.cluster.local
port_value: 8888
- name: email_api_cluster
connect_timeout: 2s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: email_api_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: email-api-svc.juwan.svc.cluster.local
port_value: 8888
- name: authz_adapter_cluster
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: authz_adapter_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: authz-adapter-svc.juwan.svc.cluster.local
port_value: 9002
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address:
address: 0.0.0.0
port_value: 9901
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy-gateway
namespace: juwan
labels:
app: envoy-gateway
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: envoy-gateway
template:
metadata:
labels:
app: envoy-gateway
spec:
serviceAccountName: envoy-gateway
containers:
- name: envoy
image: envoyproxy/envoy:v1.31-latest
imagePullPolicy: IfNotPresent
command: ["/usr/local/bin/envoy"]
args:
- "-c"
- "/etc/envoy/envoy.yaml"
- "--log-level"
- "info"
ports:
- containerPort: 8080
name: http
- containerPort: 9901
name: admin
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
volumes:
- name: envoy-config
configMap:
name: envoy-config
---
apiVersion: v1
kind: Service
metadata:
name: envoy-gateway
namespace: juwan
spec:
selector:
app: envoy-gateway
ports:
- name: http
port: 80
targetPort: 8080
- name: admin
port: 9901
targetPort: 9901
type: ClusterIP
+138 -34
View File
@@ -21,6 +21,16 @@ data:
codec_type: AUTO codec_type: AUTO
generate_request_id: true generate_request_id: true
use_remote_address: true use_remote_address: true
internal_address_config:
cidr_ranges:
- address_prefix: 10.0.0.0
prefix_len: 8
- address_prefix: 172.16.0.0
prefix_len: 12
- address_prefix: 192.168.0.0
prefix_len: 16
- address_prefix: 127.0.0.0
prefix_len: 8
route_config: route_config:
name: local_route name: local_route
virtual_hosts: virtual_hosts:
@@ -33,31 +43,68 @@ data:
status: 200 status: 200
body: body:
inline_string: ok inline_string: ok
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match: - match:
prefix: /api/email path: /api/users/login
route: route:
cluster: email_api_cluster cluster: user_api_cluster
timeout: 30s timeout: 30s
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match:
path: /api/users/register
route:
cluster: user_api_cluster
timeout: 30s
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match: - match:
prefix: /api/users prefix: /api/users
route: route:
cluster: user_api_cluster cluster: user_api_cluster
timeout: 30s timeout: 30s
- match:
path: /api/email/verification-code/send
route:
cluster: email_api_cluster
timeout: 30s
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match:
prefix: /api/email
route:
cluster: email_api_cluster
timeout: 30s
- match: - match:
prefix: / prefix: /
direct_response: direct_response:
status: 404 status: 404
body: body:
inline_string: "gateway route not found" inline_string: "gateway route not found"
http_filters: http_filters:
- name: envoy.filters.http.lua - name: envoy.filters.http.lua
typed_config: typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: | inline_code: |
local TOKEN_COOKIE = "csrf_token" local TOKEN_HEADER = "xsrf-token"
local GUARD_COOKIE = "csrf_guard" local TOKEN_COOKIE = "__Host-XSRF-TOKEN"
local TOKEN_HEADER = "x-csrf-token" local GUARD_COOKIE = "__Host-XSRF-GUARD"
local GUARD_HEADER = "x-csrf-guard"
local seeded = false local seeded = false
@@ -100,42 +147,41 @@ data:
local cookie_header = headers:get("cookie") local cookie_header = headers:get("cookie")
local cookies = split_cookie(cookie_header) local cookies = split_cookie(cookie_header)
local csrf_token_cookie = cookies[TOKEN_COOKIE] local token_cookie = cookies[string.lower(TOKEN_COOKIE)]
local csrf_guard_cookie = cookies[GUARD_COOKIE] local guard_cookie = cookies[string.lower(GUARD_COOKIE)]
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", csrf_token_cookie == nil or csrf_token_cookie == "") request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", token_cookie == nil or token_cookie == "")
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", csrf_guard_cookie == nil or csrf_guard_cookie == "") request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", guard_cookie == nil or guard_cookie == "")
if csrf_token_cookie == nil or csrf_token_cookie == "" then if token_cookie == nil or token_cookie == "" then
csrf_token_cookie = build_token(headers:get("x-request-id")) token_cookie = build_token(headers:get("x-request-id"))
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", csrf_token_cookie) request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie)
else else
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", csrf_token_cookie) request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie)
end end
if csrf_guard_cookie == nil or csrf_guard_cookie == "" then if guard_cookie == nil or guard_cookie == "" then
csrf_guard_cookie = build_token(headers:get("x-request-id")) guard_cookie = build_token(headers:get("x-request-id"))
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", csrf_guard_cookie) request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie)
else else
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", csrf_guard_cookie) request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie)
end end
if is_safe_method(method) then if is_safe_method(method) then
return return
end end
local csrf_token_header = headers:get(TOKEN_HEADER) local token_header = headers:get(TOKEN_HEADER)
local csrf_guard_header = headers:get(GUARD_HEADER)
if csrf_token_header == nil or csrf_guard_header == nil then if token_header == nil or token_header == "" then
request_handle:respond( request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"}, {[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"missing csrf headers"}' '{"code":403,"message":"missing XSRF-TOKEN header"}'
) )
return return
end end
if csrf_token_cookie == nil or csrf_guard_cookie == nil then if token_cookie == nil or token_cookie == "" or guard_cookie == nil or guard_cookie == "" then
request_handle:respond( request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"}, {[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"missing csrf cookies"}' '{"code":403,"message":"missing csrf cookies"}'
@@ -143,10 +189,10 @@ data:
return return
end end
if csrf_token_header ~= csrf_token_cookie or csrf_guard_header ~= csrf_guard_cookie then if token_header ~= token_cookie then
request_handle:respond( request_handle:respond(
{[":status"] = "403", ["content-type"] = "application/json"}, {[":status"] = "403", ["content-type"] = "application/json"},
'{"code":403,"message":"csrf token mismatch"}' '{"code":403,"message":"xsrf token mismatch"}'
) )
return return
end end
@@ -164,17 +210,65 @@ data:
if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then
response_handle:headers():add( response_handle:headers():add(
"set-cookie", "set-cookie",
TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; SameSite=Strict" TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure"
) )
end end
if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then
response_handle:headers():add( response_handle:headers():add(
"set-cookie", "set-cookie",
GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; SameSite=Strict" GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure; HttpOnly"
) )
end end
end end
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
juwan_user_jwt:
issuer: "juwan-user-rpc"
from_cookies:
- "JToken"
local_jwks:
inline_string: '{"keys":[{"kty":"oct","k":"MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ","alg":"HS256","use":"sig","kid":"juwan-hs256-1"}]}'
forward: false
claim_to_headers:
- header_name: "x-auth-user-id"
claim_name: "UserId"
- header_name: "x-auth-is-admin"
claim_name: "IsAdmin"
rules:
- match:
path: "/healthz"
- match:
path: "/api/users/login"
- match:
path: "/api/users/register"
- match:
path: "/api/email/verification-code/send"
- match:
prefix: "/api/users"
requires:
provider_name: juwan_user_jwt
- match:
prefix: "/api/email"
requires:
provider_name: juwan_user_jwt
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
failure_mode_allow: false
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
grpc_service:
envoy_grpc:
cluster_name: authz_adapter_cluster
timeout: 0.5s
- name: envoy.filters.http.router - name: envoy.filters.http.router
typed_config: typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
@@ -193,6 +287,7 @@ data:
socket_address: socket_address:
address: user-api-svc.juwan.svc.cluster.local address: user-api-svc.juwan.svc.cluster.local
port_value: 8888 port_value: 8888
- name: email_api_cluster - name: email_api_cluster
connect_timeout: 2s connect_timeout: 2s
type: STRICT_DNS type: STRICT_DNS
@@ -207,6 +302,21 @@ data:
address: email-api-svc.juwan.svc.cluster.local address: email-api-svc.juwan.svc.cluster.local
port_value: 8888 port_value: 8888
- name: authz_adapter_cluster
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: authz_adapter_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: authz-adapter-svc.juwan.svc.cluster.local
port_value: 9002
admin: admin:
access_log_path: /tmp/admin_access.log access_log_path: /tmp/admin_access.log
address: address:
@@ -233,6 +343,7 @@ spec:
labels: labels:
app: envoy-gateway app: envoy-gateway
spec: spec:
serviceAccountName: envoy-gateway
containers: containers:
- name: envoy - name: envoy
image: envoyproxy/envoy:v1.31-latest image: envoyproxy/envoy:v1.31-latest
@@ -260,13 +371,6 @@ spec:
port: 8080 port: 8080
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 10 periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts: volumeMounts:
- name: envoy-config - name: envoy-config
mountPath: /etc/envoy mountPath: /etc/envoy
+2 -2
View File
@@ -12,10 +12,10 @@ spec:
s3Credentials: s3Credentials:
accessKeyId: accessKeyId:
name: rc-creds name: rc-creds
key: SOucqRaJr4OyfcIu key: ACCESS_KEY_ID
secretAccessKey: secretAccessKey:
name: rc-creds name: rc-creds
key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz key: SECRET_ACCESS_KEY
wal: wal:
compression: gzip compression: gzip
storage: storage:
-424
View File
@@ -1,424 +0,0 @@
# JWT Secret + ETCD Encryption Deployment Guide
完整的 JWT 认证系统部署指南,包括密钥管理、RBAC 权限控制和 ETCD 加密。
## 部署顺序
### 第1步:创建 Secret 和 RBAC(必需)
创建 JWT 秘钥和服务账户权限:
```bash
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
```
验证创建成功:
```bash
# 检查 Secret
kubectl get secret jwt-secret -n juwan
kubectl get secret jwt-secret -n juwan -o yaml
# 检查 ServiceAccounts
kubectl get sa user-rpc -n juwan
kubectl get sa envoy-gateway -n juwan
# 检查 RBAC 权限
kubectl get role jwt-secret-reader -n juwan
kubectl get rolebinding -n juwan -l app=jwt-secret-reader
```
### 第2步:更新 user-rpc 部署(依赖第1步)
已自动更新 `deploy/k8s/service/user/user-rpc.yaml`
- ✅ 更新 `serviceAccountName``find-endpoints``user-rpc`
- ✅ 添加环境变量 `JWT_SECRET_KEY` 从 Secret `jwt-secret` 读取
应用更新:
```bash
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
```
验证部署:
```bash
# 检查 ServiceAccount 已正确绑定
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 5 serviceAccountName
# 查看 Pod 是否以 user-rpc ServiceAccount 身份运行
kubectl get pod -n juwan -l app=user-rpc -o yaml | grep serviceAccount
# 验证环境变量已注入
kubectl exec -it POD_NAME -n juwan -- env | grep JWT_SECRET_KEY
```
### 第3步:更新 Envoy 网关部署(依赖第1步)
已自动更新 `deploy/k8s/envoy/envoy.yaml`
- ✅ 添加 `serviceAccountName: envoy-gateway` 到 Deployment spec
应用更新:
```bash
kubectl apply -f deploy/k8s/envoy/envoy.yaml
```
验证部署:
```bash
# 检查 ServiceAccount 已正确绑定
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
# 检查 Pod 状态
kubectl get pod -n juwan -l app=envoy-gateway
```
### 第4步:启用 ETCD 加密(强烈推荐用于生产环境)
这是一个集群级别的配置,需要在 Kubernetes 控制平面节点上执行。
**前提条件:**
- 具有 Kubernetes 集群管理员权限
- 可以访问控制平面节点
- 备份 ETCD 数据库
**步骤:**
1. **生成加密密钥**
```bash
head -c 32 /dev/urandom | base64
```
记录输出的 Base64 密钥。
2. **创建加密配置文件**
在控制平面节点上,创建 `/etc/kubernetes/encryption-config.yaml`
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_ENCODED_32_BYTE_KEY>
- identity: {}
```
替换 `<BASE64_ENCODED_32_BYTE_KEY>` 为第1步生成的密钥。
3. **修改 kube-apiserver 配置**
在控制平面节点上,编辑 `/etc/kubernetes/manifests/kube-apiserver.yaml`
**添加参数:**
```yaml
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
```
**添加卷挂载:**
```yaml
spec:
containers:
- name: kube-apiserver
volumeMounts:
- name: encryption-config
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: encryption-config
hostPath:
path: /etc/kubernetes
type: DirectoryOrCreate
```
4. **重启 kube-apiserver**
修改清单后,kubelet 会自动重启 kube-apiserver。检查状态:
```bash
# 在控制平面节点
kubectl get pods -n kube-system | grep kube-apiserver
# 监控重启过程
kubectl logs -n kube-system -l component=kube-apiserver -f
```
5. **验证加密是否启用**
创建一个新 Secret 并检查它在 ETCD 中是否加密:
```bash
# 创建测试 Secret
kubectl create secret generic test-secret -n juwan --from-literal=key=value
# 从 control plane 节点检查 ETCD 数据
# 如果数据不可读并包含加密标记,说明加密已启用
```
6. **保存加密密钥**
⚠️ **关键:将加密密钥安全地保存在离线存储中**
- 密钥丢失后,无法解密 ETCD 中的数据
- 无法恢复任何 Secrets
- 建议用密码管理工具(如 HashiCorp Vault)或 HSM 存储密钥
### 第5步:验证整个系统
完整的验证清单:
```bash
# 检查所有 Secrets 已创建
kubectl get secret -n juwan
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d
# 检查 ServiceAccounts 已创建
kubectl get sa -n juwan
kubectl describe sa user-rpc -n juwan
kubectl describe sa envoy-gateway -n juwan
# 检查 RBAC 权限
kubectl get role -n juwan
kubectl get rolebinding -n juwan
kubectl describe role jwt-secret-reader -n juwan
# 测试权限:user-rpc 可以读 jwt-secret
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:user-rpc --resource-name=jwt-secret -n juwan
# 测试权限:envoy-gateway 可以读 jwt-secret
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:envoy-gateway --resource-name=jwt-secret -n juwan
# 测试权限:其他 ServiceAccount 无法读取
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:other-service -n juwan
# 检查 Deployments 已正确配置
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 2 serviceAccountName
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
# 检查 Pods 是否已启动并运行
kubectl get pods -n juwan -l app=user-rpc
kubectl get pods -n juwan -l app=envoy-gateway
# 查看 JWT Secret 是否已挂载到 Pod
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep JWT_SECRET_KEY
```
## 监控和日志
### 查看 Pod 日志
```bash
# user-rpc 日志
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
# Envoy 日志
kubectl logs -n juwan -l app=envoy-gateway -f
```
### 检查 Pod 事件
```bash
# 查看 Pod 创建和启动事件
kubectl describe pod -n juwan -l app=user-rpc
kubectl describe pod -n juwan -l app=envoy-gateway
```
### 权限问题排查
如果 Pod 无法读取 Secret
```bash
# 检查 Pod 使用的 ServiceAccount
kubectl get pod POD_NAME -n juwan -o yaml | grep serviceAccountName
# 检查 RBAC 绑定
kubectl get rolebinding -n juwan -o wide
# 检查 Role 权限定义
kubectl get role jwt-secret-reader -n juwan -o yaml
# 尝试用 Pod 的身份读取 Secret(需要 kubectl-user-impersonate 或类似工具)
kubectl get secret jwt-secret --as=system:serviceaccount:juwan:user-rpc -n juwan
```
## 安全最佳实践
### 1. 密钥轮换
周期性更换 JWT 秘钥(建议每季度):
```bash
# 1. 生成新密钥
NEW_KEY=$(head -c 32 /dev/urandom | base64)
# 2. 更新 Secret
kubectl create secret generic jwt-secret \
--from-literal=secret-key=$NEW_KEY \
--dry-run=client -o yaml | kubectl apply -f -
# 3. 重启 Pods 以加载新密钥(滚动更新)
kubectl rollout restart deployment/user-rpc -n juwan
kubectl rollout restart deployment/envoy-gateway -n juwan
# 4. 验证新 Pods 已启动并运行
kubectl rollout status deployment/user-rpc -n juwan
kubectl rollout status deployment/envoy-gateway -n juwan
# 5. 已颁发的旧令牌将变为无效
# 需要用户重新登录获取新令牌
```
### 2. 审计和监控
在生产环境中启用 Kubernetes 审计日志来跟踪 Secret 访问:
```yaml
# kube-apiserver 审计策略示例
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# 记录 secret 资源的访问
- level: RequestResponse
verbs: ["get", "list", "watch"]
resources: ["secrets"]
# 记录所有认证失败
- level: RequestResponse
omitStages:
- RequestReceived
userGroups: ["system:unauthenticated"]
- level: Metadata
omitStages:
- RequestReceived
```
### 3. 网络策略
使用 NetworkPolicy 限制 Pods 之间的通信:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: jwt-secret-access
namespace: juwan
spec:
podSelector:
matchLabels:
app: jwt-secret-reader
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: user-rpc
- podSelector:
matchLabels:
app: envoy-gateway
ports:
- protocol: TCP
port: 443 # API Server
```
## 灾难恢复
### 备份 Secret 和 RBAC 配置
```bash
# 备份 JWT Secret
kubectl get secret jwt-secret -n juwan -o yaml > jwt-secret-backup.yaml
# 备份 RBAC 配置
kubectl get role jwt-secret-reader -n juwan -o yaml > jwt-role-backup.yaml
kubectl get rolebinding -n juwan -l app=jwt-secret-reader -o yaml > jwt-rolebinding-backup.yaml
# 加密备份文件
gpg --symmetric jwt-secret-backup.yaml
```
### 恢复步骤
如果 Secret 被意外删除:
```bash
# 从备份恢复
kubectl apply -f jwt-secret-backup.yaml
# 重启 Pods 以重新加载 Secret
kubectl rollout restart deployment/user-rpc -n juwan
kubectl rollout restart deployment/envoy-gateway -n juwan
```
## 常见问题
### Q: Pod 无法启动,显示 "failed to pull secret"
A: 检查:
1. Secret 是否存在:`kubectl get secret jwt-secret -n juwan`
2. ServiceAccount 是否绑定了 RBAC`kubectl describe rolebinding -n juwan`
3. Secret 名称和命名空间是否正确
### Q: 加密后如何验证 ETCD 中的数据已加密?
A: 从 control plane 节点:
```bash
# 直接读取 ETCD(如果配置了加密,数据应该不可读)
sudo strings /var/lib/etcd/member/snap/db | grep -i secret
```
### Q: 能否更改加密密钥而不重新创建 ETCD?
A: 可以,但流程复杂:
1. 更新 encryption-config.yaml 中的新密钥
2. 将新密钥添加到提供程序列表(保持旧密钥)
3. 重启 kube-apiserver
4. 触发重新加密:`kubectl get all --all-namespaces -o json | kubectl apply -f -`
### Q: 如何在 Minikube 中启用 ETCD 加密?
A: 参考 `ENCRYPTION.md` 中的 Minikube 特定说明部分。
## 相关文件
- `jwt-secret.yaml` - Secret 和 RBAC 配置
- `ENCRYPTION.md` - ETCD 加密详细文档
- `README.md` - 快速参考指南
- `/deploy/k8s/service/user/user-rpc.yaml` - user-rpc Deployment 配置
- `/deploy/k8s/envoy/envoy.yaml` - Envoy 网关 Deployment 配置
## 下一步
部署完成后:
1. **集成 JWT 验证到 RPC Handlers**
- 实现 gRPC unary interceptor
- 验证令牌有效性
- 处理令牌刷新逻辑
2. **集成 JWT 验证到 Envoy**
- 扩展 Lua filter 进行令牌验证
- 返回 401(无效令牌)或 200(有效令牌)
3. **端到端测试**
- 创建用户和登录
- 生成和验证 JWT
- 测试令牌刷新和撤销
- 验证 ETCD 加密
4. **生产部署**
- 启用审计日志
- 配置密钥轮换计划
- 建立备份和恢复流程
- 监控 Secret 访问
-129
View File
@@ -1,129 +0,0 @@
# ETCD Encryption Configuration for Kubernetes
To enable static encryption at rest for Kubernetes secrets in ETCD, you need to configure the API Server with an EncryptionConfiguration.
## 1. Generate an Encryption Key
```bash
# Generate a 32-byte base64-encoded key
head -c 32 /dev/urandom | base64
# Example output: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
```
## 2. Create EncryptionConfiguration File
Create `/etc/kubernetes/encryption-config.yaml` on the control plane node:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
- identity: {}
```
## 3. Update kube-apiserver Static Pod Manifest
Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` on the control plane node:
```yaml
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
# ... existing flags ...
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
volumeMounts:
- name: encryption-config
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: encryption-config
hostPath:
path: /etc/kubernetes
type: DirectoryOrCreate
```
## 4. Verify Encryption is Working
```bash
# After restarting the API server, create a secret and verify it's encrypted
kubectl create secret generic test-secret --from-literal=key=value -n juwan
# Check if the secret is encrypted in etcd
kubectl get secret test-secret -o yaml
# You can also check raw etcd data (requires etcd access):
# etcdctl --endpoints=https://127.0.0.1:2379 get /kubernetes.io/secrets/juwan/test-secret
# The data should be encrypted (not human-readable)
```
## 5. Important Notes
- **Backup your encryption key** in a secure location
- **Never commit encryption keys** to version control
- If you lose the key, all encrypted secrets will be unrecoverable
- After enabling encryption, existing unencrypted secrets will not be automatically encrypted
- To encrypt existing secrets, you can use: `kubectl delete secret <name> && kubectl create secret ...`
- Or use: `kubectl patch secret <name> -p '{}' --type=merge` (triggers re-encryption)
## 6. RBAC Configuration for JWT Secret
The `jwt-secret.yaml` includes RBAC rules that:
- Create a `jwt-secret` Secret in the `juwan` namespace
- Create ServiceAccounts for `user-rpc` and `envoy-gateway`
- Create a Role `jwt-secret-reader` that allows reading only the `jwt-secret` Secret
- Bind this Role to both ServiceAccounts via RoleBindings
This ensures:
- Only `user-rpc` and `envoy-gateway` Pods can read the JWT secret
- Other services and users cannot access the JWT secret
- Least privilege access principle is enforced
## 7. Update Deployment to Use ServiceAccount
Make sure your Deployment references the ServiceAccount:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-rpc
namespace: juwan
spec:
template:
spec:
serviceAccountName: user-rpc # This is important!
containers:
- name: user-rpc
env:
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: jwt-secret
key: secret-key
```
## 8. For Minikube Users
If using Minikube, you can enable encryption with:
```bash
minikube config set apiserver.encryption-provider-config /path/to/encryption-config.yaml
minikube start
```
Or manually edit the kube-apiserver manifest after starting Minikube:
```bash
minikube ssh
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
# Add the flags and volume mounts as shown above
```
-415
View File
@@ -1,415 +0,0 @@
# 部署流程图和时间线
## 部署架构流程图
```
┌──────────────────────────────────────────────────────────────────┐
│ JWT 认证系统部署流程 │
└──────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Phase 1: 前置检查 (5分钟) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✓ Kubernetes 集群版本 >= 1.24 │
│ ✓ kubectl 已配置,可访问集群 │
│ ✓ juwan namespace 已存在 │
│ ✓ redis-operator CRD 已安装 │
│ ✓ 集群管理员权限(用于 ETCD 加密) │
│ │
│ 命令检查: │
│ $ kubectl cluster-info │
│ $ kubectl get ns juwan │
│ $ kubectl get crd redisclusters.redis.redis.opstreelabs.in │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Phase 2: 创建 Secret 和 RBAC (5分钟) ⚡ 必需 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 执行: │
│ $ kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml │
│ │
│ 创建的资源: │
│ ✓ Secret: jwt-secret (包含 JWT 秘钥) │
│ ✓ ServiceAccount: user-rpc │
│ ✓ ServiceAccount: envoy-gateway │
│ ✓ Role: jwt-secret-reader (只读权限) │
│ ✓ RoleBinding: jwt-secret-reader-user-rpc │
│ ✓ RoleBinding: jwt-secret-reader-envoy-gateway │
│ │
│ 验证: │
│ $ kubectl get secret jwt-secret -n juwan │
│ $ kubectl get sa -n juwan | grep -E "user-rpc|envoy" │
│ $ kubectl get role -n juwan │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Phase 3: 更新 Deployments (10分钟) ⚡ 必需 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step 3a: 更新 user-rpc Deployment │
│ 执行: │
│ $ kubectl apply -f deploy/k8s/service/user/user-rpc.yaml │
│ │
│ 变更: │
│ - serviceAccountName: user-rpc (绑定权限) │
│ - env.JWT_SECRET_KEY (从 Secret 挂载) │
│ - 保持 Redis Cluster 配置 │
│ │
│ 等待 Pods 启动: │
│ $ kubectl rollout status deployment/user-rpc -n juwan │
│ │
│ --- │
│ │
│ Step 3b: 更新 Envoy Gateway Deployment │
│ 执行: │
│ $ kubectl apply -f deploy/k8s/envoy/envoy.yaml │
│ │
│ 变更: │
│ - serviceAccountName: envoy-gateway (绑定权限) │
│ - 保持 CSRF Lua 防护配置 │
│ │
│ 等待 Pods 启动: │
│ $ kubectl rollout status deployment/envoy-gateway -n juwan │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Phase 4: 验证部署 (15分钟) ⚡ 必需 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 检查清单: │
│ │
│ 1️⃣ Secret 和权限验证 │
│ $ kubectl get secret jwt-secret -n juwan │
│ $ kubectl get role jwt-secret-reader -n juwan │
│ $ kubectl get rolebinding -n juwan | grep jwt-secret │
│ │
│ 2️⃣ 权限测试 │
│ $ kubectl auth can-i get secrets \ │
│ --as=system:serviceaccount:juwan:user-rpc \ │
│ --resource-name=jwt-secret -n juwan │
│ 预期: yes │
│ │
│ 3️⃣ Pods 运行状态 │
│ $ kubectl get pods -n juwan -l app=user-rpc │
│ $ kubectl get pods -n juwan -l app=envoy-gateway │
│ 预期: 3 个 user-rpc Pods + 1 个 envoy-gateway Pod 都在 Running │
│ │
│ 4️⃣ 环境变量验证 │
│ $ kubectl exec -it <user-rpc-pod> -n juwan -- env | grep JWT │
│ 预期: JWT_SECRET_KEY=... │
│ │
│ 5️⃣ Redis 连接验证 │
│ $ kubectl run redis-cli --image=redis:latest --rm -it \ │
│ -- redis-cli -h user-redis.juwan:6379 PING │
│ 预期: PONG │
│ │
│ 详见: VERIFICATION.md 第1-8部分 │
│ │
└─────────────────────────────────────────────────────────────────────┘
├─────────── 生产环境额外步骤 ──────────────┐
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────────────────┐
│ Phase 5a: 应用集成 │ │ Phase 5b: 启用 ETCD 加密 (30分钟) │
│ (2-3 小时) ⚠️ 推荐 │ │ ⚠️ 生产推荐,需集群管理员权限 │
├──────────────────────────┤ ├─────────────────────────────────────┤
│ │ │ │
│ 实施内容: │ │ 前提条件: │
│ ✓ gRPC Interceptor │ │ ✓ Control Plane 节点访问权限 │
│ ✓ Login/Logout Handler │ │ ✓ ETCD 备份已创建 │
│ ✓ JWT Middleware │ │ ✓ 加密密钥已生成 │
│ ✓ Token Refresh Logic │ │ │
│ ✓ Error Handling │ │ 步骤: │
│ ✓ Unit Tests │ │ 1. 生成 32 字节密钥 │
│ │ │ $ head -c 32 /dev/urandom | base64
│ 参考: │ │ │
│ INTEGRATION.md │ │ 2. 创建加密配置文件 │
│ │ │ /etc/kubernetes/encryption-config.yaml
│ 时间估计: │ │ │
│ - gRPC 拦截器: 30分钟 │ │ 3. 修改 kube-apiserver manifest │
│ - Handlers: 60分钟 │ │ 添加密钥路径和卷挂载 │
│ - Middleware: 30分钟 │ │ │
│ - 测试: 60分钟 │ │ 4. 重启 kube-apiserver │
│ │ │ kubelet 自动重启 │
│ │ │ │
│ │ │ 5. 验证加密已启用 │
│ │ │ kubectl create secret generic ... │
│ │ │ 检查 ETCD 中的数据是否加密 │
│ │ │ │
│ │ │ 详见: ENCRYPTION.md (8个部分) │
│ │ │ 验证: VERIFICATION.md 第9部分 │
│ │ │ │
└──────────────────────────┘ └─────────────────────────────────────┘
│ │
└───────────────────┬─────────────────────┘
┌──────────────────────────────────────────┐
│ Phase 6: 完成 ✅ │
├──────────────────────────────────────────┤
│ │
│ 最终检查: │
│ ✓ 所有 Pods 运行正常 │
│ ✓ RBAC 权限已验证 │
│ ✓ JWT 功能已集成 │
│ ✓ 日志和监控已配置 │
│ ✓ (可选) ETCD 加密已启用 │
│ │
│ 生产推荐: │
│ ✓ 启用审计日志 │
│ ✓ 配置密钥轮换计划(季度) │
│ ✓ 备份密钥到安全位置 │
│ ✓ 配置告警和监控 │
│ │
└──────────────────────────────────────────┘
```
## 时间估计和路径
```
推荐部署路径
═════════════════════════════════════════════════════════════════
🚀 最快路径 (30 分钟) - 开发环境
────────────────────────────────────────────────────────────────
Phase 1: 前置检查 ⏱️ 5 分钟
Phase 2: 创建 Secret 和 RBAC ⏱️ 5 分钟
Phase 3: 更新 Deployments ⏱️ 10 分钟
Phase 4: 验证部署 ⏱️ 10 分钟
────────────────────────────────────────────────────────────────
总计: 30 分钟
📊 默认路径 (75 分钟) - 测试环境
────────────────────────────────────────────────────────────────
Phase 1-4: 如上 ⏱️ 30 分钟
Phase 5a: 应用集成(简单版) ⏱️ 45 分钟
────────────────────────────────────────────────────────────────
总计: 75 分钟
🏆 完整路径 (3.5-4 小时) - 生产环境
────────────────────────────────────────────────────────────────
Phase 1-4: 如上 ⏱️ 30 分钟
Phase 5a: 应用集成(完整版) ⏱️ 2-3 小时
Phase 5b: ETCD 加密配置 ⏱️ 30 分钟
────────────────────────────────────────────────────────────────
总计: 3.5-4 小时
📚 学习路径 (6-8 小时) - 从零开始理解
────────────────────────────────────────────────────────────────
文档阅读 (SUMMARY + DEPLOYMENT + INTEGRATION) ⏱️ 1-2 小时
完整路径部署 ⏱️ 3.5-4 小时
验证和测试 ⏱️ 1-2 小时
────────────────────────────────────────────────────────────────
总计: 6-8 小时
```
## 并行和串行步骤
```
可以并行执行的任务
═════════════════════════════════════════════════════════════════
┌─────────────────────────────────┐
│ 应用集成 (Phase 5a) │ ────┐
├─────────────────────────────────┤ │ 可选,独立
│ • gRPC interceptor │ │ 进行
│ • REST middleware │ │
│ • Handler 实现 │ │
│ • 单元测试 │ │
└─────────────────────────────────┘ │
├─ 与 Phase 2-4 并行
┌─────────────────────────────────┐ │
│ ETCD 加密 (Phase 5b) │ ────┘
├─────────────────────────────────┤ │ 需要集群管理员
│ • 生成密钥(可单独进行) │ │ 权限,在
│ • 创建配置文件 │ │ Control Plane
│ • 修改 kube-apiserver │ │ 节点执行
│ • 重启 API server │ │
└─────────────────────────────────┘────┘
必须串行执行的步骤
═════════════════════════════════════════════════════════════════
Phase 1 → Phase 2 → Phase 3 → Phase 4 → (Phase 5a + Phase 5b)
↓ ↓ ↓ ↓
前置检查 创建资源 部署应用 验证完整 可选扩展功能
• Phase 2 必须在 Phase 1 之后(需要 namespace
• Phase 3 必须在 Phase 2 之后(需要 RoleBinding
• Phase 4 必须在 Phase 3 之后(需要 Pods 启动)
• Phase 5a/5b 可在 Phase 4 完成后并行进行
```
## 关键时间点
```
事件时间线
═════════════════════════════════════════════════════════════════
T+0 Phase 1: 验证前置条件
└─ 预计 5 分钟
T+5 Phase 2: kubectl apply jwt-secret.yaml
└─ 预计 1 分钟执行,5 分钟验证
T+11 Phase 3a: kubectl apply user-rpc.yaml
└─ 3 个 Pods 启动(滚动更新)
└─ 预计 ~3 分钟(取决于镜像拉取)
T+14 Phase 3b: kubectl apply envoy.yaml
└─ 1 个 Pod 启动
└─ 预计 ~2 分钟
T+16 Phase 4: 执行完整验证检查
└─ 12 个验证部分,共 ~15 分钟
T+31 ✅ 基础部署完成
T+31 (可选) Phase 5a: 应用代码集成
到 └─ 2-3 小时编码和测试
T+211
T+31 (可选) Phase 5b: ETCD 加密
到 └─ 30 分钟配置
T+61
T+211 或 T+61 🎉 全部完成
(取决于是否执行 Phase 5)
```
## 推荐的部署顺序
### 对于 DevOps/SRE
```
优先级顺序:
1️⃣ Phase 1-4 (核心部署) [必需]
└─ 时间: 30 分钟
2️⃣ Phase 5b (ETCD 加密) [生产强烈推荐]
└─ 时间: 30 分钟
└─ 开始时间: T+16 之前 (与 Phase 4 并行)
3️⃣ 密钥备份和恢复计划 [重要]
└─ 时间: 15 分钟
└─ 参考: DEPLOYMENT.md 灾难恢复
4️⃣ Phase 5a 支持 [当开发完成时]
└─ 协助开发团队集成 JWT
└─ 参考: INTEGRATION.md
```
### 对于应用开发者
```
优先级顺序:
1️⃣ 了解系统架构 [了解背景]
└─ 文档: SUMMARY.md
└─ 时间: 10 分钟
2️⃣ Phase 5a: 代码集成 [并行进行]
└─ 参考: INTEGRATION.md
└─ 时间: 2-3 小时
3️⃣ 单元测试 [在开发中]
└─ 参考: INTEGRATION.md 第 9 部分
└─ 时间: 1 小时
4️⃣ 集成测试 [与运维协调]
└─ 测试完整流程
└─ 时间: 1-2 小时
```
## 回滚应急步骤
如果部署失败,可以快速回滚:
```
紧急回滚
═════════════════════════════════════════════════════════════════
如果 Phase 2 (Secret) 失败:
→ kubectl delete secret jwt-secret -n juwan
→ kubectl delete sa user-rpc envoy-gateway -n juwan
→ kubectl delete role jwt-secret-reader -n juwan
→ 修正配置后重新应用
如果 Phase 3 (Deployment) 失败:
→ kubectl rollout undo deployment/user-rpc -n juwan
→ kubectl rollout undo deployment/envoy-gateway -n juwan
→ 或删除部署并使用稳定的旧版本重新部署
如果 Phase 5b (ETCD 加密) 失败:
→ 从 kube-apiserver 清单中移除加密参数
→ 重启 kube-apiserver
→ 删除 /etc/kubernetes/encryption-config.yaml
→ 从最近的 ETCD 备份恢复(如果需要)
注意: 备份是关键!每次重大操作前都应备份。
```
## 部署检查点 (Go/No-Go)
```
关键检查点
═════════════════════════════════════════════════════════════════
✅ Checkpoint 1 (Phase 2 后)
- Secret 已创建
- ServiceAccounts 已创建
- Go → 继续 Phase 3
- No-Go → 检查 kubectl 权限
✅ Checkpoint 2 (Phase 3 后)
- Pods 已启动 (Running)
- No PodSchedulingFailure
- Go → 继续 Phase 4
- No-Go → 检查资源限制和镜像拉取
✅ Checkpoint 3 (Phase 4 权限测试)
- user-rpc 可以读 jwt-secret
- envoy-gateway 可以读 jwt-secret
- 其他 SA 无法读取
- Go → 继续 Phase 5
- No-Go → 检查 RBAC 配置
✅ Checkpoint 4 (Phase 4 Redis 连接)
- Redis Cluster 健康 (3/3 nodes)
- PING 返回 PONG
- Go → 继续应用集成
- No-Go → 检查 Redis Pods 和网络
✅ Checkpoint 5 (Phase 5b - ETCD 加密)
- 新创建的 Secret 在 ETCD 中已加密
- Go → 生产就绪
- No-Go → 检查 kube-apiserver 配置参数
```
---
## 使用这个流程图
1. **首次部署** → 从 "推荐部署路径" 选择合适的版本
2. **卡在某一步** → 查看对应的 Phase 描述和命令
3. **估算时间** → 查看 "时间估计和路径" 部分
4. **需要回滚** → 参考 "回滚应急步骤"
5. **检查进度** → 使用 "部署检查点"
详细的部署步骤见:[DEPLOYMENT.md](./DEPLOYMENT.md)
-399
View File
@@ -1,399 +0,0 @@
# JWT + ETCD 加密系统文档索引
## 📚 文档完整导航
### 快速入门 (5-15分钟)
**推荐路径:** 从上到下顺序阅读
1. **[SUMMARY.md](./SUMMARY.md)** ⭐ 从这里开始
- 📋 项目概览和架构图
- 🎯 核心特性一览
- ✅ 生产就绪检查清单
- 🚀 下一步行动计划
2. **[README.md](./README.md)**
- 🏃 4个快速部署步骤
- 🔐 安全考虑事项
- 🔄 密钥轮换程序
- 🆘 故障排查
3. **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)**
- ⚡ 一页速查表
- 📝 常见命令复制粘贴
- 🗺️ 文档地图
- 🎓 关键参数速知
### 部署实施 (30-60分钟)
4. **[DEPLOYMENT.md](./DEPLOYMENT.md)** - 最详细的部署指南
- 📦 第1步:创建 Secret 和 RBAC(必需)
- 🔄 第2步:更新 user-rpc Deployment
- 🌐 第3步:更新 Envoy Gateway Deployment
- 🔐 第4步:启用 ETCD 加密(生产推荐)
- ✔️ 第5步:验证整个系统
- 📊 监控和日志配置
- 🛠️ 安全最佳实践
- 🆘 故障排查指南
- 💾 灾难恢复流程
### 验证和监控 (20-30分钟)
5. **[VERIFICATION.md](./VERIFICATION.md)** - 完整验证清单
**12个验证部分:**
| 部分 | 用途 | 时间 |
|-----|------|------|
| 第1部分 | Secret/RBAC 基础验证 | 2分钟 |
| 第2部分 | 权限测试(allow/deny | 3分钟 |
| 第3部分 | Deployment 配置检查 | 2分钟 |
| 第4部分 | Redis 连接测试 | 2分钟 |
| 第5部分 | 应用启动日志 | 3分钟 |
| 第6部分 | 网络和服务发现 | 3分钟 |
| 第7部分 | Prometheus 指标 | 3分钟 |
| 第8部分 | Loki 日志聚合 | 2分钟 |
| 第9部分 | ETCD 加密验证 | 5分钟 |
| 第10部分 | JWT 功能测试 | 10分钟 |
| 第11部分 | 故障排查诊断 | 5分钟 |
| 第12部分 | 清理和总结 | 2分钟 |
### 高级话题
6. **[ENCRYPTION.md](./ENCRYPTION.md)** - ETCD 加密完整指南
- 🔑 第1部分:密钥生成
- 📋 第2部分:配置格式
- 🔧 第3部分:kube-apiserver 修改
- ✅第4部分:验证加密
- ⚠️ 第5部分:关键警告(数据不可恢复)
- 🔐 第6部分:RBAC 解释
- 📦 第7部分:Deployment 示例
- 🍎 第8部分:Minikube 特定说明
7. **[INTEGRATION.md](../api/INTEGRATION.md)** - 代码集成指南
- 🔗 第1部分:gRPC Unary Interceptor
- 🔗 第2部分:gRPC Stream Interceptor
- 👤 第3部分:登录 Handler 实现
- 🔐 第4部分:受保护 Handler 中的声明提取
- 🔄 第5部分:令牌刷新端点
- 🚪 第6部分:登出处理
- 🛣️ 第7部分:REST Routes 配置
- 🔎 第8部分:错误处理最佳实践
- 🧪 第9部分:单元测试示例
---
## 📁 文件结构详解
```
deploy/k8s/
├── secrets/
│ ├── jwt-secret.yaml ✅ Kubernetes 清单文件
│ │ ├── Secret: jwt-secret (JWT 秘钥数据)
│ │ ├── ServiceAccount: user-rpc
│ │ ├── ServiceAccount: envoy-gateway
│ │ ├── Role: jwt-secret-reader
│ │ ├── RoleBinding: jwt-secret-reader-user-rpc
│ │ └── RoleBinding: jwt-secret-reader-envoy-gateway
│ │
│ ├── README.md 📖 快速参考指南(5分钟入门)
│ ├── SUMMARY.md 📊 系统概览(10分钟了解全貌)
│ ├── QUICK_REFERENCE.md ⚡ 速查表(查找命令和参数)
│ ├── DEPLOYMENT.md 📦 详细部署指南(60分钟完整部署)
│ ├── ENCRYPTION.md 🔐 ETCD 加密指南(Control Plane 配置)
│ ├── VERIFICATION.md ✅ 验证清单(部署后验证)
│ └── INDEX.md 🗺️ 本文件(文档导航)
└── envoy/
│ └── envoy.yaml ✅ Envoy 网关配置
│ └── 已更新: serviceAccountName: envoy-gateway
service/user/
├── user-api.yaml ✅ user-api Service
├── user-rpc.yaml ✅ user-rpc Deployment(已更新)
│ ├── serviceAccountName: user-rpc (已更新)
│ ├── JWT_SECRET_KEY env var (已更新)
│ └── Redis Cluster configuration
└── ...
app/users/
├── api/
│ └── INTEGRATION.md 📝 REST/gRPC 集成指南
└── rpc/
├── internal/utils/jwt.go ✅ JwtManager 实现(已存在)
├── internal/config/config.go ✅ JWT 配置(已存在)
├── internal/svc/
│ └── serviceContext.go ✅ 依赖注入(已存在)
└── etc/pb.yaml ✅ 运行时配置(已存在)
```
---
## 🎯 按场景查找文档
### 场景 1:我想快速了解这个系统是什么
**推荐阅读顺序:**
1. [SUMMARY.md](./SUMMARY.md) - 项目概览(5分钟)
2. [SUMMARY.md](./SUMMARY.md) 中的架构图和特性说明
**关键信息:**
- JWT 令牌系统 + Redis 存储 + RBAC 权限 + ETCD 加密
- 支持 7 天有效期、30 天可刷新
- Envoy 网关 CSRF 防护
---
### 场景 2:我想立即部署到 Kubernetes
**推荐阅读顺序:**
1. [README.md](./README.md) - 快速参考(2分钟)
2. [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - 复制粘贴命令(3分钟)
3. 运行部署命令(5分钟)
4. [VERIFICATION.md](./VERIFICATION.md) 第1-7部分 - 验证(10分钟)
**快速命令:**
```bash
# Copy from QUICK_REFERENCE.md "部署命令" 部分
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
kubectl apply -f deploy/k8s/envoy/envoy.yaml
```
---
### 场景 3:部署后验证一切正常
**推荐阅读:**
- [VERIFICATION.md](./VERIFICATION.md) - 12部分完整验证清单
- 逐部分执行验证命令
**预计时间:** 30-40分钟
**验证触发点:**
- ✅ Secrets 和 RBAC 已创建
- ✅ Pods 已启动运行
- ✅ 权限验证通过
- ✅ Redis 连接成功
---
### 场景 4:启用 ETCD 加密(生产推荐)
**推荐阅读顺序:**
1. [ENCRYPTION.md](./ENCRYPTION.md) - 完整加密指南
2. 按照 8 个步骤逐一执行
3. [VERIFICATION.md](./VERIFICATION.md) 第9部分 - 加密验证
**需要的权限:**
- Control Plane 节点的 root/sudo 权限
- Kubernetes 集群管理员权限
**预计时间:** 15-20分钟
---
### 场景 5:集成 JWT 到我的应用代码中
**推荐阅读顺序:**
1. [INTEGRATION.md](../api/INTEGRATION.md) 第1-2部分 - gRPC 拦截器
2. 第3-4部分 - 登录和受保护 Handlers
3. 第7-8部分 - REST API 中间件
4. 第9部分 - 单元测试
**需要实现:**
- ✅ gRPC Unary/Stream Interceptors
- ✅ 登录/登出端点
- ✅ JWT Middleware for REST
- ✅ 错误处理
**预计时间:** 2-3 小时
---
### 场景 6:部署后遇到问题
**根据错误类型选择:**
| 错误类型 | 查看文档 |
|---------|--------|
| Pod 无法启动 | [VERIFICATION.md](./VERIFICATION.md) 第11部分 |
| 权限被拒绝 | [VERIFICATION.md](./VERIFICATION.md) 第2部分 + [README.md](./README.md) 故障排查 |
| Redis 连接失败 | [VERIFICATION.md](./VERIFICATION.md) 第4部分 |
| ETCD 加密失败 | [ENCRYPTION.md](./ENCRYPTION.md) 第5-6部分 |
| 配置不清楚 | [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 配置文件位置 |
---
### 场景 7:定期维护任务
#### 任务:轮换 JWT 秘钥
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 安全最佳实践 > 密钥轮换
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 密钥轮换步骤
**频率:** 季度(3个月)
#### 任务:轮换 ETCD 加密密钥
**阅读:** [ENCRYPTION.md](./ENCRYPTION.md) 第5部分
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) ETCD 加密系统
**频率:** 年度(12个月)
#### 任务:备份密钥
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 灾难恢复
**或:** [ENCRYPTION.md](./ENCRYPTION.md) 关键警告
**频率:** 立即 + 每次轮换后
---
## 📊 文档深度对比
| 文档 | 深度 | 丰富度 | 代码 | 适合角色 |
|-----|------|--------|------|---------|
| README | 浅 | 概览 | - | PM/初学者 |
| SUMMARY | 浅 | 概览 | - | 决策者 |
| QUICK_REFERENCE | 中 | 速查 | 命令 | DevOps/SRE |
| DEPLOYMENT | 深 | 详细 | 示例 | DevOps/运维 |
| VERIFICATION | 深 | 详细 | 脚本 | QA/DevOps |
| ENCRYPTION | 非常深 | 极详细 | YAML | 安全/运维 |
| INTEGRATION | 非常深 | 代码级 | 完整 | 开发者 |
---
## 🔄 学习路径建议
### 对于 DevOps/SRE
1. SUMMARY.md (5 分钟)
2. DEPLOYMENT.md (30 分钟)
3. VERIFICATION.md (30 分钟)
4. ENCRYPTION.md (20 分钟)
5. 实践部署 (60 分钟)
**总计:** ~3 小时
### 对于应用开发者
1. SUMMARY.md > "集成点" 部分 (5 分钟)
2. INTEGRATION.md (60 分钟)
3. QUICK_REFERENCE.md > "JWT Manager API" (10 分钟)
4. 代码实现 (2-3 小时)
5. 单元测试 (INTEGRATION.md 第9部分)
**总计:** ~4 小时
### 对于安全/合规人员
1. SUMMARY.md (5 分钟)
2. ENCRYPTION.md (30 分钟)
3. DEPLOYMENT.md > 安全最佳实践 (15 分钟)
4. VERIFICATION.md 第9部分 (10 分钟)
**总计:** ~1 小时
### 对于项目经理
1. SUMMARY.md (5 分钟)
2. DEPLOYMENT.md > "部署状态示意图" (5 分钟)
3. DEPLOYMENT.md > "快速部署" (2 分钟)
**总计:** ~15 分钟
---
## 🎓 学习成果预期
### 完成后,您将能够:
✅ 在 Kubernetes 中部署 JWT 认证系统
✅ 配置 RBAC 权限控制
✅ 启用 ETCD 加密保护敏感数据
✅ 在 Go-zero 应用中集成 JWT
✅ 实现令牌刷新和撤销
✅ 诊断和排查常见问题
✅ 执行密钥轮换和灾难恢复
---
## 🆘 求助指南
### 第一步:找到相关文档
- 浏览本索引找到相关章节
- 或用 Ctrl+F 搜索关键词
### 第二步:查看文档中的相关部分
- DEPLOYMENT.md 的相关章节
- 或 VERIFICATION.md 的故障排查部分
### 第三步:运行诊断命令
- QUICK_REFERENCE.md 的 "故障排查" 部分
- 或 VERIFICATION.md 的 "故障排查" 部分
### 第四步:检查日志
```bash
kubectl logs -n juwan -l app=user-rpc -f
kubectl logs -n juwan -l app=envoy-gateway -f
```
### 第五步:查看详细文档
如果上述步骤未能解决,查看对应的详细文档:
- 配置问题 → DEPLOYMENT.md
- 权限问题 → VERIFICATION.md 第2/11部分
- 集成问题 → INTEGRATION.md
- 加密问题 → ENCRYPTION.md
---
## 📞 文档反馈
如果您发现:
- ❌ 文档不清楚
- ❌ 命令不工作
- ❌ 信息缺失或过时
- ❌ 错别字或格式问题
请在相应的 `.md` 文件中标记,或提交更新建议。
---
## 📌 关键概念快速链接
| 概念 | 详见 |
|-----|------|
| JWT 令牌生命周期 | SUMMARY.md "关键特性" |
| Redis 双键结构 | SUMMARY.md "关键特性" |
| RBAC 权限隔离 | SUMMARY.md "关键特性" |
| CSRF 防护 | SUMMARY.md "关键特性" |
| ETCD 加密 | ENCRYPTION.md |
| 错误处理 | INTEGRATION.md 第8部分 |
| 密钥轮换 | DEPLOYMENT.md "安全最佳实践" |
| 灾难恢复 | DEPLOYMENT.md "灾难恢复" |
---
## ✨ 文档特性
**模块化** - 每个文档独立,但相互链接
**分层** - 从快速概览到深度细节
**实践导向** - 包含实际命令和代码示例
**完整性** - 覆盖部署、验证、维护、故障排查
**易查找** - 目录、索引、速查表
---
**开始阅读:** 👉 [SUMMARY.md](./SUMMARY.md)
或根据您的角色选择:
| 角色 | 开始文档 | 预计时间 |
|-----|--------|--------|
| DevOps/运维 | [DEPLOYMENT.md](./DEPLOYMENT.md) | 1-2 小时 |
| 应用开发 | [INTEGRATION.md](../api/INTEGRATION.md) | 2-3 小时 |
| 安全审查 | [ENCRYPTION.md](./ENCRYPTION.md) | 30 分钟 |
| 项目经理 | [SUMMARY.md](./SUMMARY.md) | 15 分钟 |
| 新手 | [README.md](./README.md) → [SUMMARY.md](./SUMMARY.md) | 15-20 分钟 |
-350
View File
@@ -1,350 +0,0 @@
# JWT + ETCD 加密系统 - 快速参考卡片
## 一页速查表
### 部署命令
```bash
# 创建 Secret 和 RBAC
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
# 更新 Deployments
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
kubectl apply -f deploy/k8s/envoy/envoy.yaml
# 验证部署
kubectl get secret jwt-secret -n juwan
kubectl get sa user-rpc envoy-gateway -n juwan
kubectl get role jwt-secret-reader -n juwan
kubectl get pods -n juwan -l app=user-rpc
kubectl get pods -n juwan -l app=envoy-gateway
```
### 权限验证
```bash
# user-rpc 可以读 jwt-secret
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:user-rpc \
--resource-name=jwt-secret -n juwan
# 预期: yes
# 其他 SA 无法读 jwt-secret
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:default \
--resource-name=jwt-secret -n juwan
# 预期: no
```
### 日志查看
```bash
# user-rpc 日志
kubectl logs -n juwan -l app=user-rpc -f
# Envoy 日志
kubectl logs -n juwan -l app=envoy-gateway -f
# 特定 Pod 日志
kubectl logs -n juwan <pod-name> --all-containers=true -f
```
### 环境变量验证
```bash
# 检查 JWT_SECRET_KEY 已注入
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) \
-n juwan -- env | grep JWT_SECRET_KEY
```
### Redis 验证
```bash
# 连接到 Redis Cluster
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
redis-cli -h user-redis.juwan:6379 -c CLUSTER INFO
# 测试键操作
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
redis-cli -h user-redis.juwan:6379 -c GET jwt:user:test-user-id
```
### ETCD 加密配置
```bash
# 1. 在 Control Plane 节点生成密钥
head -c 32 /dev/urandom | base64
# 2. 编辑 kube-apiserver 清单
sudo nano /etc/kubernetes/manifests/kube-apiserver.yaml
# 添加参数:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# 3. 创建加密配置文件
cat <<EOF | sudo tee /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_KEY>
- identity: {}
EOF
# 4. 验证加密
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
sudo ETCDCTL_API=3 etcdctl --cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--endpoints=127.0.0.1:2379 \
get /registry/secrets/juwan/test-encryption | od -A x -t x1z
```
### 故障排查
```bash
# Pod 无法启动?查看事件
kubectl describe pod <pod-name> -n juwan
# 权限被拒绝?检查 RBAC
kubectl get rolebinding -n juwan -o wide
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
# 无法挂载 Secret?检查 Secret 存在性
kubectl get secret jwt-secret -n juwan -o yaml
# Redis 连接错误?测试连通性
kubectl exec -it <user-rpc-pod> -n juwan -- \
redis-cli -h user-redis.juwan:6379 PING
```
## JWT Manager API 速查
### JwtManager 方法
```go
// 生成新令牌
token, err := svcCtx.JwtManager.New(ctx, userID, email, name)
// 验证令牌
claims, err := svcCtx.JwtManager.Valid(ctx, token)
// 刷新令牌(如果过期但 Redis 仍有数据)
newToken, err := svcCtx.JwtManager.Renew(ctx, token)
// 提取声明(不验证签名)
claims, err := svcCtx.JwtManager.Extract(ctx, token)
// 检查令牌是否存在于 Redis
exists, err := svcCtx.JwtManager.Exists(ctx, token)
// 撤销令牌(登出)
err := svcCtx.JwtManager.Revoke(ctx, userID, token)
// 获取用户当前令牌
token, err := svcCtx.JwtManager.GetUserToken(ctx, userID)
// 将声明转换为载荷
payload := svcCtx.JwtManager.ClaimsToPayload(claims)
```
## 配置文件位置
| 配置 | 位置 | 关键参数 |
|-----|------|--------|
| JWT Secret | `deploy/k8s/secrets/jwt-secret.yaml` | `secret-key` |
| user-rpc 配置 | `app/users/rpc/etc/pb.yaml` | `JWT.SecretKey`, `REDIS_HOST` |
| Envoy 配置 | `deploy/k8s/envoy/envoy.yaml` | CSRF 验证 Lua 代码 |
| ETCD 加密 | `/etc/kubernetes/encryption-config.yaml`Control Plane | `secret` (32字节密钥) |
## 关键参数速查
```yaml
# JWT 令牌有效期
Token Exp: 7 days
# Redis 存储 TTL
Redis TTL: 30 days
# 可刷新时间窗口
Refresh Window: 30 days - 7 days = 23 days
# CSRF Token 位置
Cookie: csrf_token=...
Header: X-CSRF-Token: ...
# ETCD 加密算法
Algorithm: AES-CBC
Key Size: 256 bits (32 bytes)
Encoding: Base64
# Secret 挂载方式
Method: volumeMount (read-only)
Method: valueFrom.secretKeyRef
```
## 常见问题速查
| 问题 | 排查命令 | 解决方案 |
|-----|--------|--------|
| Pod 无法启动 | `kubectl describe pod` | 检查 Secret/RBAC |
| 权限被拒绝 | `kubectl auth can-i get secrets` | 验证 RBAC 绑定 |
| Redis 连接失败 | `redis-cli PING` | 检查 Redis Pods |
| JWT 验证失败 | 查看 Pod 日志 | 检查 Redis 中的令牌 |
| CSRF 验证失败 | 查看 Envoy 日志 | 检查 Cookie/Header 匹配 |
| ETCD 加密失败 | `kubectl get secret` | 检查 kube-apiserver 启动参数 |
## 部署检查清单 (5分钟版)
```bash
# 第1步: 部署 Secret (10秒)
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml && sleep 5
# 第2步: 验证 Secret (10秒)
kubectl get secret jwt-secret -n juwan && echo "✓ Secret 已创建"
# 第3步: 验证 RBAC (10秒)
kubectl get role jwt-secret-reader -n juwan && echo "✓ RBAC 已配置"
# 第4步: 更新 Deployments (20秒)
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
kubectl apply -f deploy/k8s/envoy/envoy.yaml
# 第5步: 等待 Pods 启动 (30秒)
kubectl rollout status deployment/user-rpc -n juwan
kubectl rollout status deployment/envoy-gateway -n juwan
# 第6步: 快速功能测试 (2分钟)
# 创建一个令牌并验证可读取
REDIS_POD=$(kubectl get pod -n juwan -l redis=user-redis -o name | head -1)
kubectl exec -it $REDIS_POD -n juwan -- redis-cli KEYS "jwt:*"
```
## 密钥轮换步骤
```bash
# 1. 生成新密钥
NEW_KEY=$(head -c 32 /dev/urandom | base64)
# 2. 更新 Secret
kubectl create secret generic jwt-secret \
--from-literal=secret-key=$NEW_KEY \
--dry-run=client -o yaml | kubectl apply -f -
# 3. 重启 Pods(自动挂载新 Secret
kubectl rollout restart deployment/user-rpc -n juwan
kubectl rollout restart deployment/envoy-gateway -n juwan
# 4. 等待 Pods 启动
kubectl rollout status deployment/user-rpc -n juwan
kubectl rollout status deployment/envoy-gateway -n juwan
# 5. 旧令牌现在需要刷新或重新登录
```
## 文档地图
```
deploy/k8s/secrets/
├── jwt-secret.yaml ← Secrets + RBAC 配置
├── README.md ← 开始阅读(快速指南)
├── SUMMARY.md ← 本文件(系统概览)
├── DEPLOYMENT.md ← 详细部署步骤(12步)
├── ENCRYPTION.md ← ETCD 加密详细指南
├── VERIFICATION.md ← 完整验证清单(12部分)
└── QUICK_REFERENCE.md ← 本快速参考卡片
app/users/api/
└── INTEGRATION.md ← JWT 代码集成指南
app/users/rpc/
├── internal/utils/jwt.go ← JwtManager 实现
├── internal/config/config.go ← JWT 配置
├── internal/svc/serviceContext.go ← 依赖注入
└── etc/pb.yaml ← 运行时配置
```
## 关键时间点
| 阶段 | 时间 | 操作 |
|-----|------|------|
| 令牌签发 | T0 | 生成 JWT,过期时间 = T0 + 7天 |
| | | 在 Redis 存储,TTL = 30天 |
| Token 过期 | T0 + 7天 | JWT 验证失败 |
| 令牌刷新 | T0 + 7天到T0 + 30天 | 如果 Redis 仍有数据,生成新令牌 |
| 完全失效 | T0 + 30天 | Redis 删除,无法再刷新 |
| 重新登录 | T0 + 30天+ | 用户需要重新登录 |
## 性能提示
```bash
# 高并发下优化 Redis 连接
# 在 pb.yaml 中调整:
CacheConf:
- Host: "user-redis.juwan:6379"
Type: "cluster"
MaxConnections: 100
ConnectionPoolSize: 50
# 监控 JWT 验证吞吐量
# 在 Prometheus 查询:
rate(jwt_validations_total[5m])
rate(jwt_refresh_total[5m])
```
## 安全提示
**必做**
- [ ] 定期轮换 JWT 秘钥(季度)
- [ ] 定期轮换 ETCD 加密密钥(年度)
- [ ] 备份加密密钥到安全位置
- [ ] 启用审计日志
- [ ] 监控异常的令牌验证失败
**禁止**
- [ ] 不要在日志中输出 JWT 秘钥
- [ ] 不要在代码库中存储密钥
- [ ] 不要发送明文密钥到 Slack/Email
- [ ] 不要在多个环境间共享密钥
## 版本信息
```
Kubernetes: 1.24+
Go-zero: v1.10.0+
Redis: 7.0+
ETCD: 3.5+
Envoy: v1.32.2+
```
## 支持和反馈
遇到问题?按优先级检查:
1. **运行验证脚本**
```bash
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
./deploy/k8s/secrets/verify-jwt-setup.sh
```
2. **查看日志**
```bash
kubectl logs -n juwan -l app=user-rpc -f
```
3. **阅读 VERIFICATION.md**
- 第1-5部分: 基础配置
- 第6-8部分: 网络和监控
- 第9部分: ETCD 加密
- 第11部分: 故障排查
4. **详细指南**
- DEPLOYMENT.md - 完整步骤
- INTEGRATION.md - 代码集成
- ENCRYPTION.md - 加密配置
-148
View File
@@ -1,148 +0,0 @@
# JWT Secret Management
This directory contains secure configuration for JWT secret key management.
## Files
- `jwt-secret.yaml`: Kubernetes Secret + ServiceAccount + RBAC rules
- `ENCRYPTION.md`: Guide for enabling ETCD static encryption at rest
## Quick Start
### 1. Create the Secret and RBAC
```bash
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
```
This will create:
- Secret `jwt-secret` in namespace `juwan` containing the JWT secret key
- ServiceAccount `user-rpc` in namespace `juwan`
- ServiceAccount `envoy-gateway` in namespace `juwan`
- Role `jwt-secret-reader` that allows reading only `jwt-secret`
- RoleBindings to grant both ServiceAccounts read permission on the secret
### 2. Update user-rpc Deployment
Update `deploy/k8s/service/user/user-rpc.yaml` to:
1. Set the serviceAccountName:
```yaml
spec:
template:
spec:
serviceAccountName: user-rpc
```
2. Add environment variable to load JWT secret:
```yaml
spec:
template:
spec:
containers:
- name: user-rpc
env:
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: jwt-secret
key: secret-key
```
### 3. Update envoy-gateway Deployment
Update `deploy/k8s/envoy/envoy.yaml` to:
1. Set the serviceAccountName:
```yaml
spec:
template:
spec:
serviceAccountName: envoy-gateway
```
2. Add environment variable or mount Secret:
```yaml
volumeMounts:
- name: jwt-secret
mountPath: /etc/jwt
readOnly: true
volumes:
- name: jwt-secret
secret:
secretName: jwt-secret
defaultMode: 0400
```
Then reference it in the Envoy config:
```yaml
data:
envoy.yaml: |
# Read JWT secret from /etc/jwt/secret-key
```
### 4. Enable ETCD Encryption
Follow the guide in `ENCRYPTION.md` to enable static encryption at rest for all secrets in ETCD.
## Security Considerations
### Least Privilege
- Only `user-rpc` and `envoy-gateway` can read the JWT secret
- No other services or users have access
- The Role allows reading **only** the `jwt-secret`, not other secrets
### Encryption at Rest
- With ETCD encryption enabled, the secret is encrypted when stored on disk
- Even if someone gains access to the ETCD database files, they cannot read the secret without the encryption key
### Secret Rotation
To rotate the JWT secret key:
1. Generate a new key
2. Update the Secret:
```bash
kubectl create secret generic jwt-secret --from-literal=secret-key=NEW_KEY --dry-run=client -o yaml | kubectl apply -f -
```
3. Pod mounts/env vars will be updated automatically within a few minutes
4. Old tokens will become invalid (you may need to log users out)
## Production Checklist
- [ ] ETCD encryption enabled (see ENCRYPTION.md)
- [ ] JWT secret key changed from default
- [ ] Both user-rpc and envoy-gateway Deployments use correct serviceAccountName
- [ ] Both Deployments load the secret via environment variable or volume mount
- [ ] Regular secret rotation policy implemented
- [ ] Secret backup stored in secure location (encrypted)
- [ ] RBAC audit logging enabled to track secret access
## Troubleshooting
### Cannot read jwt-secret
Check if the Pod is using the correct ServiceAccount:
```bash
kubectl get deployment user-rpc -o yaml | grep serviceAccountName
```
### Secret not being mounted
Verify the Secret exists:
```bash
kubectl get secret jwt-secret -n juwan
```
Check Pod logs for mounting errors:
```bash
kubectl logs -l app=user-rpc -n juwan
```
### Permission denied error
Verify RBAC binding:
```bash
kubectl get rolebinding -n juwan
kubectl get role jwt-secret-reader -n juwan
```
-366
View File
@@ -1,366 +0,0 @@
# JWT 认证系统 + ETCD 加密 - 完整部署总结
## 项目概览
这个项目为微服务提供了一个完整的 JWT 认证系统,包括:
1. **JWT 令牌管理** - 令牌生成、验证、刷新和撤销
2. **Redis Cluster 存储** - 令牌交换缓存(30天TTL)和用户会话管理
3. **RBAC 权限控制** - 限制只有 user-rpc 和 envoy-gateway 服务可以访问 JWT 秘钥
4. **ETCD 加密** - 在 Kubernetes 集群中对所有 Secrets 进行加密
5. **网关保护** - Envoy 网关处理 CSRF 防护和请求路由
## 创建的文件清单
### 部署配置文件
#### `/deploy/k8s/secrets/`
| 文件 | 说明 | 关键内容 |
|-----|------|--------|
| `jwt-secret.yaml` | Secret + RBAC 配置 | 包含JWT秘钥、ServiceAccounts、Role、RoleBindings |
| `README.md` | 快速参考指南 | Secret 创建和 Deployment 更新说明 |
| `DEPLOYMENT.md` | 详细部署步骤 | 12个部署步骤,包括ETCD加密配置 |
| `ENCRYPTION.md` | ETCD加密完整指南 | 密钥生成、配置修改、验证流程 |
| `VERIFICATION.md` | 验证清单 | 12个部分的完整验证脚本和检查项 |
### 应用代码更新
| 文件路径 | 修改内容 |
|---------|--------|
| `/app/users/rpc/internal/utils/jwt.go` | JwtManager 实现(已存在) |
| `/app/users/rpc/internal/config/config.go` | JwtConfig 结构体(已存在) |
| `/app/users/rpc/internal/svc/serviceContext.go` | Redis Cluster + JwtManager 依赖注入(已存在) |
| `/app/users/rpc/etc/pb.yaml` | JWT 和 Redis Cluster 配置(已存在) |
| `/deploy/k8s/service/user/user-rpc.yaml` | ✅ **已更新** - 添加 serviceAccountName + JWT_SECRET_KEY 环境变量 |
| `/deploy/k8s/envoy/envoy.yaml` | ✅ **已更新** - 添加 serviceAccountName: envoy-gateway |
| `/app/users/api/INTEGRATION.md` | 🆕 **新建** - JWT 集成指南(interceptors, handlers, middleware |
## 部署状态示意图
```
┌─────────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ juwan Namespace │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Secret: jwt-secret │ │ │
│ │ │ ├─ secret-key: <encrypted in ETCD> │ │ │
│ │ │ └─ Protected by RBAC Role + RoleBindings │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ │ △ │ │
│ │ │ (mounted via serviceAccountName) │ │
│ │ │ │ │
│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │
│ │ │ user-rpc │ │ envoy-gateway │ │ │
│ │ │ Deployment │ │ Deployment │ │ │
│ │ ├─────────────────┤ ├──────────────────────┤ │ │
│ │ │ SA: user-rpc │ │ SA: envoy-gateway │ │ │
│ │ │ Replicas: 3 │ │ Replicas: 1 │ │ │
│ │ │ Port: 9001(RPC) │ │ Port: 8080(HTTP) │ │ │
│ │ │ 4001(Met) │ │ │ │ │
│ │ └─────────────────┘ └──────────────────────┘ │ │
│ │ │ JWT Manager │ CSRF Filter │ │
│ │ │ (HS256 signing) │ (X-CSRF-Token) │ │
│ │ │ │ │ │
│ │ ┌──────▼──────────────────────────▼─────┐ │ │
│ │ │ user-redis (RedisCluster) │ │ │
│ │ │ 3-node cluster │ │ │
│ │ │ - Token exchange cache (30d TTL) │ │ │
│ │ │ - User session management │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Control Plane (kube-apiserver) │ │
│ │ • ETCD 加密: AES-CBC 32-byte key │ │
│ │ • Secrets 自动加密存储 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## 核心配置参数
### JWT 配置
```yaml
JWT:
SecretKey: "your-secret-jwt-key-change-this-in-production"
Issuer: "your-app-name"
# Token 有效期: 7 天
# Redis TTL: 30 天(支持令牌刷新)
```
### Redis Cluster
```yaml
RedisCluster:
ClusterSize: 3 🔴 主服务器 + 2 🔵 从服务器
Address: "user-redis.juwan:6379"
HighAvailability: 自动故障转移
```
### RBAC 权限
```yaml
Role: jwt-secret-reader
Resources: [secrets]
ResourceNames: [jwt-secret]
Verbs: [get] # 只读,无列表/创建/删除
Subjects:
- user-rpc (ServiceAccount)
- envoy-gateway (ServiceAccount)
```
## 部署流程
### 快速部署(5分钟)
```bash
# 第1步:创建 Secret 和 RBAC
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
# 第2步:更新 Deployments
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
kubectl apply -f deploy/k8s/envoy/envoy.yaml
# 第3步:验证
./verify-jwt-setup.sh
```
### ETCD 加密部署(需要集群管理员权限)
```bash
# 在 Control Plane 节点执行
1. 生成 32 字节密钥
2. 创建 /etc/kubernetes/encryption-config.yaml
3. 修改 /etc/kubernetes/manifests/kube-apiserver.yaml
4. 重启 kube-apiserver
5. 验证加密已启用
```
详见:`deploy/k8s/secrets/ENCRYPTION.md`
## 关键特性
### 1. JWT 令牌生命周期
```
登录 → 生成 JWT
├─ 有效期: 7 天(exp claim
└─ 存储到 Redis: 30 天(TTL
Token 过期(7天+
├─ JWT 签名验证失败
└─ 检查 Redis 是否仍有数据
├─ 有 → 生成新 Token(刷新)✅
└─ 无 → 令牌已过期,需要重新登录 ❌
```
### 2. Redis 双键结构
```
jwt:user:{userId} → {token}
用途: 快速查询用户当前令牌
TTL: 30 天
jwt:token:{token} → {payload}
用途: 令牌验证和刷新
TTL: 30 天
```
### 3. CSRF 防护(Envoy 网关)
```
安全方法 (GET/HEAD/OPTIONS)
→ 自动生成 csrf_token
→ 返回 Set-Cookie: csrf_token=...
不安全方法 (POST/PUT/DELETE/PATCH)
→ 检查 Cookie csrf_token
→ 检查 X-CSRF-Token 头
→ 两者必须相等,否则 403
```
### 4. 权限隔离
```
Only user-rpc + envoy-gateway 可以:
✅ 读 jwt-secret
Other services 无法:
❌ 列出 secrets
❌ 获取 jwt-secretRBAC 拒绝)
❌ 删除 secrets
```
### 5. ETCD 加密
```
未加密:
etcdctl get /registry/seca/...
→ secret-key: "plaintext-value"
已加密 (AES-CBC):
etcdctl get /registry/secrets/...
→ 二进制数据,无法读取
```
## 集成点
### 1. RPC Handler(需要实现)
```go
// 在 gRPC server 中注册拦截器
s := grpc.NewServer(
grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)),
)
// 拦截器会:
// 1. 提取 Authorization 头中的 Token
// 2. 调用 JwtManager.Valid()
// 3. 如果过期,尝试 JwtManager.Renew()
// 4. 将声明注入 context
```
参考:`app/users/api/INTEGRATION.md` 第1-2章
### 2. REST Endpoint(需要实现)
```go
// 创建 JWT Middleware
protected := middleware.JwtMiddleware(svcCtx)
// 应用到受保护的路由
router.HandleFunc("GET /api/v1/users/me",
protected(user.GetUserInfoHandler(svcCtx)))
// Middleware 会验证 Authorization: Bearer {token}
```
参考:`app/users/api/INTEGRATION.md` 第3-4章
### 3. 登录/登出流程(需要实现)
```
登录:
1. 验证用户凭证(DB 查询)
2. JwtManager.New() → 生成令牌
3. 返回令牌给客户端
登出:
1. 从上下文提取 userId
2. JwtManager.Revoke() → 删除 Redis 中的令牌
3. 用户需要重新登录获取新令牌
```
参考:`app/users/api/INTEGRATION.md` 第5-6章
## 文档导航
| 场景 | 推荐阅读 |
|-----|--------|
| 第一次部署 | `README.md``DEPLOYMENT.md` |
| 部署遇到问题 | `VERIFICATION.md` + `DEPLOYMENT.md` 故障排查部分 |
| 代码集成 | `app/users/api/INTEGRATION.md` |
| ETCD 加密配置 | `ENCRYPTION.md` |
| ETCD 加密验证 | `VERIFICATION.md` 第9部分 |
| 安全最佳实践 | `DEPLOYMENT.md` 安全最佳实践部分 |
| 灾难恢复 | `DEPLOYMENT.md` 灾难恢复部分 |
## 生产就绪检查清单
- [ ] 所有 Pods 都在 Running 状态
- [ ] JWT Secret 已创建并正确挂载
- [ ] RBAC 权限验证通过
- [ ] Redis Cluster 健康(3/3 节点)
- [ ] ETCD 加密已启用(如需要)
- [ ] 监控和日志聚合正常工作
- [ ] 密钥轮换计划已制定
- [ ] 备份和恢复流程已文档化
- [ ] 安全审计日志已启用
- [ ] 端到端测试已通过
## 下一步行动
### 短期(本周)
1. **部署 Secret 和 RBAC**
```bash
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
```
2. **更新 Deployments**
```bash
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
kubectl apply -f deploy/k8s/envoy/envoy.yaml
```
3. **验证部署**
```bash
./verify-jwt-setup.sh
```
### 中期(本月)
1. **实现 JWT 集成**
- 创建 gRPC 拦截器
- 实现登录/登出端点
- 添加 JWT 中间件到 REST API
2. **端到端测试**
- 测试令牌生成和验证
- 测试令牌刷新
- 测试 CSRF 防护
### 长期(本季度)
1. **启用 ETCD 加密**
- 按照 `ENCRYPTION.md` 配置
- 验证所有 Secrets 都已加密
2. **生产部署**
- 启用审计日志
- 配置监控和告警
- 制定密钥轮换政策
## 支持
如遇到问题:
1. **检查日志**
```bash
kubectl logs -n juwan -l app=user-rpc -f
kubectl logs -n juwan -l app=envoy-gateway -f
```
2. **运行验证脚本**
```bash
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
./deploy/k8s/secrets/verify-jwt-setup.sh
```
3. **查看详细文档**
- 部署问题 → `DEPLOYMENT.md`
- 代码集成 → `INTEGRATION.md`
- ETCD 加密 → `ENCRYPTION.md`
- 诊断 → `VERIFICATION.md`
## 总结
这个系统为微服务提供了:
**安全的身份验证** - JWT 令牌 + HS256 签名
**灵活的令牌管理** - 7天有效期,30天可刷新
**高可用性** - Redis Cluster 自动故障转移
**权限隔离** - RBAC 限制密钥访问
**数据加密** - ETCD 加密保护敏感信息
**请求保护** - Envoy CSRF 双令牌验证
现在可以部署并集成到应用中了!
-507
View File
@@ -1,507 +0,0 @@
# 完整部署验证清单
完成所有部署后使用此清单验证系统是否正确配置和运行。
## 第一部分:基础设施验证
### Secret 和 RBAC 创建
```bash
# 检查 Secret 已创建
kubectl get secret -n juwan | grep jwt-secret
# 预期输出: jwt-secret Created
# 查看 Secret 详情(不显示敏感数据)
kubectl describe secret jwt-secret -n juwan
# 应该看到:
# Name: jwt-secret
# Namespace: juwan
# Type: Opaque
# Data
# ====
# secret-key: <encrypted>
# 验证 Secret 内容已正确加载
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d | wc -c
# 预期输出: 应该是 32 个字符(32 字节密钥的 Base64 解码)
```
### ServiceAccount 验证
```bash
# 检查 user-rpc ServiceAccount
kubectl get sa user-rpc -n juwan
kubectl describe sa user-rpc -n juwan
# 应该显示正确的 Secrets 挂载
# 检查 envoy-gateway ServiceAccount
kubectl get sa envoy-gateway -n juwan
kubectl describe sa envoy-gateway -n juwan
```
### RBAC 权限验证
```bash
# 检查 Role 定义
kubectl get role -n juwan -l app=jwt-secret-reader
kubectl describe role jwt-secret-reader -n juwan
# 应该显示:
# PolicyRule:
# Resources Non-Resource URLs Resource Names Verbs
# --------- ----------------- -------------- -----
# secrets [] [jwt-secret] [get]
# 检查 RoleBindings
kubectl get rolebinding -n juwan | grep jwt-secret-reader
# 应该显示两个绑定: jwt-secret-reader-user-rpc 和 jwt-secret-reader-envoy-gateway
# 验证每个 RoleBinding
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
kubectl describe rolebinding jwt-secret-reader-envoy-gateway -n juwan
```
## 第二部分:权限测试
### 权限允许测试
```bash
# 测试 user-rpc 可以读 jwt-secret
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:user-rpc \
--resource-name=jwt-secret \
-n juwan
# 预期输出: yes
# 测试 envoy-gateway 可以读 jwt-secret
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:envoy-gateway \
--resource-name=jwt-secret \
-n juwan
# 预期输出: yes
# 测试 user-rpc 无法读其他 Secrets
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:user-rpc \
-n juwan
# 预期输出: no
# 测试其他 ServiceAccount 无法读 jwt-secret
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:default \
--resource-name=jwt-secret \
-n juwan
# 预期输出: no
```
## 第三部分:Deployment 配置验证
### user-rpc Deployment 验证
```bash
# 检查 ServiceAccountName 是否正确设置
kubectl get deployment user-rpc -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
# 预期输出: user-rpc
# 检查是否包含所有必需的环境变量
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 20 "env:"
# 应该包括:
# - name: JWT_SECRET_KEY
# valueFrom:
# secretKeyRef:
# name: jwt-secret
# key: secret-key
# 检查 Pod 是否正在运行
kubectl get pods -n juwan -l app=user-rpc
# 应该显示至少 3 个 Running 的 Pod
# 验证 Pod 已加载 Secret(在 Pod 中执行)
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep -i jwt
# 应该输出环境变量,例如:
# JWT_SECRET_KEY=your-secret-jwt-key-change-this-in-production
```
### Envoy Gateway Deployment 验证
```bash
# 检查 ServiceAccountName 是否正确设置
kubectl get deployment envoy-gateway -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
# 预期输出: envoy-gateway
# 检查 Pod 是否正在运行
kubectl get pods -n juwan -l app=envoy-gateway
# 应该显示 Running 的 Pod
# 检查 Envoy 日志
kubectl logs -n juwan -l app=envoy-gateway
# 应该看到启动日志,没有权限相关错误
```
## 第四部分:Redis 连接验证
### Redis Cluster 验证
```bash
# 检查 RedisCluster CRD 状态
kubectl get rediscluster -n juwan
# 应该显示 user-redisStatus 应该是 Healthy
# 详细查看 RedisCluster 状态
kubectl describe rediscluster user-redis -n juwan
# 应该显示:
# Status:
# Cluster Status: Healthy
# Nodes Ready: 3/3
# Master: 1
# Replicas: 2
# 检查 Redis Pods
kubectl get pods -n juwan | grep redis
# 应该显示 3 个 Redis Pod,都在 Running 状态
# 测试 Redis 连接
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
redis-cli -h user-redis.juwan -c CLUSTER INFO
# 应该看到集群信息,cluster_state:ok 表示集群健康
```
## 第五部分:应用启动日志检查
### user-rpc 启动日志
```bash
# 查看 user-rpc Pods 的启动日志
kubectl logs -n juwan -l app=user-rpc --all-containers=true
# 应该包含类似以下消息:
# - "Starting gRPC server on 0.0.0.0:9001"
# - "Redis Cluster connected successfully" 或 JWT Manager 初始化成功
# - "Listening on metrics port 4001"
# 如果有错误,查看详细日志
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
```
### Envoy 启动日志
```bash
# 查看 Envoy 启动日志
kubectl logs -n juwan -l app=envoy-gateway
# 应该包含:
# - "[info] Configuration: /etc/envoy/envoy.yaml"
# - "[info] listener listening on 0.0.0.0:8080"
# - 没有权限相关错误
```
## 第六部分:网络和服务发现验证
### Service 验证
```bash
# 检查 user-rpc-svc
kubectl get svc user-rpc-svc -n juwan
# 应该显示 ClusterIP 和两个端口 (9001/rpc 和 4001/metrics)
# 检查 Envoy Gateway Service
kubectl get svc envoy-gateway -n juwan
# 应该显示 ClusterIP 和端口 80
# 检查 Redis Service
kubectl get svc -n juwan | grep redis
# 应该显示 user-redisClusterIP)服务
```
### DNS 解析验证
```bash
# 测试服务名称解析
kubectl run -it --rm debug --image=busybox --restart=Never -- \
nslookup user-rpc-svc.juwan.svc.cluster.local
# 应该返回 ClusterIP 地址
kubectl run -it --rm debug --image=busybox --restart=Never -- \
nslookup user-redis.juwan.svc.cluster.local
# 应该返回 ClusterIP 地址
```
## 第七部分:监控和指标验证
### Prometheus 指标收集
```bash
# 检查 Prometheus 是否在收集指标
kubectl port-forward -n monitoring svc/prometheus 9090:9090 &
# 打开浏览器访问 http://localhost:9090
# 查看 Status > Targets
# 应该看到 user-rpc-svc:4001 目标显示为 UP
# 查询一个指标
curl 'http://localhost:9090/api/v1/query?query=up{job="kubernetes-pods"}'
# 应该返回 user-rpc 的指标数据
# 关闭端口转发
kill %1
```
### 测试源代码级指标端点
```bash
# 从 user-rpc Pod 直接访问指标端点
kubectl port-forward -n juwan svc/user-rpc-svc 4001:4001 &
# 测试指标端点
curl http://localhost:4001/metrics
# 应该看到 Prometheus 格式的指标,例如:
# # HELP go_goroutines Number of goroutines that currently exist.
# # TYPE go_goroutines gauge
# go_goroutines 25
# 关闭端口转发
kill %1
```
## 第八部分:日志聚合验证(Loki)
```bash
# 检查 Loki 是否正确接收日志
kubectl port-forward -n monitoring svc/loki 3100:3100 &
# 查询日志
curl 'http://localhost:3100/loki/api/v1/query_range?query={job="kubernetes-pods"}&start=0&end=9999999999'
# 应该返回最近的日志条目
# 检查特定应用的日志
curl 'http://localhost:3100/loki/api/v1/query_range?query={app="user-rpc"}&start=0&end=9999999999'
kill %1
```
## 第九部分:ETCD 加密验证
如果已启用 ETCD 加密,执行以下验证:
```bash
# 从 control plane 节点
ssh <control-plane-node>
# 检查 ETCD 配置
sudo cat /etc/kubernetes/encryption-config.yaml | head -20
# 验证 kube-apiserver 正在使用加密配置
sudo ps aux | grep kube-apiserver | grep encryption-provider
# 创建新 Secret 进行测试
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
# 检查 ETCD 中的数据是否加密
# 注意:如果加密正确,数据应该不可读
sudo ETCDCTL_API=3 etcdctl \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--endpoints=127.0.0.1:2379 \
get /registry/secrets/juwan/test-encryption
# 输出应该是二进制数据,不可读(表示已加密)
# 或者使用十六进制 dump
sudo ETCDCTL_API=3 etcdctl \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--endpoints=127.0.0.1:2379 \
get /registry/secrets/juwan/test-encryption | od -A x -t x1z -v
```
## 第十部分:功能测试
### JWT 令牌生成和验证测试
如果已实现 JWT handlers,测试完整流程:
```bash
# 1. 前向 user-api 服务
kubectl port-forward -n juwan svc/user-api-svc 8888:8888 &
# 2. 调用登录端点获取令牌
TOKEN=$(curl -X POST http://localhost:8888/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}' \
| jq -r '.token')
echo "Token: $TOKEN"
# 3. 使用令牌访问受保护的端点
curl -H "Authorization: Bearer $TOKEN" http://localhost:8888/api/v1/users/me
# 4. 测试令牌刷新
curl -X POST http://localhost:8888/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"token\":\"$TOKEN\"}"
# 5. 测试无效令牌
curl -H "Authorization: Bearer invalid-token" http://localhost:8888/api/v1/users/me
# 应该返回 401 Unauthorized
kill %1
```
### CSRF 保护测试
```bash
# 1. 前向 Envoy Gateway
kubectl port-forward -n juwan svc/envoy-gateway 8080:80 &
# 2. 获取 CSRF 令牌(安全方法)
curl -i http://localhost:8080/
# 查看响应头中的 Set-Cookie,应该包含 csrf_token
# 3. 提取 CSRF 令牌
CSRF_TOKEN=$(curl -i http://localhost:8080/ 2>/dev/null | grep -i csrf_token | sed 's/.*csrf_token=//;s/;.*//')
# 4. 使用 CSRF 令牌进行 POST 请求
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "Cookie: csrf_token=$CSRF_TOKEN" \
-H "X-CSRF-Token: $CSRF_TOKEN" \
-d '{"email":"user@example.com","password":"password"}'
# 5. 测试无效 CSRF 令牌(应该返回 403)
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Cookie: csrf_token=valid_token" \
-H "X-CSRF-Token: invalid_token" \
-d '{"email":"user@example.com","password":"password"}'
# 应该返回 403 Forbidden
kill %1
```
## 第十一部分:故障排查
如果任何验证失败,运行以下诊断:
### Pod 无法启动
```bash
# 显示 Pod 事件
kubectl describe pod <pod-name> -n juwan
# 查看完整日志(包括初始化容器)
kubectl logs <pod-name> -n juwan --all-containers=true --previous
# 检查 Pod 资源限制是否导致 OOMKilled
kubectl get event -n juwan --sort-by='.lastTimestamp'
```
### 权限被拒绝错误
```bash
# 验证 ServiceAccount 是否正确
kubectl get pod <pod-name> -n juwan -o jsonpath='{.spec.serviceAccountName}'
# 检查 RBAC 绑定
kubectl get rolebinding -n juwan -o wide
# 手动测试权限
kubectl auth can-i get secrets \
--as=system:serviceaccount:juwan:user-rpc \
-n juwan
```
### Redis 连接错误
```bash
# 检查 Redis Pods 状态
kubectl get pods -n juwan -l redis=user-redis
# 查看 Redis 日志
kubectl logs -n juwan -l redis=user-redis
# 测试 Redis 连接(从 user-rpc Pod
kubectl exec -it <user-rpc-pod> -n juwan -- \
redis-cli -h user-redis.juwan:6379 PING
# 应该返回 PONG
```
### ETCD 加密问题
```bash
# 验证加密配置
kubectl get secret jwt-secret -n juwan -o json | jq '.data'
# 如果 ETCD 加密启用,直接读取 ETCD 的数据应该是二进制的
# 如果看到明文,说明加密未启用或配置不正确
```
## 第十二部分:清理测试资源
```bash
# 删除测试 Secrets
kubectl delete secret test-encryption test-secret -n juwan --ignore-not-found
# 清理前转发的端口
lsof -i :9090 :3100 :8888 :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9
```
## 快速检查脚本
创建 `verify-jwt-setup.sh` 进行自动化验证:
```bash
#!/bin/bash
namespace="juwan"
echo "=== JWT Setup Verification ==="
# 检查 Secret
echo -n "✓ JWT Secret存在: "
kubectl get secret jwt-secret -n $namespace &>/dev/null && echo "✓" || echo "✗"
# 检查 ServiceAccounts
echo -n "✓ user-rpc ServiceAccount: "
kubectl get sa user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
echo -n "✓ envoy-gateway ServiceAccount: "
kubectl get sa envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
# 检查 RBAC
echo -n "✓ JWT RBAC Role: "
kubectl get role jwt-secret-reader -n $namespace &>/dev/null && echo "✓" || echo "✗"
# 检查 Deployments
echo -n "✓ user-rpc Deployment: "
kubectl get deployment user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
echo -n "✓ envoy-gateway Deployment: "
kubectl get deployment envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
# 检查 Pods
echo -n "✓ user-rpc Pods运行中: "
[ $(kubectl get pods -n $namespace -l app=user-rpc --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ] && echo "✓" || echo "✗"
echo -n "✓ envoy-gateway 运行中: "
kubectl get pods -n $namespace -l app=envoy-gateway --field-selector=status.phase=Running &>/dev/null && echo "✓" || echo "✗"
echo "=== Verification Complete ==="
```
运行脚本:
```bash
chmod +x verify-jwt-setup.sh
./verify-jwt-setup.sh
```
## 总结
所有检查项都通过后,JWT + ETCD 加密系统已准备就绪。下一步可以:
1. 集成 JWT 验证到 RPC handlers
2. 实现令牌刷新端点
3. 部署应用代码时启用 JWT 认证
4. 监控令牌生成和验证指标
5. 定期轮换加密密钥和 JWT 秘钥
-1
View File
@@ -5,7 +5,6 @@ metadata:
namespace: juwan namespace: juwan
type: Opaque type: Opaque
data: data:
# base64 encoded: your-secret-jwt-key-change-this-in-production
secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ== secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ==
--- ---
apiVersion: v1 apiVersion: v1
@@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: authz-adapter
namespace: juwan
labels:
app: authz-adapter
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: authz-adapter
template:
metadata:
labels:
app: authz-adapter
spec:
serviceAccountName: find-endpoints
containers:
- name: authz-adapter
image: 103.236.53.208:4418/library/authz-adapter@sha256:84dd29596f94dd38d3a7a7924f4d5ed71b661b6d2a78d65c1741b11c2d8eea98
ports:
- containerPort: 9002
name: grpc
env:
- name: LISTEN_ON
value: "0.0.0.0:9002"
- name: USER_RPC_TARGET
value: "user-rpc-svc.juwan.svc.cluster.local:9001"
readinessProbe:
tcpSocket:
port: 9002
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 9002
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: timezone
mountPath: /etc/localtime
volumes:
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
---
apiVersion: v1
kind: Service
metadata:
name: authz-adapter-svc
namespace: juwan
spec:
selector:
app: authz-adapter
ports:
- name: grpc
port: 9002
targetPort: 9002
type: ClusterIP
+1 -1
View File
@@ -19,7 +19,7 @@ spec:
serviceAccountName: find-endpoints serviceAccountName: find-endpoints
containers: containers:
- name: user-api - name: user-api
image: 103.236.53.208:4418/library/user-api@sha256:a152f5fd13fc865ae3d9aeaa54eacad6bcaa0cb4f0ccb770fbb746be95360991 image: 103.236.53.208:4418/library/user-api@sha256:d3187beb9c777a8dcbdc6a835a7887cb29fbea9571b08fe538a1eece403226e2
ports: ports:
- containerPort: 8888 - containerPort: 8888
readinessProbe: readinessProbe:
+103 -103
View File
@@ -29,7 +29,7 @@ spec:
] ]
containers: containers:
- name: user-rpc - name: user-rpc
image: 103.236.53.208:4418/library/user-rpc@sha256:3d1d3cc02188a9b1a29a308a4867638b25b6e480e5a6bdaeb938f262f53969b7 image: 103.236.53.208:4418/library/user-rpc@sha256:28d785c4152d28b5cb368316e0fb3d48d728303e4439cdce13ebdbc5af8d19ce
ports: ports:
- containerPort: 9001 - containerPort: 9001
- containerPort: 4001 - containerPort: 4001
@@ -160,105 +160,105 @@ spec:
# type: Utilization # type: Utilization
# averageUtilization: 80 # averageUtilization: 80
#--- #---
## Redis 主从复制 # Redis 主从复制
#apiVersion: redis.redis.opstreelabs.in/v1beta2 apiVersion: redis.redis.opstreelabs.in/v1beta2
#kind: RedisReplication kind: RedisReplication
#metadata: metadata:
# name: user-redis name: user-redis
# namespace: juwan namespace: juwan
#spec: spec:
# clusterSize: 3 clusterSize: 3
# kubernetesConfig: kubernetesConfig:
# image: quay.io/opstree/redis:v7.0.12 image: quay.io/opstree/redis:v7.0.12
# imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
# resources: resources:
# requests: requests:
# cpu: 100m cpu: 100m
# memory: 128Mi memory: 128Mi
# limits: limits:
# cpu: 500m cpu: 500m
# memory: 512Mi memory: 512Mi
# redisSecret: redisSecret:
# name: user-redis name: user-redis
# key: password key: password
#
# redisExporter: redisExporter:
# enabled: true enabled: true
# image: quay.io/opstree/redis-exporter:latest image: quay.io/opstree/redis-exporter:latest
# imagePullPolicy: Always imagePullPolicy: Always
# podSecurityContext: podSecurityContext:
# runAsUser: 1000 runAsUser: 1000
# fsGroup: 1000 fsGroup: 1000
# storage: storage:
# volumeClaimTemplate: volumeClaimTemplate:
# spec: spec:
# accessModes: ["ReadWriteOnce"] accessModes: ["ReadWriteOnce"]
# resources: resources:
# requests: requests:
# storage: 1Gi storage: 1Gi
#
#--- ---
## Sentinel 监控 # Sentinel 监控
#apiVersion: redis.redis.opstreelabs.in/v1beta2 apiVersion: redis.redis.opstreelabs.in/v1beta2
#kind: RedisSentinel kind: RedisSentinel
#metadata: metadata:
# name: user-redis-sentinel name: user-redis-sentinel
# namespace: juwan namespace: juwan
#spec: spec:
# clusterSize: 3 clusterSize: 3
# kubernetesConfig: kubernetesConfig:
# image: quay.io/opstree/redis-sentinel:v7.0.12 image: quay.io/opstree/redis-sentinel:v7.0.12
# imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
# resources: resources:
# requests: requests:
# cpu: 100m cpu: 100m
# memory: 128Mi memory: 128Mi
# limits: limits:
# cpu: 500m cpu: 500m
# memory: 512Mi memory: 512Mi
# podSecurityContext: podSecurityContext:
# runAsUser: 1000 runAsUser: 1000
# fsGroup: 1000 fsGroup: 1000
# redisSentinelConfig: redisSentinelConfig:
# redisReplicationName: user-redis redisReplicationName: user-redis
# masterGroupName: mymaster masterGroupName: mymaster
# redisPort: "6379" redisPort: "6379"
# quorum: "2" quorum: "2"
# downAfterMilliseconds: "5000" downAfterMilliseconds: "5000"
# failoverTimeout: "10000" failoverTimeout: "10000"
# parallelSyncs: "1" parallelSyncs: "1"
#
#--- ---
## PostgreSQL 集群 # PostgreSQL 集群
#apiVersion: postgresql.cnpg.io/v1 apiVersion: postgresql.cnpg.io/v1
#kind: Cluster kind: Cluster
#metadata: metadata:
# namespace: juwan namespace: juwan
# name: user-db name: user-db
#spec: spec:
# instances: 3 instances: 3
# primaryUpdateStrategy: unsupervised primaryUpdateStrategy: unsupervised
# bootstrap: bootstrap:
# initdb: initdb:
# database: app database: app
# owner: app owner: app
# # 只在 PVC 为空时初始化 # 只在 PVC 为空时初始化
# postInitSQL: postInitSQL:
# - CREATE EXTENSION IF NOT EXISTS pg_stat_statements; - CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
# backup: backup:
# barmanObjectStore: barmanObjectStore:
# destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/ destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/
# endpointURL: https://cn-nb1.rains3.com endpointURL: https://cn-nb1.rains3.com
# s3Credentials: s3Credentials:
# accessKeyId: accessKeyId:
# name: rc-creds name: rc-creds
# key: SOucqRaJr4OyfcIu key: ACCESS_KEY_ID
# secretAccessKey: secretAccessKey:
# name: rc-creds name: rc-creds
# key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz key: SECRET_ACCESS_KEY
# wal: wal:
# compression: gzip compression: gzip
# storage: storage:
# size: 1Gi size: 1Gi
# monitoring: monitoring:
# enablePodMonitor: true enablePodMonitor: true
+69
View File
@@ -0,0 +1,69 @@
param(
[string]$SecretBase64,
[string]$SecretYamlPath = "deploy/k8s/secrets/jwt-secret.yaml",
[string]$Kid = "juwan-hs256-1",
[string]$Issuer = "juwan-user-rpc"
)
function Convert-ToBase64Url {
param([byte[]]$Bytes)
$base64 = [Convert]::ToBase64String($Bytes)
return $base64.TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
function Get-SecretBase64FromYaml {
param([string]$Path)
if (-not (Test-Path -Path $Path)) {
throw "Secret yaml not found: $Path"
}
$content = Get-Content -Path $Path -Raw -Encoding UTF8
$match = [regex]::Match($content, '(?m)^\s*secret-key\s*:\s*([A-Za-z0-9+/=]+)\s*$')
if (-not $match.Success) {
throw "Cannot find data.secret-key in: $Path"
}
return $match.Groups[1].Value
}
if ([string]::IsNullOrWhiteSpace($SecretBase64)) {
$SecretBase64 = Get-SecretBase64FromYaml -Path $SecretYamlPath
}
try {
$rawSecret = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($SecretBase64))
}
catch {
throw "Invalid base64 secret value. Error: $($_.Exception.Message)"
}
$kBytes = [Text.Encoding]::UTF8.GetBytes($rawSecret)
$kBase64Url = Convert-ToBase64Url -Bytes $kBytes
$jwkObject = @{
keys = @(
@{
kty = "oct"
k = $kBase64Url
alg = "HS256"
use = "sig"
kid = $Kid
}
)
}
$jwkJson = $jwkObject | ConvertTo-Json -Compress
Write-Output "=== INPUT ==="
Write-Output "secret(base64): $SecretBase64"
Write-Output "secret(raw): $rawSecret"
Write-Output ""
Write-Output "=== JWK inline_string ==="
Write-Output $jwkJson
Write-Output ""
Write-Output "=== Envoy jwt_authn snippet ==="
Write-Output ('issuer: "{0}"' -f $Issuer)
Write-Output "local_jwks:"
Write-Output (' inline_string: ''{0}''' -f $jwkJson)
+228
View File
@@ -0,0 +1,228 @@
# Envoy ext_authz 适配 user-rpc.ValidateToken(最小实现)
## 目标
- Envoy 不直接调用业务 proto 方法。
- 新增一个内部服务 `authz-adapter`,实现 Envoy 标准 gRPC 鉴权接口。
- `authz-adapter` 再调用现有 `user-rpc.ValidateToken` 完成鉴权。
---
## 1) 最小接口定义(Envoy 标准)
`authz-adapter` 需要实现的是 Envoy 官方服务:
- gRPC Service: `envoy.service.auth.v3.Authorization`
- RPC Method: `Check(CheckRequest) returns (CheckResponse)`
Go 里通常使用包:
- `github.com/envoyproxy/go-control-plane/envoy/service/auth/v3`
- `github.com/envoyproxy/go-control-plane/envoy/type/v3`
---
## 2) 最小 Go 适配器骨架
```go
package main
import (
"context"
"net"
"strconv"
"strings"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/grpc"
userpb "juwan-backend/app/users/rpc/pb"
)
type server struct {
authv3.UnimplementedAuthorizationServer
userRpc userpb.UsercenterClient
}
func (s *server) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) {
attrs := req.GetAttributes()
httpReq := attrs.GetRequest().GetHttp()
if httpReq == nil {
return deny(403, "missing http attributes"), nil
}
path := httpReq.GetPath()
method := strings.ToUpper(httpReq.GetMethod())
// 放行公共接口(探针、登录/注册、发送验证码)
if path == "/healthz" ||
path == "/api/users/login" ||
path == "/api/users/register" ||
path == "/api/email/verification-code/send" {
return allow(nil), nil
}
token := extractCookie(httpReq.GetHeaders(), "JToken")
if token == "" {
return deny(401, "missing token cookie"), nil
}
userID, err := parseUserIDFromPath(path)
if err != nil {
return deny(401, "invalid user id in path"), nil
}
// 调用你现有业务 RPC
vt, err := s.userRpc.ValidateToken(ctx, &userpb.ValidateTokenReq{
Token: token,
UserId: userID,
})
if err != nil || vt == nil || !vt.Valid {
return deny(401, "invalid token"), nil
}
// 透传给后端 API
headers := []*core.HeaderValueOption{
{
Header: &core.HeaderValue{Key: "x-auth-user-id", Value: strconv.FormatInt(vt.UserId, 10)},
},
{
Header: &core.HeaderValue{Key: "x-auth-role-type", Value: strconv.FormatInt(vt.RoleType, 10)},
},
{
Header: &core.HeaderValue{Key: "x-auth-method", Value: method},
},
}
return allow(headers), nil
}
func allow(headers []*core.HeaderValueOption) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &typev3.Status{Code: int32(typev3.Code_OK)},
HttpResponse: &authv3.CheckResponse_OkResponse{
OkResponse: &authv3.OkHttpResponse{Headers: headers},
},
}
}
func deny(code int32, msg string) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &typev3.Status{Code: int32(typev3.Code_PERMISSION_DENIED)},
HttpResponse: &authv3.CheckResponse_DeniedResponse{
DeniedResponse: &authv3.DeniedHttpResponse{
Status: &typev3.HttpStatus{Code: typev3.StatusCode(code)},
Body: "{\"code\":" + strconv.Itoa(int(code)) + ",\"message\":\"" + msg + "\"}",
Headers: []*core.HeaderValueOption{
{Header: &core.HeaderValue{Key: "content-type", Value: "application/json"}},
},
},
},
}
}
func extractCookie(headers map[string]string, name string) string {
c := headers["cookie"]
parts := strings.Split(c, ";")
for _, p := range parts {
kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
if len(kv) == 2 && kv[0] == name {
return kv[1]
}
}
return ""
}
func parseUserIDFromPath(path string) (int64, error) {
// 仅示例:请按你的真实路由解析,或改为从 token claim 取 userId
seg := strings.Split(strings.Trim(path, "/"), "/")
for i := 0; i < len(seg); i++ {
if seg[i] == "users" && i+1 < len(seg) {
return strconv.ParseInt(seg[i+1], 10, 64)
}
}
return 0, strconv.ErrSyntax
}
func main() {
lis, _ := net.Listen("tcp", ":9002")
grpcServer := grpc.NewServer()
// TODO: 创建 user-rpc client 并注入
// userRpcClient := ...
authv3.RegisterAuthorizationServer(grpcServer, &server{userRpc: nil})
_ = grpcServer.Serve(lis)
}
```
---
## 3) Envoy 最小配置片段(插入现有 `http_filters`
`envoy.filters.http.router` 之前加入:
```yaml
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
failure_mode_allow: false
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
grpc_service:
envoy_grpc:
cluster_name: authz_adapter_cluster
timeout: 0.5s
```
并在 `clusters` 中加入:
```yaml
- name: authz_adapter_cluster
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: authz_adapter_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: authz-adapter-svc.juwan.svc.cluster.local
port_value: 9002
http2_protocol_options: {}
```
> gRPC 上游必须启用 `http2_protocol_options: {}`。
---
## 4) 落地清单
1. 新建 `authz-adapter` 服务(Deployment + Service)。
2. Adapter 内部连 `user-rpc-svc:9001`,调用 `ValidateToken`
3. Envoy 加 `ext_authz` filter + `authz_adapter_cluster`
4. 明确失败语义:
- 无 token -> 401
- token 无效/过期 -> 401
- 权限不足 -> 403
5. 透传统一鉴权头:`x-auth-user-id``x-auth-role-type`
6. 灰度建议:先仅对 `/api/users` 开启,再扩展到 `/api/email`
> 实践建议:若保留 K8s `readiness/liveness` 探针使用 `/healthz`,请确保该路径在 `ext_authz` 上也放行,否则会出现探针 403 导致 Pod 重启。
---
## 5) 与当前 `jwt_authn` 的关系
- 可以并存:
-`jwt_authn` 快速验签
-`ext_authz` 做 Redis 会话态、黑名单、细粒度权限
- 也可以只保留 `ext_authz`(由 adapter 内完成全部逻辑)。
推荐:**先并存**,稳定后再决定是否简化。
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -3,26 +3,31 @@ module juwan-backend
go 1.25.1 go 1.25.1
require ( require (
github.com/envoyproxy/go-control-plane/envoy v1.36.0
github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lib/pq v1.11.2 github.com/lib/pq v1.11.2
github.com/redis/go-redis/v9 v9.17.3 github.com/redis/go-redis/v9 v9.17.3
github.com/zeromicro/go-queue v1.2.2
github.com/zeromicro/go-zero v1.10.0 github.com/zeromicro/go-zero v1.10.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.79.1
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@@ -37,7 +42,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
@@ -50,13 +55,13 @@ require (
github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/segmentio/kafka-go v0.4.47 // indirect github.com/segmentio/kafka-go v0.4.47 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/zeromicro/go-queue v1.2.2 // indirect
go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect
@@ -70,7 +75,7 @@ require (
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
@@ -84,7 +89,6 @@ require (
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.10.0 // indirect golang.org/x/time v0.10.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+18 -5
View File
@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
@@ -16,6 +18,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -28,6 +32,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -70,8 +78,8 @@ github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -92,7 +100,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -122,6 +129,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -155,8 +164,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -195,8 +208,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=