Kibana:从 Discover 排障到 Space 权限治理
Discover 显示零条,先别把时间范围拉到一个月
Kibana 最容易制造误判的页面不是管理页,而是 Discover。搜索结果为空时,人很自然地扩大时间窗、把 Data View 改成通配符,甚至切到拥有更多权限的账号。这样确实可能“搜到东西”,也可能一次扫过大量历史分片,把别的业务索引和敏感字段带进浏览器。
空结果至少有四层来源:浏览器所在的 Kibana Space 不对,Data View 没覆盖目标索引,时间字段或时区过滤掉了文档,当前 Elasticsearch 角色无权读取目标索引。Kibana feature privilege 又会决定用户能不能进入 Discover、Dashboard 或 Dev Tools。页面可见性和数据权限是两条轴;只查其中一条,排障会在 UI 与集群之间来回猜。
Kibana 也不是一组静态网页。浏览器先访问 Kibana 服务,服务处理会话、Space、Saved Objects 和功能权限,再把搜索请求送到 Elasticsearch。Kibana 自己使用的服务身份负责内部索引和后台任务,最终用户访问业务数据仍受自身角色约束。把 Kibana 服务账号交给人使用,会把这两种责任混在一起。
它必须跟着 Elasticsearch 一起选型和升级
Kibana 属于 Elastic Stack。自建环境中,Elasticsearch 与 Kibana 应固定到同一发行版本;升级顺序是先 Elasticsearch、后 Kibana,并最终保持版本一致。官方明确不支持把多台 Kibana 做跨版本滚动升级:旧实例应先全部停止,再启动升级后的实例。Kibana 启动时还可能执行 Saved Objects 迁移,所以回滚依据不能只是一份旧镜像,必须包含 Kibana feature state 的集群快照。
发行包已经带有受支持的 Node.js。自行换系统 Node.js 不会让安装更“标准”,只会多出一条未经产品支持的运行时变量。插件同样要跟着 Kibana 版本更新;镜像升级而插件仍停在旧版,常见结果是启动阶段直接拒绝加载。
如果后端采用 OpenSearch,控制台应选择 OpenSearch Dashboards。两者都经常监听 5601,也都能显示类似的 Discover 和 Dashboard,但配置、插件、权限对象与 Saved Objects 不构成互换契约。
先在回环地址上证明服务链路
下面的容器只适合一次性本地实验。它关闭 Elasticsearch 安全能力,并把端口绑定到回环地址;不要把这段配置放到共享开发机、办公网服务器或云主机。这里使用与 Elasticsearch 主文一致的 9.5.2;换版时两端必须使用同一精确版本,并先核对官方 Kibana Docker 安装说明,不能漂移到 latest。在装有 Docker Engine 的 Linux 主机,以可管理本机 Docker 的操作者执行;先确认本页专用网络、容器名和两个回环端口未被已有实验占用。若任一对象已存在,停下核对,不加入或删除来历不明的同名资源。
set -euo pipefail
for NAME in elasticsearch-kibana-lab kibana-lab; do
if docker container inspect "$NAME" >/dev/null 2>&1; then
printf '容器 %s 已存在,停止本次实验\n' "$NAME" >&2
exit 1
fi
done
if docker network inspect kibana-lab >/dev/null 2>&1; then
printf '网络 kibana-lab 已存在,停止本次实验\n' >&2
exit 1
fi
for PORT in 19200 15601; do
test -z "$(ss -H -lnt "( sport = :$PORT )")" || {
printf '宿主端口 %s 已被占用\n' "$PORT" >&2
exit 1
}
done
export STACK_VERSION=9.5.2
docker network create --label article-lab=kibana-lab kibana-lab
docker run -d --name elasticsearch-kibana-lab \
--label article-lab=kibana-lab \
--network kibana-lab \
-p 127.0.0.1:19200:9200 \
-e discovery.type=single-node \
-e xpack.security.enabled=false \
-e ES_JAVA_OPTS="-Xms1g -Xmx1g" \
docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
docker run -d --name kibana-lab \
--label article-lab=kibana-lab \
--network kibana-lab \
-p 127.0.0.1:15601:5601 \
-e ELASTICSEARCH_HOSTS=http://elasticsearch-kibana-lab:9200 \
docker.elastic.co/kibana/kibana:${STACK_VERSION}容器显示 Up 只能说明进程还在。两个服务初始化可能持续一段时间;以下回环请求对连接拒绝及暂时的服务端错误作有界重试,超过次数后保留容器和日志,不重跑同名启动块。先核对 Elasticsearch 的产品身份,再读 Kibana 日志中的就绪状态,最后才打开浏览器。
curl -q --noproxy '*' --fail-with-body --retry 30 --retry-connrefused \
--retry-delay 2 -sS http://127.0.0.1:19200/
docker logs --tail 120 kibana-lab
curl -q --noproxy '*' --fail-with-body --retry 30 --retry-connrefused \
--retry-delay 2 -sSI http://127.0.0.1:15601/后端根响应应包含 Elasticsearch 版本,Kibana 日志不应出现版本不兼容、插件失败或反复迁移,浏览器入口应返回 HTTP 响应。内存不足时 Elasticsearch 可能先退出;这时保留两端日志,比连续重启更有价值。
kibana.yml 同时描述网络、服务身份和密钥责任
生产配置至少要回答:进程监听在哪里,用户看到哪个公共地址,Kibana 连接哪些 Elasticsearch 节点,以什么服务身份连接,信任哪条 CA 链,多个 Kibana 实例如何解密同一批会话和 Saved Objects。
server.host: "127.0.0.1"
server.port: 5601
server.publicBaseUrl: "https://kibana.example.test"
elasticsearch.hosts:
- "https://es-coordinator-a.example.test:9200"
- "https://es-coordinator-b.example.test:9200"
elasticsearch.serviceAccountToken: "${KIBANA_SERVICE_ACCOUNT_TOKEN}"
elasticsearch.ssl.certificateAuthorities:
- "/etc/kibana/certs/elastic-ca.pem"
elasticsearch.ssl.verificationMode: full
xpack.security.encryptionKey: "${KIBANA_SECURITY_KEY}"
xpack.encryptedSavedObjects.encryptionKey: "${KIBANA_SAVED_OBJECTS_KEY}"
xpack.reporting.encryptionKey: "${KIBANA_REPORTING_KEY}"server.host 是进程监听面,server.publicBaseUrl 是反向代理之后的外部身份。后者配错时,登录回调、Cookie 和分享链接会指向错误协议或内网地址。elasticsearch.hosts 则是服务到集群的内部路径,不是浏览器入口。
verificationMode: full 同时检查 CA 和主机名。证书报错应修正域名、SNI、证书 SAN 或 CA 挂载,不能把校验关掉。三个 encryption key 也不是随容器启动生成的临时值:多实例必须共享稳定的密钥,重建后仍要能解密既有会话、连接器和报表任务。密钥只通过 Secret 管理系统注入,配置仓库保留变量引用和轮换责任人。
用三条样本把 API、Data View 和 Discover 对齐
先在隔离实验索引写入不含真实业务信息的样本。时间在执行时生成,避免固定示例早已落出 Discover 默认窗口。
NOW="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
curl -q --noproxy '*' --fail-with-body -sS -X PUT 'http://127.0.0.1:19200/kibana-lab-logs' \
-H 'Content-Type: application/json' \
-d '{
"mappings": {"properties": {
"@timestamp": {"type": "date"},
"service": {"type": "keyword"},
"status": {"type": "keyword"},
"trace_id": {"type": "keyword"},
"message": {"type": "text"}
}}
}'
curl -q --noproxy '*' --fail-with-body -sS -X POST 'http://127.0.0.1:19200/_bulk?refresh=wait_for' \
-H 'Content-Type: application/x-ndjson' \
--data-binary "{\"index\":{\"_index\":\"kibana-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"checkout\",\"status\":\"ERROR\",\"trace_id\":\"trace-a\",\"message\":\"payment timeout\"}
{\"index\":{\"_index\":\"kibana-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"checkout\",\"status\":\"OK\",\"trace_id\":\"trace-b\",\"message\":\"request complete\"}
{\"index\":{\"_index\":\"kibana-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"catalog\",\"status\":\"OK\",\"trace_id\":\"trace-c\",\"message\":\"request complete\"}
"Bulk 响应中的 errors 应为 false。进入 Dev Tools,先做一条范围明确、字段受控的查询:
GET kibana-lab-logs/_search
{
"size": 10,
"timeout": "5s",
"track_total_hits": 1000,
"_source": ["@timestamp", "service", "status", "trace_id"],
"sort": [{"@timestamp": "asc"}],
"query": {
"bool": {"filter": [
{"term": {"service": "checkout"}},
{"range": {"@timestamp": {"gte": "now-5m", "lte": "now"}}}
]}
}
}这里应得到两条文档,而且 _source 不包含 message。size 只限制返回数量,不限制底层扫描;timeout 也不是容量治理。响应若出现 timed_out、分片失败或总命中关系只是下界,就不能把当前数字当作完整结论。
随后在当前 Space 创建仅覆盖 kibana-lab-logs 的 Data View,时间字段选择 @timestamp。Discover 使用相同时间窗和 service: checkout 条件,也应得到两条。Dev Tools 有结果、Discover 没结果时,采集链路已经不是首要怀疑对象;应核对当前 Space、Data View、时间字段、浏览器时区、KQL 过滤器与用户角色。
Space 隔离对象,角色约束功能和数据
Kibana Space 为 Dashboard、Visualization、Data View 等 Saved Objects 提供命名和访问边界。隐藏某项功能只改变当前 Space 的展示,不构成安全授权。真正的访问结果来自 Kibana privilege 与 Elasticsearch privilege 的并集。
| 控制面 | 管什么 | 常见误判 |
|---|---|---|
| Space 与功能可见性 | 当前空间展示哪些应用和对象 | 菜单隐藏就等于用户无权调用 API |
| Kibana feature privilege | Discover、Dashboard、Dev Tools 等功能的 read 或 all | 给了 Discover read 就自动能读业务索引 |
| Elasticsearch index privilege | 哪些索引可 read、是否可查看元数据 | 有索引 read 就能进入任何 Kibana Space |
| 文档与字段级安全 | 返回哪些文档和字段 | 受限聚合可以代表全局计数 |
生产排障角色通常只需要业务索引别名上的 read 与 view_index_metadata,再在指定 Space 授予 Discover、Dashboard 的读取能力。角色权限是累加的;另一个角色若对 All Spaces 授予了更高权限,不能靠当前 Space 的较低配置把它减回去。
用待验证身份在 Dev Tools 查询当前有效权限,先检查所有角色叠加后的结果,不向业务索引发送写入或删除请求:
GET /_security/user/_privileges对于这里的专用排障账号,逐项核对 indices 中的资源名称及 privileges:资源必须属于批准的业务索引或别名,索引权限只允许 read、view_index_metadata;出现其他权限名称或原始 action 通配符就停止使用该账号并交由管理员核对。该账号也不应有集群管理或 run_as 能力;applications 中的 Kibana 权限另按所需 Space 和功能核对。具体返回字段见 Get user privileges。这条白名单用于此排障身份,不能套用到 Kibana 服务身份。
再对实际访问的资源查询常见读写权限:
POST /_security/user/_has_privileges
{
"index": [{
"names": ["kibana-lab-logs"],
"privileges": ["read", "view_index_metadata", "write", "index", "create", "create_doc", "delete", "auto_configure", "create_index", "delete_index", "manage"]
}]
}在响应的 index.kibana-lab-logs 下,read 与 view_index_metadata 应为 true,其余请求项逐项为 false。顶层 has_all_requested=false 只表示至少一项未授予,不能作为通过条件。这组探针覆盖常见写入路径,最终仍须通过前面的有效权限白名单与资源审核。针对每个实际索引和别名重复检查,API 行为见 Check user privileges。
create_doc 是容易漏掉的窄写权限:拥有它的账号即使 write=false、index=false,仍可通过 _create 新增文档;create 还可以经 index API 覆盖文档。因此,在独立测试索引上给只读账号叠加 create_doc 后,上述白名单应拒绝该身份,具体权限检查应显示 create_doc=true。若做实际 _create 对照,只能针对合成测试数据;不能拿业务索引作破坏性探针。权限随发行版扩展,新增名称先按 Elasticsearch privileges 核对用途,不自动加入白名单。
401、403 和空页面不是同一种故障
401 更像会话或身份链失败:Cookie 过期、反向代理未传递认证信息、OIDC/SAML 回调地址错误,或者 Kibana 服务身份无法访问内部索引。浏览器 Network 与 Kibana 服务日志应放在一起看。
403 表示身份通常已被识别,但当前动作未获授权。此时错误里的 action 名比“页面打不开”更有用:读取业务索引、查询字段能力、保存对象、执行集群管理分别对应不同 privilege。直接换管理员账号虽然能让页面恢复,也会抹掉最关键的拒绝证据。
菜单消失则优先检查 Space feature visibility 和 Kibana feature privilege。页面能打开但只显示部分数据,应进一步核对文档级、字段级安全与 Data View 通配符。受限角色看到的聚合值只是它有权看到的集合,不应被当作全局业务指标。
Saved Objects 是可迁移资产,也是敏感数据
Kibana 可以在 Space 之间复制 Saved Objects,也可以导出为 NDJSON。Dashboard 的引用对象通常会被一起处理,这让迁移方便,也让文件包含更多内部信息:索引名、查询、字段、内部 URL 和对象关系都可能出现在其中。
导入兼容方向是同版本、同一 major 的更新 minor,或下一 major;不能把新版导出的对象反向灌回旧版。升级前应同时保留 NDJSON 级资产备份与包含 Kibana feature state 的 Elasticsearch snapshot。前者适合审查和选择性迁移,后者才承担系统状态恢复。
导入大小受 savedObjects.maxImportExportSize 和 savedObjects.maxImportPayloadBytes 约束。遇到超限时,先拆分对象归属和引用,不要把阈值无限抬高。跨 Space 复制还可能处理 ID 冲突与引用重写,导入后要逐个打开关键 Dashboard,确认 Data View、链接与权限仍指向预期对象。
截图、CSV、Console 历史都已经离开集群权限系统
Discover 隐藏一列,只改变当前表格。展开文档、导出 CSV 或再次调用 API,原字段仍可能出现。生产 Data View 应默认选择必要字段,高敏字段优先在采集或索引侧脱敏,再用字段级权限限制读取。
截图要检查页面顶栏、域名、用户名、过滤器、Trace ID 和展开行;CSV 要有字段白名单、最大时间窗、行数上限、受控落点和销毁责任。Console 导出的请求文本同样敏感,因为它可能保存真实索引名、个人数据和能够再次执行的破坏性方法。
为了把一次人工查询串回代理和 Elasticsearch 日志,可以为请求附加不含个人信息的 X-Opaque-Id。审计记录用户、动作、目标索引、响应状态和 opaque ID 即可,不应把完整结果集复制进 Kibana 或代理日志。
容量问题通常在 5601 后面
浏览器的一次搜索会伴随 Data View、字段能力、Saved Objects、自动补全和主查询。把时间窗从小时级拉到长周期,代价会落到 Elasticsearch 分片 fan-out、搜索线程池、聚合内存、网络传输和浏览器渲染。size: 0 只是不返回文档,并不等于没有扫描。
Kibana 侧观察 Node.js 堆、事件循环利用率、请求延迟和错误率;Elasticsearch 侧观察 search thread pool、rejected、task、慢日志、断路器、分片数和磁盘水位。增加 Kibana 副本只能缓解 Web 层压力,不能降低后端搜索成本。默认时间窗、索引别名、并发限制、异步搜索和服务端资源保护才是查询风暴的控制点。
多实例还要求相同版本、插件、配置与 encryption key。反向代理需要处理上传大小、空闲超时和长连接。浏览器超时后,后端 task 可能仍在运行,因此取消与资源释放也要通过 Elasticsearch task 和容量指标复核。
退出实验和升级失败都要有可验证终点
本地实验先核对两个容器的实验标签、网络模式和实际宿主端口,再核对网络标签。任一结果不符时不得对 19200 发 DELETE,也不得删除同名容器或网络。确认这些仍是本页合成资源后,输入完整网络名;索引存在性 API 对 HEAD /{index} 返回 200 才删除,不存在则跳过,其他状态或传输失败必须停止。删除响应应显示 acknowledged: true,随后才移除容器和网络:
set -euo pipefail
test "$(docker inspect -f '{{index .Config.Labels "article-lab"}}' elasticsearch-kibana-lab)" = kibana-lab || exit 1
test "$(docker inspect -f '{{index .Config.Labels "article-lab"}}' kibana-lab)" = kibana-lab || exit 1
test "$(docker inspect -f '{{.HostConfig.NetworkMode}}' elasticsearch-kibana-lab)" = kibana-lab || exit 1
test "$(docker inspect -f '{{.HostConfig.NetworkMode}}' kibana-lab)" = kibana-lab || exit 1
test "$(docker network inspect -f '{{index .Labels "article-lab"}}' kibana-lab)" = kibana-lab || exit 1
test "$(docker port elasticsearch-kibana-lab 9200/tcp)" = 127.0.0.1:19200 || exit 1
test "$(docker port kibana-lab 5601/tcp)" = 127.0.0.1:15601 || exit 1
read -r -p '确认退出合成实验,请输入完整网络名 kibana-lab: ' CONFIRM_NETWORK
test "$CONFIRM_NETWORK" = kibana-lab || exit 1
INDEX_HTTP="$(curl -q --noproxy '*' -sSI -o /dev/null -w '%{http_code}' \
'http://127.0.0.1:19200/kibana-lab-logs')"
case "$INDEX_HTTP" in
200) curl -q --noproxy '*' --fail-with-body -sS -X DELETE \
'http://127.0.0.1:19200/kibana-lab-logs' ;;
404) printf '实验索引不存在,跳过索引删除\n' ;;
*) printf '索引检查返回 %s,停止清理\n' "$INDEX_HTTP" >&2; exit 1 ;;
esac
docker rm -f kibana-lab elasticsearch-kibana-lab
docker network rm kibana-lab
unset STACK_VERSION NOW这些命令只针对本页创建的资源。若额外挂载了 volume,先核对 volume 名称和标签,再决定是否删除;不要在共享机器上执行全局 prune。
生产回滚不同。应恢复与 Elasticsearch 配套的 Kibana 版本、同版插件、上一版配置和原 encryption key 引用,并使用升级前 snapshot 恢复 Kibana feature state。已经泄露到 shell 历史、容器 inspect、日志或工单的 service token 与 OIDC secret 必须轮换,删除容器并不能撤回秘密。
