OpenSearch Dashboards:Tenant、查询工作面与最小权限实践
同一用户看见两套 Dashboard,往往不是缓存问题
OpenSearch Dashboards 开启多租户后,Global、Private 和 Custom Tenant 各自保存一套 index pattern、visualization 与 dashboard。用户在 Global Tenant 建好图表,切到 Private Tenant 后看到空列表;刷新、清缓存甚至重建浏览器配置都不会让对象出现,因为它们本来就在不同的 Saved Objects 空间里。
更麻烦的是,Tenant 只管控制台对象,不自动收窄业务索引权限。一个用户看不到某张 Dashboard,仍可能通过 Dev Tools 读取索引;另一个用户能打开共享图表,也可能因为缺少 index permission 得到 403。排障时必须把三件事分开:登录身份由谁认证,当前 Tenant 或 Workspace 允许操作哪些对象,OpenSearch role 允许访问哪些 cluster action 与 index pattern。
OpenSearch Dashboards 是独立运行的 Web 服务。它使用专门的服务用户维护内部索引和会话,用户请求则通过 Security 插件映射到 backend role、tenant permission 和 index permission。预定义的 kibana_server 角色只属于 Dashboards 服务身份,官方明确不应把它分配给人类用户;名字里仍有 Kibana,是兼容历史,不代表它可以连接 Elastic Stack。
版本、插件和后端是一套发布单元
OpenSearch Dashboards 应与 OpenSearch 及已安装插件保持相同的 major、minor、patch。Docker tag、离线包、Helm chart 和插件清单都要落到同一份发布记录中。只升级 Dashboards 镜像而不校准后端与插件,常见结果不是某个图表失效,而是服务在启动阶段报告版本不兼容。
官方提供 Docker、tarball、RPM、Debian、Helm 与 Windows 等安装入口,发行包已经包含 Node.js。只有在明确验证兼容范围和企业运行时政策后才替换内置 Node.js;日常部署优先使用发行包自带运行时,减少升级时的组合数量。
OpenSearch Dashboards 不是 Kibana 的换皮版本。两者虽然都常用 5601、都有 Discover 和 Dev Tools,也共享一部分历史术语,但 Space/Tenant、Security 插件、配置键、插件 API、对象迁移和发布节奏已经分开。后端是 Elasticsearch 时使用 Kibana;后端是 OpenSearch 时,才由本文这条工具链接管。
用隔离容器看清最短调用链
为了让后面的只读拒删实验真的由 Security 插件裁决,本机短期实验也保持插件开启。示例仍使用发行包自带的 demo 证书和初始用户,只能放在个人隔离主机、回环端口上;不能用于共享网络、远程服务器或生产。版本固定为与 OpenSearch 主文一致的 3.8.0;官方版本下载页列出了同版镜像。Linux 主机先按官方 Docker 前提检查 Docker、可用内存和 vm.max_map_count。以有本机 Docker 管理权限的身份,在同一 Bash 终端执行以下命令;同名网络、容器或回环端口已被占用时停止,不把本页实验加入已有对象。
set -euo pipefail
for NAME in opensearch-dashboards-lab dashboards-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 dashboards-lab >/dev/null 2>&1; then
printf '网络 dashboards-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 OPENSEARCH_VERSION=3.8.0
export OPENSEARCH_INITIAL_ADMIN_PASSWORD="Aa1$(openssl rand -hex 20)"'!'
docker network create --label article-lab=opensearch-dashboards dashboards-lab
docker run -d --name opensearch-dashboards-lab \
--label article-lab=opensearch-dashboards \
--network dashboards-lab \
-p 127.0.0.1:19200:9200 \
-e discovery.type=single-node \
-e OPENSEARCH_INITIAL_ADMIN_PASSWORD \
-e OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g" \
opensearchproject/opensearch:${OPENSEARCH_VERSION}
docker run -d --name dashboards-lab \
--label article-lab=opensearch-dashboards \
--network dashboards-lab \
-p 127.0.0.1:15601:5601 \
-e 'OPENSEARCH_HOSTS=["https://opensearch-dashboards-lab:9200"]' \
opensearchproject/opensearch-dashboards:${OPENSEARCH_VERSION}初始管理员密码只用于这个短命实验;Docker 管理员仍可从容器元数据读取环境变量,不能换成真实生产密码。openssl 不可用或密码强度检查失败时先查启动日志,不能关闭 Security 插件绕过。验证时不要停在容器状态。下文 os_curl 只对本机 demo 自签名证书使用 -k,并通过 -q --noproxy '*' 排除 curl 配置文件和代理;它把实验密码短时放在本机 curl 进程参数中,不能搬到生产脚本。
os_curl() {
curl -q --noproxy '*' -k --fail-with-body -sS \
-u "admin:${OPENSEARCH_INITIAL_ADMIN_PASSWORD}" "$@"
}
os_curl --retry 30 --retry-connrefused --retry-delay 2 \
https://127.0.0.1:19200/
docker logs --tail 120 dashboards-lab
curl -q --noproxy '*' --fail-with-body --retry 30 --retry-connrefused \
--retry-delay 2 -sSI http://127.0.0.1:15601/根 API 应返回 distribution: opensearch 和 3.8.0;浏览器打开 http://127.0.0.1:15601 后,用本次 admin 密码登录。需要在受控的个人终端查看生成密码时运行 printf '%s\n' "$OPENSEARCH_INITIAL_ADMIN_PASSWORD",不要把输出复制到工单或截图。若日志停在后端连接、插件初始化或 Saved Objects 阶段,先比较两端镜像 tag 与插件集合;Dashboards 官方 Docker 说明列出了安全插件开启时的 HTTPS 后端、kibanaserver 服务身份和 demo 证书验证配置。若 OpenSearch 因内存或 vm.max_map_count 退出,保留退出码和 JVM 日志;反复重启只会让最早的证据滚出日志窗口。OpenSearch 官方 Docker 说明确认 demo 安全配置会安装自签名证书并要求自定义初始管理员密码。
配置文件最危险的不是密码字段,而是身份传播断了
受控部署中的 opensearch_dashboards.yml 可以从下面这组边界开始:
server.host: "127.0.0.1"
server.port: 5601
opensearch.hosts:
- "https://os-coordinator-a.example.test:9200"
- "https://os-coordinator-b.example.test:9200"
opensearch.username: "kibanaserver"
opensearch.password: "${OPENSEARCH_DASHBOARDS_PASSWORD}"
opensearch.ssl.certificateAuthorities:
- "/etc/opensearch-dashboards/certs/root-ca.pem"
opensearch.ssl.verificationMode: full
opensearch.requestHeadersAllowlist:
- authorization
- securitytenant
opensearch_security.multitenancy.enabled: true
opensearch_security.readonly_mode.roles:
- "dashboards_read_only"opensearch.username 是 Dashboards 服务用户,不是日常查询用户。它需要 kibana_server 角色来维护内部对象和系统索引,但不能把这个角色复用给人。登录用户的认证与授权信息还要经过请求头传播到 Security 插件;启用多租户时,authorization 和 securitytenant 都必须在 allowlist 中。漏掉 securitytenant 会让 Tenant 选择失效,官方文档指出 Dashboards 甚至可能进入红色状态。
readonly_mode.roles 主要改变界面表现。它可以隐藏部分写入口,却不能替代 OpenSearch 的 cluster、index 与 tenant permission。真正的生产只读身份应在服务端角色中无法执行写索引、改模板、删除索引和集群管理动作;用户绕过 UI 直接调用 REST API 时也必须被拒绝。
后端证书使用 verificationMode: full,同时验证证书链和主机名。Dashboards 密码、OIDC client secret 和 TLS 私钥由 Secret 管理器挂载,仓库只保存变量引用。把 server.host 设为 0.0.0.0 会扩大监听面,通常应让进程只面对受控反向代理,再由网络策略限制 Dashboards 到协调节点的访问。
先用 Dev Tools 建立基准,再看 Discover
本地实验里写入三条无敏感信息的文档。索引前缀和字段名故意与真实业务分离,防止示例被复制后误指向生产对象。
NOW="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
os_curl -X PUT 'https://127.0.0.1:19200/dashboards-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"}
}}
}'
os_curl -X POST 'https://127.0.0.1:19200/_bulk?refresh=wait_for' \
-H 'Content-Type: application/x-ndjson' \
--data-binary "{\"index\":{\"_index\":\"dashboards-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"checkout\",\"status\":\"ERROR\",\"trace_id\":\"trace-a\",\"message\":\"payment timeout\"}
{\"index\":{\"_index\":\"dashboards-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"checkout\",\"status\":\"OK\",\"trace_id\":\"trace-b\",\"message\":\"request complete\"}
{\"index\":{\"_index\":\"dashboards-lab-logs\"}}
{\"@timestamp\":\"${NOW}\",\"service\":\"catalog\",\"status\":\"OK\",\"trace_id\":\"trace-c\",\"message\":\"request complete\"}
"Bulk 响应的 errors 应为 false。在 Dashboards Dev Tools 中查询 checkout 的两条记录:
GET dashboards-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-1h", "lte": "now"}}}
]}
}
}预期返回两条,响应不包含 message。Dev Tools 直接代理 OpenSearch REST API;size 只收窄结果集,不保证底层少扫分片,timeout 也不能替代服务端资源控制。看到 timed_out、分片失败或非精确总命中时,应保留响应状态再判断,不要只抄屏幕里的数字。
保持管理员登录,在 Global Tenant 创建只覆盖 dashboards-lab-logs 的 index pattern,时间字段使用 @timestamp。Discover 选择最近一小时,并用 DQL 查询 service: checkout,结果也应为两条;如果操作已超过一小时,应改用覆盖写入时 NOW 的绝对时间窗,而不是扩大索引模式。Query Workbench 的 SQL/PPL 与 Discover 的 DQL 或 Lucene 是不同语言;保存查询时必须连同入口和语言一起记录,不能把一段表达式当成跨页面通用语法。
Dev Tools 有数据而 Discover 为空,优先核对当前 Tenant、index pattern、时间字段、浏览器时区和 DQL 过滤器。这样能把 UI 对象错误与采集故障分开,避免用扩大索引通配符来碰运气。
Tenant 权限和索引权限要分别做反证
多租户默认可以提供 Global、Private 与 Custom Tenant。Global 用于有权限用户之间共享对象,Private 只属于当前用户,Custom 则通过角色授予读写。关键 Dashboard 不应只存在某位员工的 Private Tenant;否则人员退出后,团队知道图表曾经存在,却没有稳定的所有权入口。
Security 插件里的角色至少涉及三类能力:cluster permissions 管集群级动作,index permissions 管目标索引和允许的 action,tenant permissions 管 Saved Objects 的读取或写入。Dashboards 的只读 UI 角色只是第四层提示。
| 现象 | 第一检查点 | 不能直接得出的结论 |
|---|---|---|
| Tenant 列表没有目标项 | role mapping 与 tenant permission | 用户不能读取该 Tenant 关联的业务索引 |
Dashboard 可打开但图表 403 | index pattern 与 index permission | Saved Objects 已损坏 |
| Dev Tools 菜单被隐藏 | readonly mode 与功能配置 | REST API 写操作已被服务端禁止 |
| Private Tenant 有图、Global 没有 | 当前 Tenant 与对象归属 | 浏览器缓存需要清理 |
在同一隔离集群创建只覆盖样本索引的实验身份。先以 admin 确认 Security 管理 API 可用;若这里收到 403,应检查 demo 配置中的 plugins.security.restapi.roles_enabled 与管理员映射,不要把实际业务角色临时授予 all_access。下面的 Role API 把集群读取、样本索引读取和 Global Tenant 对象读取分开;kibana_user 预定义角色只负责用户进入 Dashboards 及其内部对象能力,不应给人类账号 kibana_server。
os_curl 'https://127.0.0.1:19200/_plugins/_security/api/roles' >/dev/null这一步必须以 2xx 状态完成;403、401 或连接失败时停下,不执行下面的 PUT。确认后再创建角色与内部用户:
os_curl -X PUT 'https://127.0.0.1:19200/_plugins/_security/api/roles/dashboards_lab_reader' \
-H 'Content-Type: application/json' \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [{
"index_patterns": ["dashboards-lab-logs"],
"allowed_actions": ["read"]
}],
"tenant_permissions": [{
"tenant_patterns": ["global_tenant"],
"allowed_actions": ["kibana_all_read"]
}]
}'角色创建返回 CREATED 或 UPDATED 后,再创建用户;若先一步失败,不要用管理员身份继续模拟只读。
OS_READER_PASSWORD="Aa1$(openssl rand -hex 20)"'!'
os_curl -X PUT 'https://127.0.0.1:19200/_plugins/_security/api/internalusers/dashboards_lab_reader' \
-H 'Content-Type: application/json' \
-d "{\"password\":\"${OS_READER_PASSWORD}\",\"opendistro_security_roles\":[\"kibana_user\",\"dashboards_lab_reader\"]}"用户创建 API 会把提交的实验密码哈希保存;本机 curl -d 的瞬时进程参数仍会出现该合成密码,不能把这种传递方式用于正式账号。Create User API规定了 opendistro_security_roles 必须指向已存在的角色。两次 PUT 应返回 CREATED 或 UPDATED;若失败,先检查响应体和 Security 插件日志,不能继续用管理员身份冒充只读测试。
接着用新身份查询自己的映射及三条样本。下面的 reader_curl 不把代理或 .curlrc 带入实验,-k 仅用于这个本地 demo 证书。authinfo 应显示用户名与角色 kibana_user、dashboards_lab_reader;_search 的总命中应为 3。浏览器从 admin 退出后用此账号登录,选 Global Tenant,已有 index pattern 与两条 checkout 记录应可读;时间窗仍要覆盖样本写入时刻。若无法进入 Discover,先核对角色与 Tenant 映射;不要增加业务索引的写权限来修 UI。
reader_curl() {
curl -q --noproxy '*' -k -sS \
-u "dashboards_lab_reader:${OS_READER_PASSWORD}" "$@"
}
reader_curl --fail-with-body \
'https://127.0.0.1:19200/_plugins/_security/authinfo'
reader_curl --fail-with-body \
'https://127.0.0.1:19200/dashboards-lab-logs/_search?size=0'删除反例只针对这三条合成文档,目标索引必须与角色的 index_patterns 完全一致。用同一只读账号直接调用 REST API,比 Dev Tools 菜单是否出现更能证明服务端拒绝;预期 HTTP 403,响应包含 indices:admin/delete。若返回 200,隔离样本已删除,应立即停止、检查角色并重建样本;401、404 和超时也不算通过。生产角色的类似反证只能在同名空索引的隔离集群中做,不向生产索引发送试探删除。OpenSearch Security permissions解释了动作权限与 REST 方法的关系。
DELETE_RESULT="$(mktemp)"
DELETE_FAILED=0
if DELETE_STATUS="$(reader_curl -o "$DELETE_RESULT" -w '%{http_code}' \
-X DELETE 'https://127.0.0.1:19200/dashboards-lab-logs')" &&
test "$DELETE_STATUS" = 403 &&
grep -Fq 'indices:admin/delete' "$DELETE_RESULT"; then
printf '服务端拒绝样本索引删除\n'
reader_curl --fail-with-body \
'https://127.0.0.1:19200/dashboards-lab-logs/_search?size=0'
else
printf 'HTTP %s\n' "${DELETE_STATUS:-000}"
cat "$DELETE_RESULT"
printf '拒删验收未通过,停止使用该身份\n' >&2
DELETE_FAILED=1
fi
rm -f -- "$DELETE_RESULT"
test "$DELETE_FAILED" -eq 0 || exit 1kibana_user 等历史命名角色也要谨慎评估。它允许登录 Dashboards,并包含对相关内部索引的能力,仍需与业务数据的只读 index permission 组合。kibana_server 则只给服务用户,不能用于人工会话。
Workspace 出现后,别把它与 Tenant 混成同一个词
OpenSearch Dashboards 的 Workspace 提供面向工作区的 Saved Objects 组织与 ACL。Workspace viewer、content producer、administrator 和 dashboard admin 对应不同对象职责,底层 permission mode 又区分 workspace 本身的 read/write 与资产库的 library_read/library_write。
这与 Tenant 的角色授权不是简单改名。团队采用 Workspace 时,应先确认当前发行线、Security 插件和 savedObjects.permission.enabled 配置,再决定 Tenant 与 Workspace 如何并存或迁移。如果未安装 Security 插件且对象权限关闭,用户可能获得过宽的管理能力;不能把“建立了 Workspace”直接当作完成授权。
无论使用 Tenant 还是 Workspace,业务数据仍由 OpenSearch index permission 保护。对象 ACL 解决谁能看和改 Dashboard,索引角色解决 Dashboard 能读到哪些数据。架构评审图中最好画成两条平行链,而不是把 Workspace 包在索引权限外面造成错误安全感。
401、403、红色状态各自指向不同层
浏览器收到 401 时,先检查会话过期、反向代理认证头、OIDC/SAML 回调和 Dashboards 服务身份。若所有用户同时失败,服务日志和后端连接通常比个人浏览器更有区分度。
403 表明身份通常已经映射成功,接下来读取错误体中的 action、index 与 tenant 信息。索引读取、字段能力、保存对象和集群管理是不同权限,不要把它们统一修成 all_access。临时管理员账号能验证页面代码是否正常,却不能作为生产修复。
Dashboards 红色状态常见于后端不可达、版本或插件不配对、服务用户权限不足,以及多租户请求头未放行。把 securitytenant 从 allowlist 漏掉,表面会像 UI 启动故障,根因却在身份传播。一次有效取证至少保留服务启动日志、后端健康、当前配置摘要和角色映射,而不是只截红色状态页。
对象导出、截图和查询历史都要按数据副本处理
Index pattern、visualization、dashboard 与 saved search 可以导出为 NDJSON。文件可能包含内部索引、查询、字段、Data Source 引用和 URL;导入到另一个 Tenant 或 Workspace 时,还要核对插件、目标 Data Source 与冲突处理。它适合迁移控制台资产,不替代包含系统索引与安全状态的 OpenSearch snapshot。
Discover 隐藏字段并不会改变原文档。截图要检查域名、用户名、过滤器、Trace ID 与展开内容,CSV 要限制字段、时间窗、行数和落盘位置。Console 历史与导出的请求文本更危险:里面保存的是可以再次执行的 REST 方法。导入历史前先逐条核对目标集群,删除认证头、真实索引名、个人数据和破坏性请求。
审计日志宜记录用户、backend role、Tenant 或 Workspace、HTTP 方法、目标索引、响应状态和不含个人信息的 X-Opaque-Id。不要为了“证据完整”把整个请求体和结果集复制到应用日志,那会把搜索集群里的敏感数据扩散到第二套日志系统。
搜索慢,不一定是 Dashboards 慢
一次 Discover 刷新通常不只发一条 _search。字段能力、自动补全、Saved Objects、主查询和后台会话可能同时发生。时间窗、index pattern 和分片数量决定 fan-out;聚合基数与返回字段决定内存和网络;浏览器还要承担结果渲染。
Dashboards 侧观察 Node.js 堆、事件循环、请求延迟、状态与插件错误;OpenSearch 侧观察 search thread pool、rejected、task、慢日志、断路器、分片和磁盘水位。增加 Dashboards 副本只能扩展 Web 层,不能抵消一次无界查询。默认时间窗、业务索引别名、并发限制、服务端资源保护与预定义查询才是成本边界。
反向代理超时后,后端查询可能继续运行。排障请求使用 X-Opaque-Id,再到 task 和审计日志核对取消传播与资源释放。团队阈值来自压测和 SLO,示例中的 size 与时间窗口只用于验证链路,不能复制成生产容量结论。
清理本地环境,也清理角色与数据副本
本页实验结束前,核对两个容器及网络都带本次标签,容器确实加入 dashboards-lab,且两个宿主端口仍由它们发布。任一条件不符就停下;不要对一个只是碰巧监听 19200 的服务发送 DELETE。确认输入完整网络名后,用 OpenSearch Index Exists API 的 HEAD 响应区分索引存在与否:200 才删除,404 跳过;其他状态或传输失败不继续清理。删除时预期返回 acknowledged: true,然后才删容器和网络。本次未挂载数据卷,实验角色、用户和样本会随容器一起消失:
set -euo pipefail
test "$(docker inspect -f '{{index .Config.Labels "article-lab"}}' opensearch-dashboards-lab)" = opensearch-dashboards || exit 1
test "$(docker inspect -f '{{index .Config.Labels "article-lab"}}' dashboards-lab)" = opensearch-dashboards || exit 1
test "$(docker inspect -f '{{.HostConfig.NetworkMode}}' opensearch-dashboards-lab)" = dashboards-lab || exit 1
test "$(docker inspect -f '{{.HostConfig.NetworkMode}}' dashboards-lab)" = dashboards-lab || exit 1
test "$(docker network inspect -f '{{index .Labels "article-lab"}}' dashboards-lab)" = opensearch-dashboards || exit 1
test "$(docker port opensearch-dashboards-lab 9200/tcp)" = 127.0.0.1:19200 || exit 1
test "$(docker port dashboards-lab 5601/tcp)" = 127.0.0.1:15601 || exit 1
read -r -p '确认退出合成实验,请输入完整网络名 dashboards-lab: ' CONFIRM_NETWORK
test "$CONFIRM_NETWORK" = dashboards-lab || exit 1
INDEX_HTTP="$(curl -q --noproxy '*' -k -sSI -o /dev/null -w '%{http_code}' \
-u "admin:${OPENSEARCH_INITIAL_ADMIN_PASSWORD}" \
'https://127.0.0.1:19200/dashboards-lab-logs')"
case "$INDEX_HTTP" in
200) os_curl -X DELETE 'https://127.0.0.1:19200/dashboards-lab-logs' ;;
404) printf '实验索引不存在,跳过索引删除\n' ;;
*) printf '索引检查返回 %s,停止清理\n' "$INDEX_HTTP" >&2; exit 1 ;;
esac
docker rm -f dashboards-lab opensearch-dashboards-lab
docker network rm dashboards-lab
unset OPENSEARCH_VERSION OPENSEARCH_INITIAL_ADMIN_PASSWORD OS_READER_PASSWORD NOW
unset -f os_curl reader_curl没有创建的 volume 不需要猜测性清理。若执行时自行增加了挂载,先解析它的确切路径或 volume 名称,确认只属于本实验,再单独处理;不要使用全局 prune。
生产退出还包括撤销临时 role mapping、回收服务密码与 OIDC secret、失效旧会话、转移 Private Tenant 中仍有价值的对象、销毁导出和截图。秘密若进入命令行、容器 inspect、日志或聊天记录,删除容器不能完成回滚,必须轮换凭证并复核审计日志。
权威资料与规范地址
Security Plugin 授权
OpenSearch Security permissions
