diff --git a/app/authz/adapter/README.md b/app/authz/adapter/README.md new file mode 100644 index 0000000..a4609fa --- /dev/null +++ b/app/authz/adapter/README.md @@ -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` diff --git a/app/authz/adapter/authz.go b/app/authz/adapter/authz.go new file mode 100644 index 0000000..0957d03 --- /dev/null +++ b/app/authz/adapter/authz.go @@ -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) + } + } +} diff --git a/app/users/api/internal/handler/user/loginHandler.go b/app/users/api/internal/handler/user/loginHandler.go index 6bdd0b2..5ab1abe 100644 --- a/app/users/api/internal/handler/user/loginHandler.go +++ b/app/users/api/internal/handler/user/loginHandler.go @@ -23,25 +23,25 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { l := user.NewLoginLogic(r.Context(), svcCtx) 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 { httpx.ErrorCtx(r.Context(), w, err) } 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) } } diff --git a/app/users/api/internal/handler/user/registerHandler.go b/app/users/api/internal/handler/user/registerHandler.go index 9cb375a..affba01 100644 --- a/app/users/api/internal/handler/user/registerHandler.go +++ b/app/users/api/internal/handler/user/registerHandler.go @@ -46,9 +46,9 @@ func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { resp, err := l.Register(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + httpx.ErrorCtx(r.Context(), w, utils.NewErrorResp(400, err)) } else { - httpx.OkJsonCtx(r.Context(), w, utils.NewErrorResp(400, err)) + httpx.OkJsonCtx(r.Context(), w, resp) } } } diff --git a/app/users/api/internal/logic/user/loginLogic.go b/app/users/api/internal/logic/user/loginLogic.go index baa6522..767c8b8 100644 --- a/app/users/api/internal/logic/user/loginLogic.go +++ b/app/users/api/internal/logic/user/loginLogic.go @@ -38,11 +38,17 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro Username: req.Username, Passwd: req.Password, }) + logx.Infof("res:%v", res) if err != nil { logx.Errorf("rpc login err: %v", err) 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{ UserId: res.Id, Username: res.Username, diff --git a/app/users/api/internal/logic/user/registerLogic.go b/app/users/api/internal/logic/user/registerLogic.go index 26520c8..0ba0a0a 100644 --- a/app/users/api/internal/logic/user/registerLogic.go +++ b/app/users/api/internal/logic/user/registerLogic.go @@ -59,7 +59,7 @@ func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterRe requestId, err := contextx.RequestIdFrom(l.ctx) if err != nil { - logx.Errorf("contextx.RequestIdFrom failed: %v", errjA) + logx.Errorf("contextx.RequestIdFrom failed: %v", err) return nil, errors.New("contextx.RequestIdFrom failed") } diff --git a/app/users/rpc/etc/pb.yaml b/app/users/rpc/etc/pb.yaml index b0afa17..f4b2e19 100644 --- a/app/users/rpc/etc/pb.yaml +++ b/app/users/rpc/etc/pb.yaml @@ -28,3 +28,6 @@ CacheConf: Jwt: SecretKey: "${JWT_SECRET_KEY}" Issuer: "juwan-user-rpc" + +Log: + Level: info \ No newline at end of file diff --git a/app/users/rpc/internal/logic/loginLogic.go b/app/users/rpc/internal/logic/loginLogic.go index 9ed7bf2..46fb8ee 100644 --- a/app/users/rpc/internal/logic/loginLogic.go +++ b/app/users/rpc/internal/logic/loginLogic.go @@ -31,6 +31,7 @@ func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) { logx.WithContext(l.ctx).Errorf("LoginLogic.Login error:%v", err) return nil, err } + logx.Infof("user:%v", user) if !utils.VerifyPassword(user.Passwd, in.Passwd) { logx.WithContext(l.ctx).Errorf("User %s Login failed", user.Username) return nil, errors.New("incorrect password") diff --git a/app/users/rpc/internal/logic/validateTokenLogic.go b/app/users/rpc/internal/logic/validateTokenLogic.go index fe4fdf2..68408b7 100644 --- a/app/users/rpc/internal/logic/validateTokenLogic.go +++ b/app/users/rpc/internal/logic/validateTokenLogic.go @@ -2,7 +2,6 @@ package logic import ( "context" - "fmt" "juwan-backend/app/users/rpc/internal/svc" "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) { - 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 { return nil, err } diff --git a/common/utils/responses.go b/common/utils/responses.go index b0b881e..23c0693 100644 --- a/common/utils/responses.go +++ b/common/utils/responses.go @@ -1,10 +1,20 @@ package utils +import "encoding/json" + type ErrorResponse struct { Code int `json:"code"` 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 { return &ErrorResponse{ Code: code, diff --git a/deploy/k8s/envoy/envoy copy.yaml b/deploy/k8s/envoy/envoy copy.yaml new file mode 100644 index 0000000..e54923e --- /dev/null +++ b/deploy/k8s/envoy/envoy copy.yaml @@ -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 diff --git a/deploy/k8s/envoy/envoy.yaml b/deploy/k8s/envoy/envoy.yaml index a64b0ea..ad4f8a5 100644 --- a/deploy/k8s/envoy/envoy.yaml +++ b/deploy/k8s/envoy/envoy.yaml @@ -21,6 +21,16 @@ data: 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: @@ -33,31 +43,68 @@ data: status: 200 body: 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: - prefix: /api/email + path: /api/users/login route: - cluster: email_api_cluster + 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: + 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: prefix: /api/users route: cluster: user_api_cluster 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: 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_COOKIE = "csrf_token" - local GUARD_COOKIE = "csrf_guard" - local TOKEN_HEADER = "x-csrf-token" - local GUARD_HEADER = "x-csrf-guard" + local TOKEN_HEADER = "xsrf-token" + local TOKEN_COOKIE = "__Host-XSRF-TOKEN" + local GUARD_COOKIE = "__Host-XSRF-GUARD" local seeded = false @@ -100,42 +147,41 @@ data: local cookie_header = headers:get("cookie") local cookies = split_cookie(cookie_header) - local csrf_token_cookie = cookies[TOKEN_COOKIE] - local csrf_guard_cookie = cookies[GUARD_COOKIE] + 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", csrf_token_cookie == nil or csrf_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_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 csrf_token_cookie == nil or csrf_token_cookie == "" then - csrf_token_cookie = build_token(headers:get("x-request-id")) - request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", csrf_token_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", csrf_token_cookie) + request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie) end - if csrf_guard_cookie == nil or csrf_guard_cookie == "" then - csrf_guard_cookie = build_token(headers:get("x-request-id")) - request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", csrf_guard_cookie) + 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", csrf_guard_cookie) + request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie) end if is_safe_method(method) then return end - local csrf_token_header = headers:get(TOKEN_HEADER) - local csrf_guard_header = headers:get(GUARD_HEADER) + local token_header = headers:get(TOKEN_HEADER) - if csrf_token_header == nil or csrf_guard_header == nil then + if token_header == nil or token_header == "" then request_handle:respond( {[":status"] = "403", ["content-type"] = "application/json"}, - '{"code":403,"message":"missing csrf headers"}' + '{"code":403,"message":"missing XSRF-TOKEN header"}' ) return 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( {[":status"] = "403", ["content-type"] = "application/json"}, '{"code":403,"message":"missing csrf cookies"}' @@ -143,10 +189,10 @@ data: return 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( {[":status"] = "403", ["content-type"] = "application/json"}, - '{"code":403,"message":"csrf token mismatch"}' + '{"code":403,"message":"xsrf token mismatch"}' ) return end @@ -164,17 +210,65 @@ data: 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=/; SameSite=Strict" + 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=/; SameSite=Strict" + 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: + 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 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router @@ -193,6 +287,7 @@ data: socket_address: address: user-api-svc.juwan.svc.cluster.local port_value: 8888 + - name: email_api_cluster connect_timeout: 2s type: STRICT_DNS @@ -207,6 +302,21 @@ data: 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: @@ -233,6 +343,7 @@ spec: labels: app: envoy-gateway spec: + serviceAccountName: envoy-gateway containers: - name: envoy image: envoyproxy/envoy:v1.31-latest @@ -260,13 +371,6 @@ spec: port: 8080 initialDelaySeconds: 5 periodSeconds: 10 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi volumeMounts: - name: envoy-config mountPath: /etc/envoy diff --git a/deploy/k8s/postgreSql.yaml b/deploy/k8s/postgreSql.yaml index 7db9703..ba1f0cc 100644 --- a/deploy/k8s/postgreSql.yaml +++ b/deploy/k8s/postgreSql.yaml @@ -12,10 +12,10 @@ spec: s3Credentials: accessKeyId: name: rc-creds - key: SOucqRaJr4OyfcIu + key: ACCESS_KEY_ID secretAccessKey: name: rc-creds - key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz + key: SECRET_ACCESS_KEY wal: compression: gzip storage: diff --git a/deploy/k8s/secrets/DEPLOYMENT.md b/deploy/k8s/secrets/DEPLOYMENT.md deleted file mode 100644 index 938a60a..0000000 --- a/deploy/k8s/secrets/DEPLOYMENT.md +++ /dev/null @@ -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: - - identity: {} - ``` - - 替换 `` 为第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 访问 diff --git a/deploy/k8s/secrets/ENCRYPTION.md b/deploy/k8s/secrets/ENCRYPTION.md deleted file mode 100644 index 973ed36..0000000 --- a/deploy/k8s/secrets/ENCRYPTION.md +++ /dev/null @@ -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 && kubectl create secret ...` - - Or use: `kubectl patch secret -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 -``` diff --git a/deploy/k8s/secrets/FLOWCHART.md b/deploy/k8s/secrets/FLOWCHART.md deleted file mode 100644 index 4cf5bcc..0000000 --- a/deploy/k8s/secrets/FLOWCHART.md +++ /dev/null @@ -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 -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) diff --git a/deploy/k8s/secrets/INDEX.md b/deploy/k8s/secrets/INDEX.md deleted file mode 100644 index d4d165d..0000000 --- a/deploy/k8s/secrets/INDEX.md +++ /dev/null @@ -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 分钟 | diff --git a/deploy/k8s/secrets/QUICK_REFERENCE.md b/deploy/k8s/secrets/QUICK_REFERENCE.md deleted file mode 100644 index 085ebfa..0000000 --- a/deploy/k8s/secrets/QUICK_REFERENCE.md +++ /dev/null @@ -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 --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 < - - 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 -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 -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 - 加密配置 diff --git a/deploy/k8s/secrets/README.md b/deploy/k8s/secrets/README.md deleted file mode 100644 index 7a4ed4b..0000000 --- a/deploy/k8s/secrets/README.md +++ /dev/null @@ -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 -``` diff --git a/deploy/k8s/secrets/SUMMARY.md b/deploy/k8s/secrets/SUMMARY.md deleted file mode 100644 index 1e1247d..0000000 --- a/deploy/k8s/secrets/SUMMARY.md +++ /dev/null @@ -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: │ │ │ -│ │ │ └─ 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-secret(RBAC 拒绝) - ❌ 删除 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 双令牌验证 - -现在可以部署并集成到应用中了! diff --git a/deploy/k8s/secrets/VERIFICATION.md b/deploy/k8s/secrets/VERIFICATION.md deleted file mode 100644 index 16bedf5..0000000 --- a/deploy/k8s/secrets/VERIFICATION.md +++ /dev/null @@ -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: - -# 验证 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-redis,Status 应该是 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-redis(ClusterIP)服务 -``` - -### 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 - -# 检查 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 -n juwan - -# 查看完整日志(包括初始化容器) -kubectl logs -n juwan --all-containers=true --previous - -# 检查 Pod 资源限制是否导致 OOMKilled -kubectl get event -n juwan --sort-by='.lastTimestamp' -``` - -### 权限被拒绝错误 - -```bash -# 验证 ServiceAccount 是否正确 -kubectl get pod -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 -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 秘钥 diff --git a/deploy/k8s/secrets/jwt-secret.yaml b/deploy/k8s/secrets/jwt-secret.yaml index 90342a9..7240df9 100644 --- a/deploy/k8s/secrets/jwt-secret.yaml +++ b/deploy/k8s/secrets/jwt-secret.yaml @@ -5,7 +5,6 @@ metadata: namespace: juwan type: Opaque data: - # base64 encoded: your-secret-jwt-key-change-this-in-production secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ== --- apiVersion: v1 diff --git a/deploy/k8s/service/authz/authz-adapter.yaml b/deploy/k8s/service/authz/authz-adapter.yaml new file mode 100644 index 0000000..0e248c3 --- /dev/null +++ b/deploy/k8s/service/authz/authz-adapter.yaml @@ -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 diff --git a/deploy/k8s/service/user/user-api.yaml b/deploy/k8s/service/user/user-api.yaml index 94adde9..00a0167 100644 --- a/deploy/k8s/service/user/user-api.yaml +++ b/deploy/k8s/service/user/user-api.yaml @@ -19,7 +19,7 @@ spec: serviceAccountName: find-endpoints containers: - 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: - containerPort: 8888 readinessProbe: diff --git a/deploy/k8s/service/user/user-rpc.yaml b/deploy/k8s/service/user/user-rpc.yaml index 5658e4b..c9d87d1 100644 --- a/deploy/k8s/service/user/user-rpc.yaml +++ b/deploy/k8s/service/user/user-rpc.yaml @@ -29,7 +29,7 @@ spec: ] containers: - 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: - containerPort: 9001 - containerPort: 4001 @@ -160,105 +160,105 @@ spec: # type: Utilization # averageUtilization: 80 #--- -## Redis 主从复制 -#apiVersion: redis.redis.opstreelabs.in/v1beta2 -#kind: RedisReplication -#metadata: -# name: user-redis -# namespace: juwan -#spec: -# clusterSize: 3 -# kubernetesConfig: -# image: quay.io/opstree/redis:v7.0.12 -# imagePullPolicy: IfNotPresent -# resources: -# requests: -# cpu: 100m -# memory: 128Mi -# limits: -# cpu: 500m -# memory: 512Mi -# redisSecret: -# name: user-redis -# key: password -# -# redisExporter: -# enabled: true -# image: quay.io/opstree/redis-exporter:latest -# imagePullPolicy: Always -# podSecurityContext: -# runAsUser: 1000 -# fsGroup: 1000 -# storage: -# volumeClaimTemplate: -# spec: -# accessModes: ["ReadWriteOnce"] -# resources: -# requests: -# storage: 1Gi -# -#--- -## Sentinel 监控 -#apiVersion: redis.redis.opstreelabs.in/v1beta2 -#kind: RedisSentinel -#metadata: -# name: user-redis-sentinel -# namespace: juwan -#spec: -# clusterSize: 3 -# kubernetesConfig: -# image: quay.io/opstree/redis-sentinel:v7.0.12 -# imagePullPolicy: IfNotPresent -# resources: -# requests: -# cpu: 100m -# memory: 128Mi -# limits: -# cpu: 500m -# memory: 512Mi -# podSecurityContext: -# runAsUser: 1000 -# fsGroup: 1000 -# redisSentinelConfig: -# redisReplicationName: user-redis -# masterGroupName: mymaster -# redisPort: "6379" -# quorum: "2" -# downAfterMilliseconds: "5000" -# failoverTimeout: "10000" -# parallelSyncs: "1" -# -#--- -## PostgreSQL 集群 -#apiVersion: postgresql.cnpg.io/v1 -#kind: Cluster -#metadata: -# namespace: juwan -# name: user-db -#spec: -# instances: 3 -# primaryUpdateStrategy: unsupervised -# bootstrap: -# initdb: -# database: app -# owner: app -# # 只在 PVC 为空时初始化 -# postInitSQL: -# - CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -# backup: -# barmanObjectStore: -# destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/ -# endpointURL: https://cn-nb1.rains3.com -# s3Credentials: -# accessKeyId: -# name: rc-creds -# key: SOucqRaJr4OyfcIu -# secretAccessKey: -# name: rc-creds -# key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz -# wal: -# compression: gzip -# storage: -# size: 1Gi -# monitoring: -# enablePodMonitor: true +# Redis 主从复制 +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisReplication +metadata: + name: user-redis + namespace: juwan +spec: + clusterSize: 3 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.12 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + redisSecret: + name: user-redis + key: password + + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:latest + imagePullPolicy: Always + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + storage: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + +--- +# Sentinel 监控 +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisSentinel +metadata: + name: user-redis-sentinel + namespace: juwan +spec: + clusterSize: 3 + kubernetesConfig: + image: quay.io/opstree/redis-sentinel:v7.0.12 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + redisSentinelConfig: + redisReplicationName: user-redis + masterGroupName: mymaster + redisPort: "6379" + quorum: "2" + downAfterMilliseconds: "5000" + failoverTimeout: "10000" + parallelSyncs: "1" + +--- +# PostgreSQL 集群 +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + namespace: juwan + name: user-db +spec: + instances: 3 + primaryUpdateStrategy: unsupervised + bootstrap: + initdb: + database: app + owner: app + # 只在 PVC 为空时初始化 + postInitSQL: + - CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + backup: + barmanObjectStore: + destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/ + endpointURL: https://cn-nb1.rains3.com + s3Credentials: + accessKeyId: + name: rc-creds + key: ACCESS_KEY_ID + secretAccessKey: + name: rc-creds + key: SECRET_ACCESS_KEY + wal: + compression: gzip + storage: + size: 1Gi + monitoring: + enablePodMonitor: true diff --git a/deploy/script/gen-envoy-hs256-jwk.ps1 b/deploy/script/gen-envoy-hs256-jwk.ps1 new file mode 100644 index 0000000..721e307 --- /dev/null +++ b/deploy/script/gen-envoy-hs256-jwk.ps1 @@ -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) diff --git a/docs/ENVOY_EXT_AUTHZ_ADAPTER.md b/docs/ENVOY_EXT_AUTHZ_ADAPTER.md new file mode 100644 index 0000000..32b3881 --- /dev/null +++ b/docs/ENVOY_EXT_AUTHZ_ADAPTER.md @@ -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 内完成全部逻辑)。 + +推荐:**先并存**,稳定后再决定是否简化。 diff --git a/ENVOY_GATEWAY_GUIDE.md b/docs/ENVOY_GATEWAY_GUIDE.md similarity index 68% rename from ENVOY_GATEWAY_GUIDE.md rename to docs/ENVOY_GATEWAY_GUIDE.md index d990caf..5d31b46 100644 --- a/ENVOY_GATEWAY_GUIDE.md +++ b/docs/ENVOY_GATEWAY_GUIDE.md @@ -1,617 +1,814 @@ -# Envoy Gateway 配置指南(带 JWT 认证) - -## 📋 目录 - -1. [快速开始](#快速开始) -2. [添加新服务](#添加新服务) -3. [JWT 认证配置](#jwt-认证配置) -4. [分级访问控制](#分级访问控制) -5. [故障排查](#故障排查) - ---- - -## 快速开始 - -### 前置条件 - -- K8s 集群正在运行(已验证 ✅) -- Envoy Gateway Pod 处于 Running 状态 -- 所有后端服务已部署 - -### 当前网关状态 - -```bash -# 查看 Envoy Pod -kubectl get pods -n juwan -l app=envoy-gateway - -# 查看网关 Service -kubectl get svc -n juwan envoy-gateway - -# 查看 ConfigMap -kubectl get cm -n juwan envoy-config -``` - -### 访问网关 - -```bash -# 通过 kubectl 端口转发(本地测试) -kubectl port-forward -n juwan svc/envoy-gateway 8080:80 & - -# 测试 -curl http://localhost:8080/api/users/login -``` - ---- - -## 添加新服务 - -### 场景:添加 Product 服务 - -#### 1. 创建服务的 K8s 部署清单 - -编辑或创建 `deploy/k8s/service/product/product-api.yaml`: - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: juwan ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: product-api-config - namespace: juwan -data: - product-api.yaml: | - Name: product-api - Host: 0.0.0.0 - Port: 8890 - Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: product-api - namespace: juwan - labels: - app: product-api -spec: - replicas: 2 - selector: - matchLabels: - app: product-api - template: - metadata: - labels: - app: product-api - spec: - containers: - - name: api - image: your-registry/product-api:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8890 - name: http - volumeMounts: - - name: config - mountPath: /etc/product-api - env: - - name: TZ - value: "Asia/Shanghai" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - livenessProbe: - httpGet: - path: /health - port: 8890 - initialDelaySeconds: 10 - periodSeconds: 10 - volumes: - - name: config - configMap: - name: product-api-config ---- -apiVersion: v1 -kind: Service -metadata: - name: product-api-svc - namespace: juwan -spec: - selector: - app: product-api - ports: - - port: 8890 - targetPort: 8890 - name: http - type: ClusterIP -``` - -#### 2. 在 Envoy 网关中添加路由 - -编辑 `deploy/k8s/envoy-gateway.yaml`,在 `route_config` 的 `routes` 部分添加: - -```yaml -# ... 在现有路由下方添加: -- match: - prefix: /api/products - route: - cluster: product_api_cluster - timeout: 30s -``` - -#### 3. 在 Envoy 网关中添加上游集群 - -编辑 `deploy/k8s/envoy-gateway.yaml`,在 `clusters` 部分添加: - -```yaml -- name: product_api_cluster - connect_timeout: 5s - type: STRICT_DNS - dns_lookup_family: V4_ONLY - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: product_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: product-api-svc.juwan.svc.cluster.local - port_value: 8890 - health_checks: - - timeout: 3s - interval: 10s - unhealthy_threshold: 2 - healthy_threshold: 2 - http_health_check: - path: /health -``` - -#### 4. 部署到集群 - -```bash -# 部署 Product API -kubectl apply -f deploy/k8s/service/product/product-api.yaml - -# 更新 Envoy 配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 重启 Envoy Pod 以加载新配置 -kubectl delete pods -n juwan -l app=envoy-gateway - -# 验证 -kubectl get pods -n juwan - -# 测试新接口 -curl http://localhost:8080/api/products -``` - ---- - -## JWT 认证配置 - -### 1. 生成 JWT 密钥并存储 - -```bash -# 执行设置脚本 -bash deploy/envoy/setup-jwt-auth.sh - -# 或手动执行 -JWT_SECRET=$(openssl rand -hex 32) -echo "保存这个密钥: $JWT_SECRET" - -# 创建 K8s Secret -kubectl create secret generic jwt-secret \ - --from-literal=key=$JWT_SECRET \ - -n juwan -``` - -### 2. 配置 Envoy JWT 认证 - -编辑 `deploy/k8s/envoy-gateway.yaml`,更新 `http_filters` 部分: - -```yaml -http_filters: - # JWT 认证过滤器(必须在 router 之前) - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - - providers: - default: - issuer: "juwan" - audiences: "api" - # 使用 ConfigMap 中的 JWKS(已通过 volumeMount 挂载) - local_jwks: - filename: /etc/envoy/jwks.json - - rules: - # 规则1: 登录端点不需要认证 - - match: - prefix: /api/users/login - allow_missing_or_failed: true - - # 规则2: 注册端点不需要认证 - - match: - prefix: /api/users/register - allow_missing_or_failed: true - - # 规则3: 获取公开商品列表不需要认证 - - match: - prefix: /api/products - case_sensitive: false - methods: ["GET"] # 仅 GET 不需要认证 - allow_missing_or_failed: true - - # 规则4: 其他所有路由需要认证 - - match: - prefix: "/" - requires: - provider_name: "default" - - # 路由过滤器(在 JWT 认证之后) - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router -``` - -### 3. 在 Envoy Deployment 中挂载 JWKS - -编辑 `deploy/k8s/envoy-gateway.yaml` 的 Deployment 部分: - -```yaml -spec: - # ... 其他配置 ... - template: - spec: - containers: - - name: envoy - # ... 其他配置 ... - volumeMounts: - - name: envoy-config - mountPath: /etc/envoy - - name: jwks-config # ← 新增 - mountPath: /etc/envoy - - volumes: - - name: envoy-config - configMap: - name: envoy-config - - name: jwks-config # ← 新增 - configMap: - name: jwks-config - items: - - key: jwks.json - path: jwks.json -``` - -### 4. 在 API 服务中生成 JWT Token - -在 User API 的 login 端点(`app/users/api/internal/logic/user/loginlogic.go`): - -```go -package user - -import ( - "context" - "time" - "github.com/golang-jwt/jwt/v4" - "app/users/api/internal/svc" - "app/users/api/internal/types" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { - // TODO: 验证用户名和密码 - - // 从配置中获取 JWT 密钥 - jwtSecret := l.svcCtx.Config.JwtSecret - if jwtSecret == "" { - jwtSecret = "default-secret" // 开发环境默认值 - } - - // 生成 JWT Token - claims := jwt.MapClaims{ - "userId": 1, // 实际应从数据库获取 - "username": req.Username, - "exp": time.Now().Add(24 * time.Hour).Unix(), - "iat": time.Now().Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(jwtSecret)) - if err != nil { - return nil, err - } - - return &types.LoginResp{ - Token: tokenString, - Expires: time.Now().Add(24 * time.Hour).Unix(), - UserId: 1, - Username: req.Username, - Email: "user@example.com", - }, nil -} -``` - -### 5. 在 API 配置中设置 JWT 密钥 - -编辑 `app/users/api/etc/user-api.yaml`: - -```yaml -Name: user-api -Host: 0.0.0.0 -Port: 8888 - -JwtSecret: "${JWT_SECRET}" # 环境变量 - -Database: - DataSource: postgres://... - -UserRpc: - Endpoints: - - user-rpc-svc.juwan.svc.cluster.local:50051 -``` - -编辑 `app/users/api/internal/config/config.go`: - -```go -package config - -import "github.com/zeromicro/go-zero/rest" - -type Config struct { - rest.RestConf - JwtSecret string `json:"jwtSecret"` -} -``` - -在 K8s Deployment 中设置环境变量: - -```yaml -spec: - template: - spec: - containers: - - name: api - env: - - name: JWT_SECRET - valueFrom: - secretKeyRef: - name: jwt-secret - key: key -``` - ---- - -## 分级访问控制 - -### 场景1: 获取用户信息(有权限区分) - -如果用户查看自己的信息 → 返回完整数据 -如果用户查看他人信息 → 返回部分数据 - -#### 在 RPC 服务中实现 - -编辑 `app/users/rpc/internal/logic/getUsersByIdLogic.go`: - -```go -func (l *GetUsersByIdLogic) GetUsersById(ctx context.Context, in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 获取请求者的 userId(由 API 层通过 context 传递) - requesterID, ok := ctx.Value("userId").(int64) - if !ok { - requesterID = 0 // 未认证用户 - } - - targetID := in.Id - - // 查询数据库 - user := l.svcCtx.UserModel.FindOne(ctx, targetID) - if user == nil { - return nil, status.Error(codes.NotFound, "user not found") - } - - resp := &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - CreatedAt: user.CreatedAt, - }, - } - - // 权限检查:自己可以看全部,别人只能看部分 - if requesterID == targetID { - resp.Users.Email = user.Email // ✅ 自己可见 - resp.Users.Phone = user.Phone // ✅ 自己可见 - resp.Users.Passwd = "" // ❌ 密码永远不返回 - } - // else: 只返回基本信息(username, userId) - - return resp, nil -} -``` - -#### 在 API 层调用时传递 userId - -编辑 `app/users/api/internal/logic/user/getUserInfoLogic.go`: - -```go -func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { - // 从 context 获取当前认证用户 - currentUserID, ok := l.ctx.Value("userId").(int64) - if !ok { - // 未认证 → 只能查看公开信息 - currentUserID = 0 - } - - // 调用 RPC,传递 userId - ctx := context.WithValue(l.ctx, "userId", currentUserID) - rpcResp, err := l.svcCtx.UserRpc.GetUsersById(ctx, &pb.GetUsersByIdReq{ - Id: req.UserId, - }) - if err != nil { - return nil, err - } - - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - Email: rpcResp.Users.Email, - Phone: rpcResp.Users.Phone, - CreateAt: rpcResp.Users.CreatedAt, - }, nil -} -``` - -### 场景2: 修改用户信息(只能修改自己) - -```go -func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { - // 获取当前认证用户 - currentUserID, ok := l.ctx.Value("userId").(int64) - if !ok { - return nil, errors.New("unauthorized") - } - - // 权限检查:只能修改自己的信息 - if currentUserID != req.UserId { - return nil, errors.New("forbidden: can only update your own info") - } - - // 更新用户信息 - // ... - - return &types.UpdateUserInfoResp{ - Message: "更新成功", - }, nil -} -``` - ---- - -## 故障排查 - -### 问题1: Envoy Pod 启动失败 - -```bash -# 查看日志 -kubectl logs -n juwan -l app=envoy-gateway --tail=100 - -# 常见错误及解决 -# Error: "no such field" -# → YAML 字段名拼写错误或与 Envoy 版本不兼容 -# → 检查 Envoy 版本并查看官方文档 - -# Error: "unknown cluster" -# → envoy-gateway.yaml 中缺少 cluster 定义 -# → 确保添加了所有需要的 cluster 部分 - -# Error: "unknown extension type" -# → 使用了 Envoy 不支持的扩展类型 -# → 检查 "@type" 字段是否正确 -``` - -### 问题2: JWT 认证失败 - -```bash -# 验证 JWKS ConfigMap 是否存在 -kubectl get cm -n juwan jwks-config - -# 查看 JWKS 内容 -kubectl get cm jwks-config -n juwan -o jsonpath='{.data.jwks\.json}' - -# 验证 Envoy 能否读取 JWKS -kubectl exec -it {envoy-pod-name} -n juwan -- ls -la /etc/envoy/ - -# 测试没有 Token 的请求(应返回 401) -curl -v http://localhost/api/users/1 - -# 测试有效 Token 的请求 -TOKEN="your-jwt-token" -curl -H "Authorization: Bearer $TOKEN" http://localhost/api/users/1 -``` - -### 问题3: 后端服务无法访问 - -```bash -# 查看 Service 是否存在 -kubectl get svc -n juwan - -# 测试 DNS 解析 -kubectl exec -it {pod-name} -n juwan -- \ - nslookup product-api-svc.juwan.svc.cluster.local - -# 查看 Pod 是否正确运行 -kubectl get pods -n juwan -l app=product-api - -# 查看后端服务日志 -kubectl logs -n juwan -l app=product-api --tail=50 - -# Envoy 检查上游集群状态 -kubectl exec -it {envoy-pod-name} -n juwan -- \ - curl localhost:9901/clusters | grep -A5 product_api_cluster -``` - -### 问题4: 跨域请求失败 - -如果前端遇到 CORS 问题: - -```yaml -# 在 Envoy 配置中添加 CORS 过滤器 -http_filters: - - name: envoy.filters.http.cors - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - - # JWT 认证过滤器(在 CORS 之后) - - name: envoy.filters.http.jwt_authn - # ... -``` - ---- - -## 配置更新流程 - -每次修改 `envoy-gateway.yaml` 后的完整更新步骤: - -```bash -# 1. 验证 YAML 语法 -kubectl apply -f deploy/k8s/envoy-gateway.yaml --dry-run=client - -# 2. 应用配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 3. 监控 Pod 重启(应该自动重新加载) -kubectl get pods -n juwan -l app=envoy-gateway -w - -# 4. 查看最新日志确认无错误 -kubectl logs -n juwan -l app=envoy-gateway --tail=50 - -# 5. 测试新配置 -curl http://localhost/api/your-new-endpoint -``` - ---- - -## 总结 - -| 任务 | 文件 | 说明 | -|-----|------|------| -| 添加新 API | `desc/api/`, `app/*/api/` | 定义接口并实现业务逻辑 | -| 添加新 RPC | `desc/rpc/`, `app/*/rpc/` | 内部服务通信(不通过网关) | -| 更新网关路由 | `deploy/k8s/envoy-gateway.yaml` | 添加路由、集群、认证规则 | -| 配置认证 | `deploy/envoy/setup-jwt-auth.sh` | 生成和管理 JWT 密钥 | -| 部署到 K8s | `deploy/k8s/service/` | 创建服务的 Deployment 和 Service | - -需要更多帮助?查看 `PROJECT_GUIDE.md` 了解完整的项目架构和工作流! +# Envoy Gateway 配置指南(带 JWT 认证) + +## 📋 目录 + +1. [快速开始](#快速开始) +2. [添加新服务](#添加新服务) +3. [JWT 认证配置](#jwt-认证配置) +4. [分级访问控制](#分级访问控制) +5. [故障排查](#故障排查) +6. [当前实现说明(与仓库配置对齐)](#当前实现说明与仓库配置对齐) +7. [ext_authz 适配方案](#ext_authz-适配方案) +8. [前端接入示例(邮箱验证码)](#前端接入示例邮箱验证码) + +--- + +## 当前实现说明(与仓库配置对齐) + +> 本节对应当前实际配置文件:`deploy/k8s/envoy/envoy.yaml`。 + +### 0) 当前公共路由(不需要登录) + +当前网关对以下路径做了“公共放行”: + +- `/healthz`(直返 200,用于探针) +- `POST /api/users/login` +- `POST /api/users/register` +- `POST /api/email/verification-code/send`(注册/登录前发送验证码) + +实现方式: + +- 在 `jwt_authn.rules` 中将上述路径加入白名单(不要求 JWT) +- 在路由层对上述路径关闭 `ext_authz`(避免公共接口和探针被二次鉴权拦截) + +### 1) 用户认证后,`UserId` 放在哪里? + +当前 Envoy 使用 `envoy.filters.http.jwt_authn` 做 JWT 校验,校验通过后通过 `claim_to_headers` 将 claim 注入到转发请求头: + +- `UserId` -> `x-auth-user-id` +- `IsAdmin` -> `x-auth-is-admin` + +也就是说,后端 API(如 user-api/email-api)拿到的是 HTTP Header,不是 Envoy 动态元数据。 + +### 2) 当前配置是否实现了“换票”(token renew)? + +没有。 + +当前 Envoy 配置仅负责: + +- 从 Cookie `JToken` 提取 JWT +- 用 HS256 + `issuer: juwan-user-rpc` 验签与过期检查 + +当 token 过期时,`jwt_authn` 会直接拒绝请求,不会调用 user-rpc 的 `JwtManager.Renew`。 + +### 3) Envoy 能否直接调用 `user-rpc.ValidateToken`? + +结论:不能“直接”用现有 `ValidateToken` protobuf 接口接入 Envoy 认证链。 + +原因: + +- Envoy 内置认证过滤器(如 `jwt_authn`、`ext_authz`)要求固定协议。 +- `ext_authz` 的 gRPC 需要实现 Envoy 标准服务 `envoy.service.auth.v3.Authorization`,不是业务自定义的 `pb.usercenter/ValidateToken`。 + +可行方案(推荐顺序): + +1. **推荐**:新增一个 `authz-adapter` 服务,实现 Envoy `ext_authz` 协议,内部再调用 `user-rpc.ValidateToken`。 +2. 备选:提供一个内部 HTTP 鉴权端点(例如 user-api internal route),Envoy 通过 `ext_authz` HTTP 模式或 Lua `httpCall()` 调用。 + +如果要走方案 1(推荐),你需要补齐: + +- `authz-adapter` 服务(实现 Envoy `CheckRequest/CheckResponse`) +- Envoy 新增 `ext_authz` filter 与对应 cluster +- 鉴权透传头约定(至少 `x-auth-user-id`、`x-auth-is-admin`) +- 失败码与错误体规范(401/403) +- 性能与可用性策略(超时、失败回退、缓存) + +### 4) 与你现有 `ValidateTokenLogic` 的一致性提醒 + +当前 `app/users/rpc/internal/logic/validateTokenLogic.go` 中: + +- 代码使用 `jwt:%v` 格式拼接 `redisKey` +- 但 `JwtManager.Valid()` 需要传入的是 **JWT token 字符串本身** + +这意味着若后续接入 `ext_authz` 并调用该逻辑,建议先修正这段逻辑,避免认证结果偏差。 + +--- + +## ext_authz 适配方案 + +如果你希望 Envoy 在鉴权阶段调用 `user-rpc.ValidateToken`,请看完整落地文档: + +- [ENVOY_EXT_AUTHZ_ADAPTER.md](ENVOY_EXT_AUTHZ_ADAPTER.md) + +该文档包含: + +- Envoy 标准 `Authorization.Check` 最小实现骨架 +- 调用 `user-rpc.ValidateToken` 的适配逻辑示例 +- 可直接嵌入现有网关的 `ext_authz` filter + cluster 配置片段 + +--- + +## 前端接入示例(邮箱验证码) + +以下示例基于你当前网关与服务配置: + +- 登录:`POST /api/users/login`(公共放行) +- 发送验证码:`POST /api/email/verification-code/send`(公共放行,无需登录) +- CSRF 头:`XSRF-TOKEN`(请求头) +- CSRF Cookie:`__Host-XSRF-TOKEN`(可读) +- JWT Cookie:`JToken`(`HttpOnly`,前端不可读,但会随请求自动携带) + +> 注意:当前 Envoy 给 CSRF Cookie 设置了 `Secure` + `SameSite=Strict`。前端必须走 **HTTPS 且同站点** 才能稳定工作。 + +### 接入流程 + +1. 先发一个安全方法请求(GET),让 Envoy 下发 XSRF 双 Cookie。 +2. 注册场景可直接调用发送验证码接口,仅需携带 `XSRF-TOKEN`。 +3. 登录场景可先调用登录接口拿 `JToken`,后续访问受保护接口时自动携带。 + +### 前端示例(TypeScript + fetch) + +```ts +const API_BASE = "https://your-gateway-domain"; + +function getCookie(name: string): string { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : ""; +} + +async function primeXsrfCookies() { + await fetch(`${API_BASE}/healthz`, { + method: "GET", + credentials: "include", + }); +} + +async function login(username: string, password: string) { + const xsrfToken = getCookie("__Host-XSRF-TOKEN"); + const res = await fetch(`${API_BASE}/api/users/login`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "XSRF-TOKEN": xsrfToken, + }, + body: JSON.stringify({ username, password }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`login failed: ${res.status} ${text}`); + } + + return res.json(); +} + +type SendCodeReq = { + email: string; + scene: "register" | "login" | "reset_password" | "bind_email"; +}; + +async function sendVerificationCode(req: SendCodeReq) { + const xsrfToken = getCookie("__Host-XSRF-TOKEN"); + const res = await fetch(`${API_BASE}/api/email/verification-code/send`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "XSRF-TOKEN": xsrfToken, + }, + body: JSON.stringify(req), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`send code failed: ${res.status} ${text}`); + } + + return res.json() as Promise<{ + requestId: string; + expireInSec: number; + message: string; + }>; +} + +// 页面初始化时建议执行一次 +await primeXsrfCookies(); + +// 注册场景:无需登录即可发送验证码 +const data = await sendVerificationCode({ + email: "alice@example.com", + scene: "register", +}); + +console.log("code request:", data); + +// 如需调用受保护接口,再执行登录 +await login("alice", "P@ssw0rd"); +``` + +### 常见前端坑位 + +- 必须加 `credentials: "include"`,否则 Cookie 不会带上。 +- `JToken` 是 `HttpOnly`,前端读不到,这是正常设计。 +- 如果你前后端跨站点,`SameSite=Strict` 会导致 Cookie 不发送;需要改网关 Cookie 策略。 +- 本地 `http://localhost` 下,`Secure` Cookie 不会生效;建议本地也走 HTTPS(例如反向代理或证书)。 + +--- + +## 快速开始 + +### 前置条件 + +- K8s 集群正在运行(已验证 ✅) +- Envoy Gateway Pod 处于 Running 状态 +- 所有后端服务已部署 + +### 当前网关状态 + +```bash +# 查看 Envoy Pod +kubectl get pods -n juwan -l app=envoy-gateway + +# 查看网关 Service +kubectl get svc -n juwan envoy-gateway + +# 查看 ConfigMap +kubectl get cm -n juwan envoy-config +``` + +### 访问网关 + +```bash +# 通过 kubectl 端口转发(本地测试) +kubectl port-forward -n juwan svc/envoy-gateway 8080:80 & + +# 测试 +curl http://localhost:8080/api/users/login +``` + +--- + +## 添加新服务 + +### 场景:添加 Product 服务 + +#### 1. 创建服务的 K8s 部署清单 + +编辑或创建 `deploy/k8s/service/product/product-api.yaml`: + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: juwan +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: product-api-config + namespace: juwan +data: + product-api.yaml: | + Name: product-api + Host: 0.0.0.0 + Port: 8890 + Database: + DataSource: postgres://user:pass@pg-dx:5432/juwan +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: product-api + namespace: juwan + labels: + app: product-api +spec: + replicas: 2 + selector: + matchLabels: + app: product-api + template: + metadata: + labels: + app: product-api + spec: + containers: + - name: api + image: your-registry/product-api:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8890 + name: http + volumeMounts: + - name: config + mountPath: /etc/product-api + env: + - name: TZ + value: "Asia/Shanghai" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: /health + port: 8890 + initialDelaySeconds: 10 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: product-api-config +--- +apiVersion: v1 +kind: Service +metadata: + name: product-api-svc + namespace: juwan +spec: + selector: + app: product-api + ports: + - port: 8890 + targetPort: 8890 + name: http + type: ClusterIP +``` + +#### 2. 在 Envoy 网关中添加路由 + +编辑 `deploy/k8s/envoy-gateway.yaml`,在 `route_config` 的 `routes` 部分添加: + +```yaml +# ... 在现有路由下方添加: +- match: + prefix: /api/products + route: + cluster: product_api_cluster + timeout: 30s +``` + +#### 3. 在 Envoy 网关中添加上游集群 + +编辑 `deploy/k8s/envoy-gateway.yaml`,在 `clusters` 部分添加: + +```yaml +- name: product_api_cluster + connect_timeout: 5s + type: STRICT_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: product_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: product-api-svc.juwan.svc.cluster.local + port_value: 8890 + health_checks: + - timeout: 3s + interval: 10s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /health +``` + +#### 4. 部署到集群 + +```bash +# 部署 Product API +kubectl apply -f deploy/k8s/service/product/product-api.yaml + +# 更新 Envoy 配置 +kubectl apply -f deploy/k8s/envoy-gateway.yaml + +# 重启 Envoy Pod 以加载新配置 +kubectl delete pods -n juwan -l app=envoy-gateway + +# 验证 +kubectl get pods -n juwan + +# 测试新接口 +curl http://localhost:8080/api/products +``` + +--- + +## JWT 认证配置 + +### 1. 生成 JWT 密钥并存储 + +```bash +# 执行设置脚本 +bash deploy/envoy/setup-jwt-auth.sh + +# 或手动执行 +JWT_SECRET=$(openssl rand -hex 32) +echo "保存这个密钥: $JWT_SECRET" + +# 创建 K8s Secret +kubectl create secret generic jwt-secret \ + --from-literal=key=$JWT_SECRET \ + -n juwan +``` + +### 2. 配置 Envoy JWT 认证 + +编辑 `deploy/k8s/envoy-gateway.yaml`,更新 `http_filters` 部分: + +```yaml +http_filters: + # JWT 认证过滤器(必须在 router 之前) + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + + providers: + default: + issuer: "juwan" + audiences: "api" + # 使用 ConfigMap 中的 JWKS(已通过 volumeMount 挂载) + local_jwks: + filename: /etc/envoy/jwks.json + + rules: + # 规则1: 登录端点不需要认证 + - match: + prefix: /api/users/login + allow_missing_or_failed: true + + # 规则2: 注册端点不需要认证 + - match: + prefix: /api/users/register + allow_missing_or_failed: true + + # 规则3: 获取公开商品列表不需要认证 + - match: + prefix: /api/products + case_sensitive: false + methods: ["GET"] # 仅 GET 不需要认证 + allow_missing_or_failed: true + + # 规则4: 其他所有路由需要认证 + - match: + prefix: "/" + requires: + provider_name: "default" + + # 路由过滤器(在 JWT 认证之后) + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +``` + +### 3. 在 Envoy Deployment 中挂载 JWKS + +编辑 `deploy/k8s/envoy-gateway.yaml` 的 Deployment 部分: + +```yaml +spec: + # ... 其他配置 ... + template: + spec: + containers: + - name: envoy + # ... 其他配置 ... + volumeMounts: + - name: envoy-config + mountPath: /etc/envoy + - name: jwks-config # ← 新增 + mountPath: /etc/envoy + + volumes: + - name: envoy-config + configMap: + name: envoy-config + - name: jwks-config # ← 新增 + configMap: + name: jwks-config + items: + - key: jwks.json + path: jwks.json +``` + +### 4. 在 API 服务中生成 JWT Token + +在 User API 的 login 端点(`app/users/api/internal/logic/user/loginlogic.go`): + +```go +package user + +import ( + "context" + "time" + "github.com/golang-jwt/jwt/v4" + "app/users/api/internal/svc" + "app/users/api/internal/types" +) + +type LoginLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { + // TODO: 验证用户名和密码 + + // 从配置中获取 JWT 密钥 + jwtSecret := l.svcCtx.Config.JwtSecret + if jwtSecret == "" { + jwtSecret = "default-secret" // 开发环境默认值 + } + + // 生成 JWT Token + claims := jwt.MapClaims{ + "userId": 1, // 实际应从数据库获取 + "username": req.Username, + "exp": time.Now().Add(24 * time.Hour).Unix(), + "iat": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(jwtSecret)) + if err != nil { + return nil, err + } + + return &types.LoginResp{ + Token: tokenString, + Expires: time.Now().Add(24 * time.Hour).Unix(), + UserId: 1, + Username: req.Username, + Email: "user@example.com", + }, nil +} +``` + +### 5. 在 API 配置中设置 JWT 密钥 + +编辑 `app/users/api/etc/user-api.yaml`: + +```yaml +Name: user-api +Host: 0.0.0.0 +Port: 8888 + +JwtSecret: "${JWT_SECRET}" # 环境变量 + +Database: + DataSource: postgres://... + +UserRpc: + Endpoints: + - user-rpc-svc.juwan.svc.cluster.local:50051 +``` + +编辑 `app/users/api/internal/config/config.go`: + +```go +package config + +import "github.com/zeromicro/go-zero/rest" + +type Config struct { + rest.RestConf + JwtSecret string `json:"jwtSecret"` +} +``` + +在 K8s Deployment 中设置环境变量: + +```yaml +spec: + template: + spec: + containers: + - name: api + env: + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: jwt-secret + key: key +``` + +--- + +## 分级访问控制 + +### 场景1: 获取用户信息(有权限区分) + +如果用户查看自己的信息 → 返回完整数据 +如果用户查看他人信息 → 返回部分数据 + +#### 在 RPC 服务中实现 + +编辑 `app/users/rpc/internal/logic/getUsersByIdLogic.go`: + +```go +func (l *GetUsersByIdLogic) GetUsersById(ctx context.Context, in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { + // 获取请求者的 userId(由 API 层通过 context 传递) + requesterID, ok := ctx.Value("userId").(int64) + if !ok { + requesterID = 0 // 未认证用户 + } + + targetID := in.Id + + // 查询数据库 + user := l.svcCtx.UserModel.FindOne(ctx, targetID) + if user == nil { + return nil, status.Error(codes.NotFound, "user not found") + } + + resp := &pb.GetUsersByIdResp{ + Users: &pb.Users{ + UserId: user.UserId, + Username: user.Username, + CreatedAt: user.CreatedAt, + }, + } + + // 权限检查:自己可以看全部,别人只能看部分 + if requesterID == targetID { + resp.Users.Email = user.Email // ✅ 自己可见 + resp.Users.Phone = user.Phone // ✅ 自己可见 + resp.Users.Passwd = "" // ❌ 密码永远不返回 + } + // else: 只返回基本信息(username, userId) + + return resp, nil +} +``` + +#### 在 API 层调用时传递 userId + +编辑 `app/users/api/internal/logic/user/getUserInfoLogic.go`: + +```go +func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { + // 从 context 获取当前认证用户 + currentUserID, ok := l.ctx.Value("userId").(int64) + if !ok { + // 未认证 → 只能查看公开信息 + currentUserID = 0 + } + + // 调用 RPC,传递 userId + ctx := context.WithValue(l.ctx, "userId", currentUserID) + rpcResp, err := l.svcCtx.UserRpc.GetUsersById(ctx, &pb.GetUsersByIdReq{ + Id: req.UserId, + }) + if err != nil { + return nil, err + } + + return &types.UserInfo{ + UserId: rpcResp.Users.UserId, + Username: rpcResp.Users.Username, + Email: rpcResp.Users.Email, + Phone: rpcResp.Users.Phone, + CreateAt: rpcResp.Users.CreatedAt, + }, nil +} +``` + +### 场景2: 修改用户信息(只能修改自己) + +```go +func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { + // 获取当前认证用户 + currentUserID, ok := l.ctx.Value("userId").(int64) + if !ok { + return nil, errors.New("unauthorized") + } + + // 权限检查:只能修改自己的信息 + if currentUserID != req.UserId { + return nil, errors.New("forbidden: can only update your own info") + } + + // 更新用户信息 + // ... + + return &types.UpdateUserInfoResp{ + Message: "更新成功", + }, nil +} +``` + +--- + +## 故障排查 + +### 问题1: Envoy Pod 启动失败 + +```bash +# 查看日志 +kubectl logs -n juwan -l app=envoy-gateway --tail=100 + +# 常见错误及解决 +# Error: "no such field" +# → YAML 字段名拼写错误或与 Envoy 版本不兼容 +# → 检查 Envoy 版本并查看官方文档 + +# Error: "unknown cluster" +# → envoy-gateway.yaml 中缺少 cluster 定义 +# → 确保添加了所有需要的 cluster 部分 + +# Error: "unknown extension type" +# → 使用了 Envoy 不支持的扩展类型 +# → 检查 "@type" 字段是否正确 +``` + +### 问题2: JWT 认证失败 + +```bash +# 验证 JWKS ConfigMap 是否存在 +kubectl get cm -n juwan jwks-config + +# 查看 JWKS 内容 +kubectl get cm jwks-config -n juwan -o jsonpath='{.data.jwks\.json}' + +# 验证 Envoy 能否读取 JWKS +kubectl exec -it {envoy-pod-name} -n juwan -- ls -la /etc/envoy/ + +# 测试没有 Token 的请求(应返回 401) +curl -v http://localhost/api/users/1 + +# 测试有效 Token 的请求 +TOKEN="your-jwt-token" +curl -H "Authorization: Bearer $TOKEN" http://localhost/api/users/1 +``` + +### 问题3: 后端服务无法访问 + +```bash +# 查看 Service 是否存在 +kubectl get svc -n juwan + +# 测试 DNS 解析 +kubectl exec -it {pod-name} -n juwan -- \ + nslookup product-api-svc.juwan.svc.cluster.local + +# 查看 Pod 是否正确运行 +kubectl get pods -n juwan -l app=product-api + +# 查看后端服务日志 +kubectl logs -n juwan -l app=product-api --tail=50 + +# Envoy 检查上游集群状态 +kubectl exec -it {envoy-pod-name} -n juwan -- \ + curl localhost:9901/clusters | grep -A5 product_api_cluster +``` + +### 问题4: 跨域请求失败 + +如果前端遇到 CORS 问题: + +```yaml +# 在 Envoy 配置中添加 CORS 过滤器 +http_filters: + - name: envoy.filters.http.cors + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors + + # JWT 认证过滤器(在 CORS 之后) + - name: envoy.filters.http.jwt_authn + # ... +``` + +--- + +## 配置更新流程 + +每次修改 `envoy-gateway.yaml` 后的完整更新步骤: + +```bash +# 1. 验证 YAML 语法 +kubectl apply -f deploy/k8s/envoy-gateway.yaml --dry-run=client + +# 2. 应用配置 +kubectl apply -f deploy/k8s/envoy-gateway.yaml + +# 3. 监控 Pod 重启(应该自动重新加载) +kubectl get pods -n juwan -l app=envoy-gateway -w + +# 4. 查看最新日志确认无错误 +kubectl logs -n juwan -l app=envoy-gateway --tail=50 + +# 5. 测试新配置 +curl http://localhost/api/your-new-endpoint +``` + +--- + +## 总结 + +| 任务 | 文件 | 说明 | +|-----|------|------| +| 添加新 API | `desc/api/`, `app/*/api/` | 定义接口并实现业务逻辑 | +| 添加新 RPC | `desc/rpc/`, `app/*/rpc/` | 内部服务通信(不通过网关) | +| 更新网关路由 | `deploy/k8s/envoy-gateway.yaml` | 添加路由、集群、认证规则 | +| 配置认证 | `deploy/envoy/setup-jwt-auth.sh` | 生成和管理 JWT 密钥 | +| 部署到 K8s | `deploy/k8s/service/` | 创建服务的 Deployment 和 Service | + +需要更多帮助?查看 `PROJECT_GUIDE.md` 了解完整的项目架构和工作流! diff --git a/go.mod b/go.mod index 486d038..31a21d3 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,31 @@ module juwan-backend go 1.25.1 require ( + github.com/envoyproxy/go-control-plane/envoy v1.36.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/lib/pq v1.11.2 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 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/protobuf v1.36.11 ) 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/cenkalti/backoff/v4 v4.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-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/go-logr/logr v1.4.3 // 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/grafana/pyroscope-go v1.2.7 // 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/json-iterator/go v1.1.12 // 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/pelletier/go-toml/v2 v2.2.4 // 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_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/segmentio/kafka-go v0.4.47 // 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/client/pkg/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/sdk 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/automaxprocs v1.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/time v0.10.0 // 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/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b15815e..1fa1f41 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 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/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 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/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/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/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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/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/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -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 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +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/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 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/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/yuin/goldmark v1.1.27/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/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/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=