第一章:Golang框架安全红线清单总览
Go 语言因其简洁性与高性能被广泛用于构建 Web 框架(如 Gin、Echo、Fiber),但默认配置常隐含安全风险。开发者需在项目启动初期即识别并规避关键安全红线,而非依赖后期修补。
常见高危配置模式
- 使用
http.ListenAndServe而未启用 HTTPS 或强制重定向,导致明文传输敏感数据; - 框架中间件缺失请求体大小限制,易遭 DoS 攻击(如超大 JSON payload);
- 日志中直接输出用户输入或错误详情(如
fmt.Printf("%v", r.FormValue("email"))),造成信息泄露; - 未禁用调试模式(如 Gin 的
gin.SetMode(gin.DebugMode))上线生产环境。
关键防护动作清单
必须在 main.go 初始化阶段执行以下操作:
// 启用 HTTPS 并强制 HTTP→HTTPS 重定向(使用标准 net/http)
func redirectHTTP() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently)
})
}
// 启动时分别监听 HTTP(仅重定向)和 HTTPS(主服务)
go http.ListenAndServe(":80", redirectHTTP()) // 注意:需配合反向代理或 Let's Encrypt ACME
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", router)
输入验证与上下文隔离
所有外部输入(URL 参数、Header、Body)必须经显式校验:
| 风险点 | 安全实践 |
|---|---|
| SQL 注入 | 禁用 database/sql 拼接,全程使用 ? 占位符参数化查询 |
| XSS | HTML 渲染前调用 html.EscapeString() 或使用模板自动转义 |
| 路径遍历 | 使用 filepath.Clean() 规范化路径,并校验前缀是否在白名单目录内 |
中间件强制启用项
在路由注册前注入以下基础中间件:
SecureHeaders()(设置Content-Security-Policy,X-Content-Type-Options等);Recovery()(捕获 panic 但不暴露堆栈);Timeout(30 * time.Second)(防长连接耗尽资源)。
忽视任一红线均可能导致数据泄露、服务瘫痪或远程代码执行。安全不是附加功能,而是框架初始化的必选步骤。
第二章:注入类漏洞的Go语言特异性表现与防护
2.1 SQL注入在database/sql与ORM中的隐式绕过路径分析与参数化查询模板
隐式字符串拼接的危险模式
以下代码看似使用了sql.Query,实则因fmt.Sprintf提前拼接导致注入漏洞:
// ❌ 危险:参数未进入SQL预编译阶段
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)
rows, _ := db.Query(query) // userInput='admin' OR '1'='1' → 全量泄露
逻辑分析:fmt.Sprintf在Go层完成字符串替换,生成的SQL已固化,database/sql仅执行该恶意语句,参数化机制完全失效。
ORM中的“伪参数化”陷阱
GORM v1.x中Where("name = ?", name)安全,但Where("name = " + name)或Where("name = '" + name + "'")直接绕过绑定。
| 场景 | 是否受参数化保护 | 绕过原理 |
|---|---|---|
db.Where("age > ?", 25) |
✅ | 占位符交由驱动预编译 |
db.Where("age > " + strconv.Itoa(25)) |
❌ | 字符串拼接发生在SQL构造前 |
安全模板范式
// ✅ 正确:始终让参数独立于SQL结构
stmt, _ := db.Prepare("SELECT id FROM orders WHERE status = ? AND created_at > ?")
rows, _ := stmt.Query("shipped", time.Now().AddDate(0,0,-7))
参数说明:?占位符由底层驱动(如pq、mysql)映射为二进制协议参数,服务端严格类型校验,杜绝语法注入。
2.2 OS命令注入在os/exec与syscall调用链中的非显式拼接风险及安全执行封装
风险根源:隐式字符串拼接绕过静态检查
当 os/exec.Command 接收用户输入作为参数时,若通过 fmt.Sprintf 或 strings.Join 动态构造 args,Go 的类型系统无法识别潜在注入点——参数数组本身无拼接痕迹,但运行时仍会交由 shell 解析(尤其 sh -c 场景)。
典型危险模式
// ❌ 危险:非显式拼接,逃逸静态扫描
cmd := exec.Command("sh", "-c", fmt.Sprintf("ls %s | grep %s", userDir, userPattern))
fmt.Sprintf在运行时生成完整命令字符串,sh -c将其交由 shell 解析;userPattern若含; rm -rf /,将触发任意命令执行;os/exec未校验args[1]是否为安全字符串,仅做类型传递。
安全封装原则
- ✅ 始终使用
exec.Command(name, args...)显式参数列表(避免sh -c); - ✅ 对不可信输入强制白名单过滤或路径清理(如
filepath.Clean+filepath.IsAbs校验); - ✅ 封装
SafeCommand结构体,内置参数沙箱与上下文超时。
| 风险模式 | 安全替代方式 |
|---|---|
sh -c "cmd $x" |
exec.Command("cmd", x) |
fmt.Sprintf(...) |
[]string{"cmd", sanitize(x)} |
graph TD
A[用户输入] --> B{是否含元字符?}
B -->|是| C[拒绝/转义]
B -->|否| D[Clean & IsAbs校验]
D --> E[exec.Command-safe]
2.3 模板引擎注入(html/template vs text/template)的上下文感知误用与自动转义加固实践
Go 标准库中 html/template 与 text/template 虽共享语法,但转义策略截然不同:前者按 HTML 上下文(如标签、属性、JS、CSS)动态选择转义规则;后者仅做基础字符替换,无上下文感知能力。
安全边界混淆的典型误用
- 将用户输入直接传入
text/template渲染 HTML 页面 → 绕过所有 HTML 转义 - 在
<script>标签内使用{{.RawJS}}但未用html/template的template.JS类型标记 → 触发 XSS
转义行为对比表
| 场景 | html/template 行为 |
text/template 行为 |
|---|---|---|
{{.UserInput}}(在 <div> 中) |
自动转义 <, >, & 等 |
原样输出,无转义 |
{{.UserInput}}(在 onclick="..." 中) |
使用 HTML 属性转义(含双引号编码) | 仅替换 &,其余裸露 |
// ✅ 正确:显式标注数据类型,启用上下文感知转义
t := template.Must(template.New("").Funcs(template.FuncMap{
"js": func(s string) template.JS { return template.JS(s) },
}))
t.Execute(w, map[string]interface{}{
"InlineScript": `alert("xss")`, // ❌ 危险!需包装为 template.JS
})
该代码若未调用 js 函数包装,html/template 会将其视为普通文本,在 <script> 内输出未转义字符串,导致执行恶意 JS。template.JS 标记告知引擎:此值已由开发者确认为安全 JS 代码,跳过 HTML 转义但保留其他上下文防护。
graph TD
A[用户输入] --> B{模板类型}
B -->|html/template| C[检测当前HTML上下文]
C --> D[应用对应转义:HTML/Attr/JS/CSS/URL]
B -->|text/template| E[仅全局替换 & → &]
E --> F[HTML 渲染时 XSS 风险]
2.4 LDAP注入在go-ldap客户端中的DN构造陷阱与属性白名单校验代码模板
LDAP DN(Distinguished Name)若由用户输入拼接生成,极易触发注入漏洞——如 uid=${user},ou=users,dc=example,dc=com 中未过滤逗号、反斜杠或转义字符,攻击者可注入 uid=admin\),cn=evil 绕过认证。
常见DN构造风险点
- 用户名直接拼入OU/CN字段
- 未对特殊字符(
,,+,",\,<,>,;)执行RFC 4514转义 - 使用
fmt.Sprintf而非ldap.EscapeDN
安全DN构建与属性校验模板
// 安全DN构造:强制转义 + 白名单验证
func BuildSafeUserDN(username string) (string, error) {
// 1. 严格白名单:仅允许字母、数字、下划线、短横线
if !regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`).MatchString(username) {
return "", fmt.Errorf("invalid username format")
}
// 2. RFC 4514转义(go-ldap内置)
escapedUID := ldap.EscapeDN(username)
return fmt.Sprintf("uid=%s,ou=users,dc=example,dc=com", escapedUID), nil
}
逻辑分析:先做正则白名单预筛(防御性第一道防线),再调用
ldap.EscapeDN处理剩余转义需求;EscapeDN自动处理\,,,+,",<,>,;,=等11个危险字符,避免DN解析歧义。
推荐属性白名单(用于SearchFilter构造)
| 允许属性 | 说明 | 是否支持通配符 |
|---|---|---|
uid |
用户唯一标识 | 否 |
mail |
邮箱地址(需额外校验) | 否 |
cn |
姓名(需UTF-8长度限制) | 否 |
graph TD
A[用户输入username] --> B{白名单正则匹配?}
B -->|否| C[拒绝请求]
B -->|是| D[ldap.EscapeDN]
D --> E[拼接安全DN]
E --> F[执行Bind/Search]
2.5 NoSQL注入在MongoDB Go Driver中的BSON查询构造误区与结构化查询约束机制
常见误用:字符串拼接构造 $where 或 map[string]interface{}
// ❌ 危险:用户输入直接拼入BSON字段名或值
username := r.URL.Query().Get("user")
filter := bson.M{"name": username} // 若 username = "admin'; drop db--" 无影响,但若用于字段名则危险
此写法虽不触发传统 $where 注入(Go Driver 已禁用 $where),但若动态构造键名(如 bson.M{username: "1"}),攻击者可注入 $ne、$regex 等操作符,绕过身份校验。
安全实践:显式白名单 + bson.D 结构化约束
| 约束类型 | 推荐方式 | 说明 |
|---|---|---|
| 字段名 | 预定义常量或映射白名单 | 禁止用户控制键名 |
| 操作符 | 固定使用 bson.E{Key, Value} |
bson.D{{"email", email}} |
| 正则匹配 | 使用 primitive.Regex |
自动转义,避免 .* 注入 |
查询构造流程
graph TD
A[用户输入] --> B{是否为预设字段?}
B -->|否| C[拒绝请求]
B -->|是| D[转为 bson.D 元素]
D --> E[经 primitive.Regex 封装值]
E --> F[执行 FindOne]
正确示例:带校验的邮箱查询
// ✅ 安全:字段名固化,值经 Regex 封装
email := r.URL.Query().Get("email")
if !isValidEmail(email) { /* 拒绝非法格式 */ }
filter := bson.D{{"email", primitive.Regex{Pattern: "^" + regexp.QuoteMeta(email) + "$", Options: ""}}}
primitive.Regex 强制模式转义,bson.D 保证键序与结构不可篡改,从根本上阻断操作符注入路径。
第三章:认证与会话管理缺陷的Go生态落地风险
3.1 JWT签名绕过在github.com/golang-jwt/jwt/v5中的算法混淆漏洞复现与强算法锁定方案
漏洞成因:alg头字段未强制校验
当客户端提交 {"alg":"none"} 或 {"alg":"HS256"} 但服务端未显式指定预期算法时,jwt.Parse() 可能回退至弱默认行为。
复现关键代码
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.xxxxx" // 签名被篡改
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
return []byte("secret"), nil // ❌ 未校验 t.Method.Alg()
})
此处未验证
t.Header["alg"]是否匹配预期(如仅允许HS256),攻击者可构造alg: "none"无签名令牌或降级为弱密钥算法。
强算法锁定方案
- ✅ 使用
jwt.WithValidMethods([]string{"HS256"})选项 - ✅ 在 keyfunc 中显式拒绝非预期算法:
keyFunc := func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("invalid signing method: %v", t.Method.Alg())
}
return []byte("secret"), nil
}
| 风险项 | 安全加固方式 |
|---|---|
alg 混淆 |
WithValidMethods 限定 |
| 密钥重用 | 每算法独立密钥池 |
| 无签名令牌 | ParseWithClaims + 显式校验 |
graph TD
A[JWT Header alg] --> B{alg in ValidMethods?}
B -->|Yes| C[继续解析]
B -->|No| D[Reject Token]
3.2 Session固定与泄露在gorilla/sessions中间件中的Cookie配置失当与Secure+HttpOnly+SameSite强化模板
默认Cookie配置的风险本质
gorilla/sessions 默认启用 httpOnly: false、secure: false、sameSite: http.SameSiteDefaultMode,极易引发会话劫持与CSRF。
关键安全参数对照表
| 参数 | 危险默认值 | 推荐生产值 | 含义 |
|---|---|---|---|
HttpOnly |
false |
true |
阻止JS访问Cookie,防御XSS窃取 |
Secure |
false |
true |
强制HTTPS传输,防止明文截获 |
SameSite |
SameSiteDefaultMode |
SameSiteStrictMode |
阻断跨站请求携带Session ID |
强化初始化示例
store := sessions.NewCookieStore([]byte("secret-key"))
store.Options = &sessions.Options{
HttpOnly: true, // ✅ 禁止document.cookie读取
Secure: true, // ✅ 仅HTTPS发送(开发环境需配合localhost例外)
SameSite: http.SameSiteStrictMode, // ✅ 防CSRF核心防线
Path: "/", // 避免子路径泄漏
}
Secure: true在HTTP环境将导致Cookie不发送——需配合反向代理或开发时临时设为false并启用SameSiteLaxMode。
SameSiteStrictMode可能影响OAuth跳转体验,高交互场景建议Lax,但必须搭配Secure+HttpOnly。
3.3 密码哈希实现中bcrypt成本因子硬编码与time.Now()依赖导致的侧信道泄漏防护模式
成本因子硬编码的风险
硬编码 cost = 10 会导致哈希计算时间恒定,丧失自适应性,且在硬件升级后无法抵御暴力破解:
// ❌ 危险:固定成本因子,易被时序分析利用
hash, _ := bcrypt.GenerateFromPassword([]byte("pwd"), 10)
10 表示 2¹⁰ = 1024 轮迭代;但若所有用户统一使用该值,攻击者可通过测量响应延迟反推密码长度分布(因输入长度影响内存访问模式)。
time.Now() 引入的时序噪声
依赖系统时钟会暴露执行路径差异:
// ❌ 隐蔽时序泄漏点
start := time.Now()
hash, _ := bcrypt.GenerateFromPassword(pwd, cost)
log.Printf("hash took %v", time.Since(start)) // 日志精度暴露内部分支
高精度日志使攻击者能区分 bcrypt 的 salt 生成、轮次调度等子阶段耗时。
防护模式对比
| 方案 | 时序稳定性 | 成本可调性 | 实现复杂度 |
|---|---|---|---|
| 硬编码 cost + time.Now() | 差 | 否 | 低 |
| 动态 cost + 恒定时间日志 | 优 | 是 | 中 |
| 基于硬件性能自动调优 | 优 | 是 | 高 |
推荐实践
- 使用
golang.org/x/crypto/bcrypt的DefaultCost并按部署环境动态配置; - 所有日志打点统一采样周期(如
time.Now().UnixMilli()截断至秒级); - 对哈希操作整体封装为恒定时间边界函数(含填充等待)。
第四章:数据安全与配置治理的Go原生陷阱
4.1 敏感配置硬编码在struct tag或envconfig解析中的泄露路径与Go 1.22+内置secrets包集成范式
风险根源:Tag与环境变量的隐式暴露
Go 应用常通过 struct tag(如 envconfig:"DB_PASSWORD")或 env 包自动绑定环境变量,但若未校验值来源,os.Getenv("DB_PASSWORD") 可能返回明文——尤其当 .env 文件被意外提交或 CI 日志开启调试输出时。
Go 1.22+ secrets 包的集成范式
import "secrets"
type Config struct {
DBPassword string `envconfig:"DB_PASSWORD" secrets:"true"`
}
func LoadConfig() (*Config, error) {
cfg := new(Config)
if err := envconfig.Process("", cfg); err != nil {
return nil, err
}
// secrets 包自动拦截并安全解密该字段(需提前注册 provider)
return cfg, nil
}
逻辑分析:
secrets:"true"触发secrets包的钩子,在envconfig.Process后对字段执行secrets.Get();参数secrets.Get()默认使用secrets.Provider注册的解密器(如 AWS Secrets Manager 或本地 KMS),避免明文落地。
安全对比表
| 方式 | 是否内存驻留明文 | 是否支持动态轮换 | 是否需修改业务逻辑 |
|---|---|---|---|
| 原始 envconfig | ✅ | ❌ | ❌ |
| secrets + tag | ❌(仅解密后短暂持有) | ✅ | ✅(仅加 tag) |
graph TD
A[envconfig.Process] --> B{field has secrets:\"true\"?}
B -->|Yes| C[secrets.Get\\n→ Provider.Decrypt]
B -->|No| D[直接赋值]
C --> E[安全注入结构体]
4.2 TLS配置弱默认值(如TLS 1.0/1.1启用、不校验证书)在net/http.Server中的显式禁用与现代cipher suite模板
Go 1.19+ 默认禁用 TLS 1.0/1.1,但旧版本或显式配置仍可能暴露风险。必须主动收紧:
srv := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
},
// 强制证书校验(服务端默认已启用,客户端需显式设置)
},
}
MinVersion显式淘汰不安全协议;CipherSuites限定为AEAD类现代套件,排除CBC模式与RSA密钥交换;MaxVersion防止未来协议降级(如TLS 1.4未定义,实际生效上限为1.3)。
| 套件类型 | 安全属性 | Go常量 |
|---|---|---|
| AES-GCM | AEAD, PFS | TLS_AES_128_GCM_SHA256 |
| ChaCha20 | 移动端友好, PFS | TLS_CHACHA20_POLY1305_SHA256 |
⚠️ 注意:
InsecureSkipVerify: true(常见于测试)必须严格禁止上线。
4.3 日志敏感信息泄露在zap/slog中结构化日志字段的自动脱敏钩子与redact middleware实现
脱敏钩子设计原则
需拦截 zap.Field 或 slog.Attr 构造过程,在序列化前识别并替换敏感键(如 "password"、"token"、"id_card")。
zap 中的自动脱敏 Hook 示例
func RedactHook() zapcore.Hook {
return zapcore.HookFunc(func(entry zapcore.Entry) error {
for i := range entry.Fields {
if isSensitiveKey(entry.Fields[i].Key) {
entry.Fields[i].String = "[REDACTED]"
}
}
return nil
})
}
逻辑分析:该 hook 在日志写入前遍历所有 Field,对匹配敏感键名的字段值强制覆写为 [REDACTED];isSensitiveKey 可基于预定义 map 或正则实现,支持热更新。
slog redact middleware 实现对比
| 方案 | zap Hook | slog Middleware |
|---|---|---|
| 注入时机 | Core.Write 阶段 | Logger.LogAttrs 前 |
| 字段粒度 | Field 级(结构体) | Attr 级(键值对) |
| 扩展性 | 需重写 Core | 组合式封装,更轻量 |
敏感字段识别策略
- 支持通配符匹配(如
"*.auth_token") - 允许按 log level 动态启用(仅
Debug/Info脱敏) - 可结合 context.Value 提供运行时白名单
4.4 HTTP头注入(CRLF)在net/http.Header.Set中未校验换行符的漏洞与安全Header写入封装函数
Go 标准库 net/http.Header.Set 直接拼接字符串,不校验 \r\n 控制字符,攻击者可构造恶意键值触发响应头拆分:
// 危险示例:注入额外响应头
h := http.Header{}
h.Set("X-User", "admin\r\nSet-Cookie: session=pwned; HttpOnly")
// 实际发送:X-User: admin[CR][LF]Set-Cookie: session=pwned; HttpOnly
逻辑分析:
Header.Set内部调用canonicalMIMEHeaderKey后直接h[key] = []string{value},未对value做 CRLF 过滤。参数value若含\r或\n,将破坏 HTTP 消息边界。
安全封装原则
- 拒绝含
\r、\n、\0的 header value - 使用
strings.ContainsAny(value, "\r\n\0")预检
推荐防护方案
| 方案 | 是否拦截CRLF | 性能开销 | 适用场景 |
|---|---|---|---|
header.Set() 原生 |
❌ | 无 | 不安全 |
自定义 SafeSet() |
✅ | 极低 | 生产必备 |
| 中间件全局过滤 | ✅ | 中等 | 多服务统一治理 |
graph TD
A[用户输入value] --> B{ContainsAny<br>\r\n\0?}
B -->|Yes| C[panic/return error]
B -->|No| D[调用原生h.Set]
第五章:构建安全、可审计、可持续演进的Go服务治理体系
安全边界:基于Open Policy Agent的细粒度授权控制
在某金融级支付网关项目中,团队将OPA嵌入Go服务的HTTP中间件层,通过rego策略统一管控API访问权限。例如,对/v1/transactions/{id}/refund端点,策略强制要求:请求者角色必须为admin或risk_officer,且交易状态为settled,同时需携带经JWT验证的x-audit-id头。策略代码以.rego文件形式版本化托管于Git仓库,并通过CI流水线自动注入到服务启动时加载的opa.Server实例中。
可审计性:结构化日志与审计事件溯源链
所有关键业务操作(如用户余额变更、密钥轮换、权限分配)均生成符合RFC 5424标准的结构化日志,并通过zap的Core接口注入唯一trace_id和audit_event_id。审计日志被实时写入专用Elasticsearch集群,字段包含event_type(如user_role_grant)、actor_principal(OIDC subject)、target_resource_id、before_state_hash和after_state_hash。以下为典型审计事件示例:
| 字段 | 值 |
|---|---|
event_type |
api_key_rotation |
actor_principal |
auth0|abc123@corp.example.com |
target_resource_id |
svc-payment-gateway-001 |
timestamp |
2024-06-15T08:22:14.892Z |
可持续演进:语义化版本驱动的服务契约管理
采用go-swagger生成OpenAPI 3.0契约文档,并通过swag init -g cmd/server/main.go自动同步接口定义。每个服务发布前,CI检查新旧OpenAPI规范的兼容性:使用openapi-diff工具比对v1.2.0与v1.3.0,仅允许添加字段、新增路径、扩大枚举值等向后兼容变更。不兼容变更(如删除必填字段)将触发构建失败并推送告警至Slack #api-evolution频道。
治理基础设施:GitOps驱动的策略即代码流水线
治理策略(OPA策略、审计规则、TLS配置模板)全部声明式定义于infra/policies/目录下,由Argo CD监听Git仓库变更。当提交包含policy/authz/payment.rego的PR并合并后,Argo CD自动同步至Kubernetes集群中的opa命名空间,同时触发kubectl rollout restart deployment opa-server。策略生效延迟严格控制在12秒内(P95),并通过Prometheus指标opa_policy_compile_duration_seconds持续监控。
// audit/middleware.go:审计中间件核心逻辑
func AuditMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
eventID := uuid.New().String()
ctx := context.WithValue(r.Context(), auditKey, eventID)
r = r.WithContext(ctx)
// 记录审计事件元数据到日志上下文
logger := zap.L().With(
zap.String("audit_event_id", eventID),
zap.String("http_method", r.Method),
zap.String("path", r.URL.Path),
)
// 执行原handler
next.ServeHTTP(w, r)
})
}
密钥生命周期自动化:HashiCorp Vault集成实践
服务启动时通过Vault Agent注入动态数据库凭证,凭证TTL设为4小时,自动续期。密钥轮换流程完全自动化:Vault策略限制payment-db-creds路径仅允许read权限;Go服务调用vaultClient.Logical().Read("database/creds/payment-db-creds")获取短期凭证,并在连接池空闲时主动刷新。凭证变更事件通过Vault Event Broker推送到Kafka主题,触发下游审计系统生成credential_rotation事件。
graph LR
A[Go Service Startup] --> B[Vault Agent Sidecar]
B --> C{Fetch DB Credential}
C --> D[Inject into Connection Pool]
D --> E[Periodic Renewal via Vault API]
E --> F[On Expiry: Auto-Rotate & Log Audit Event]
F --> G[Send to Kafka audit.topic]
G --> H[Elasticsearch Indexing] 