Files
LokiStack/README.md
T
opsx d0b9b6b71d fix(ui): 前端用户可见文案 opsx-loki → lokistack
修正用户能看到/会复制走的旧名(你反馈的 K8s 示例 token 命令在内):

1. 界面标题(4 处)
   App.tsx            浏览器标题栏
   Layout.tsx         侧边栏 logo / 页面标题 fallback / 移动端导航
   login/index.tsx    登录页大标题
   settings/index.tsx 「关于 lokistack」

2. 集群接入示例(用户直接复制到集群执行,5 处)
   ClusterFormDialog.tsx:
     Role / ServiceAccount 名: opsx-loki       → lokistack
     token Secret 名:         opsx-loki-token  → lokistack-token
     取 token 命令:           get secret opsx-loki-token → lokistack-token

3. 文档
   docs/deployment/README.md, host-nginx.md  旧名全量修正
   README.md 迁移章节之外的示例命令修正;
             迁移章节(从 opsx-loki 升级)保留旧名作历史对照

【刻意不改】
  a) localStorage key(9 个)
     opsxloki.auth.{token,refresh-token,user}
     opsxloki.theme.mode / layout.collapsed / chunk-reloaded
     opsxloki.logs.{query-history,range-history}
     原因:改了所有在线用户立即掉登录,主题/折叠/查询历史全丢。
     要改必须配套迁移(参考 main.tsx 中 lokideploy.* → opsxloki.* 的写法)。

  b) 数据文件名 opsxloki.json(后端硬编码,改了读不到 DB)

  c) Go module 路径 opsxloki/...(159 处 import)
     属编译期内部标识,最终用户不可见;改名需全仓重构 + go.mod 变更。
2026-10-26 17:30:30 +00:00

18 KiB
Raw Blame History

lokistack v2 — 安全的 K8s Loki 部署平台

一个轻量级的 Gin + React + JSON 文件存储 平台,用于通过 SSH 或 kubeconfig 把 Grafana Loki 日志聚合系统部署到 Kubernetes 集群或裸金属主机上。

v2 安全增强:非 root 容器、运行时强制校验密钥、健康检查、资源限制、日志轮转、优雅关停。

面向 AI 编码代理:本仓库的 CLAUDE.md 与 AGENTS.md 记录了架构约定与常用命令,供 Claude Code、 Cursor、Aider 等工具参考。两份文件内容一致,修改其一请同步另一份。

功能特性

  • K8s 集群管理 — 通过 kubeconfig 添加集群,自动校验连通性
  • SSH 主机管理 — 支持密码或密钥认证
  • 存储配置 — 支持 S3、OSS、MinIO、RustFS 等后端
  • 一键部署 Loki — Helm Chart / YAML 清单 / 二进制 三种模式
  • 部署日志 — 实时日志流
  • Web 终端 — 在浏览器中执行远程命令
  • 单体二进制 — 约 13MB,零外部依赖
  • 安全 — Argon2 密码哈希、AES 凭证加密、JWT 鉴权、非 root 容器


从 opsx-loki 升级(资源更名说明)

本版本把所有资源名从 opsx-loki / opsxloki 统一改为 lokistack,包括镜像、Deployment、 Service、PVC、Secret、docker-compose 卷名。

Kubernetes 用户

资源名已变,不能直接 kubectl apply 覆盖(会新建空 PVC)。按下面顺序迁移,零数据丢失:

# 0) 先备份(数据只有几十 KB)
kubectl -n log exec deploy/opsx-loki -- tar czf - -C /app/data . > /tmp/data.tar.gz
kubectl -n log get secret opsxloki-secrets -o yaml > /tmp/secret-old.yaml
kubectl -n log get pvc opsxloki-data -o yaml > /tmp/pvc-old.yaml

# 1) 停旧服务(Recreate + RWO,必须停)
kubectl -n log scale deploy opsx-loki --replicas=0
kubectl -n log wait --for=delete pod -l app=opsx-loki --timeout=120s

# 2) 复制 Secret(值不变,密钥才不会失效)
kubectl -n log get secret opsxloki-secrets -o yaml \
  | sed 's/name: opsxloki-secrets/name: lokistack-secrets/' \
  | kubectl apply -f -

# 3) 只建新 PVC
kubectl apply -f kubernetes/pvc.yaml
kubectl -n log wait --for=jsonpath='{.status.phase}'=Bound pvc/lokistack-data --timeout=180s

# 4) 恢复数据到新 PVC(起临时 Pod 挂载)
kubectl -n log run restore --image=alpine:3.20 --restart=Never \
  --overrides='{"spec":{"containers":[{"name":"r","image":"alpine:3.20","command":["sleep","600"],"volumeMounts":[{"name":"d","mountPath":"/data"}]}],"volumes":[{"name":"d","persistentVolumeClaim":{"claimName":"lokistack-data"}}]}}'
kubectl -n log wait --for=condition=Ready pod/restore --timeout=120s
kubectl -n log cp /tmp/data.tar.gz restore:/tmp/
kubectl -n log exec restore -- sh -c 'cd /data && tar xzf /tmp/data.tar.gz && chown -R 1000:1000 /data'
kubectl -n log delete po restore

# 5) 启动新服务
kubectl apply -f kubernetes/deployment.yaml -f kubernetes/service.yaml -f kubernetes/service-nodeport.yaml
kubectl -n log rollout status deploy/lokistack --timeout=240s

# 6) 若有 Ingress,改后端(TKE qcloud 要 NodePort)
kubectl -n log patch ingress <你的ingress名> --type=json \
  -p='[{"op":"replace","path":"/spec/rules/0/http/paths/0/backend/service/name","value":"lokistack-nodeport"}]'

# 7) 确认服务正常后再删旧资源
kubectl -n log delete deploy opsx-loki
kubectl -n log delete svc opsx-loki opsx-loki-nodeport
# 旧 PVC / Secret 建议保留 7 天做回滚兜底
# kubectl -n log delete pvc opsxloki-data
# kubectl -n log delete secret opsxloki-secrets

Docker Compose 用户

卷名 opsxloki-data → lokistack-data。旧数据不会自动迁移,需要手动拷:

# 停服务
docker compose down

# 创建新卷并迁移数据
docker volume create lokistack-data
docker run --rm -v opsxloki-data:/from -v lokistack-data:/to alpine \
  sh -c 'cp -av /from/. /to/ && chown -R 1000:1000 /to'

# 重新启动(会用新卷)
docker compose up -d

# 验证数据在
docker compose exec lokistack cat /app/data/opsxloki.json | head -c 100

注意:数据文件名仍叫 opsxloki.json(代码里硬编码,不改),只是目录/卷名变了。


快速开始

前置条件

  • Docker Engine 24.0+ 与 Docker Compose v2+
  • 宿主机上装有 kubectl 和 helm(用于向外部集群部署 K8s 资源)
  • openssl(用于生成密钥)

第 1 步:生成密钥(必需)

启动前必须生成所需的安全密钥:

# 必需:JWT 签名密钥(建议 >= 32 字符)
export JWT_SECRET=$(openssl rand -hex 32)

# 必需:用于凭证存储的 AES 加密密钥(必须 >= 32 字节)
export ENCRYPTION_KEY=$(openssl rand -hex 32)

# 可选:跨会话持久化
echo "export JWT_SECRET=$JWT_SECRET" >> ~/.bashrc
echo "export ENCRYPTION_KEY=$ENCRYPTION_KEY" >> ~/.bashrc

警告:如果未设置 JWT_SECRET 或 ENCRYPTION_KEY,容器将 拒绝启动,以避免不安全的默认部署。

第 2 步:用 Docker Compose 启动(推荐)

git clone https://github.com/your/lokistack.git
cd lokistack

# 设置密钥(见第 1 步)
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)

# 启动
docker compose up -d

# 查看健康状态
docker compose ps
docker compose logs -f

# 打开 http://localhost:8080
# 管理员密码见「第 4 步」(首次启动随机生成,不再是固定 admin123)

第 3 步:验证健康状态

# 容器级健康检查(内部)
docker inspect --format='{{.State.Health.Status}}' lokistack

# 期望输出:"healthy"

# 手动检查
curl http://localhost:8080/index.html

第 4 步:获取管理员初始密码

首次启动会生成随机管理员密码(不再固定 admin123),同时:

  • 打印到启动日志(仅首次)
  • 写入数据卷 /app/data/initial-admin-password.txt(权限 0600)

任选一种方式获取(推荐方式 1 —— 日志会轮转丢失):

# 方式 1(推荐):从容器内读文件 —— 日志轮转后依然可查
docker compose exec lokistack cat /app/data/initial-admin-password.txt

# 方式 2:首次启动日志(只打印一次,重启后不会再有)
docker compose logs lokistack | grep -A6 "已生成管理员账号"

# 方式 3:容器已停止时也能用(不要求 running)
docker cp lokistack:/app/data/initial-admin-password.txt ./

# 方式 4:直接读 named volume(宿主机 root)
sudo cat /var/lib/docker/volumes/lokistack_lokistack-data/_data/initial-admin-password.txt

⚠️ 为什么不能只靠日志:docker logs 只在首次启动打印一次;日志轮转或容器重启后, 数据库里已有用户,不会再打印。所以文件才是可靠来源。

💡 容器名说明:本 compose 指定了 container_name: lokistack,所以可直接用 lokistack。 若你的部署没有指定,用服务名形式更通用:docker compose exec lokistack cat ...

Kubernetes 环境(同一个文件):

# 读文件(推荐)
kubectl -n log exec deploy/lokistack -- cat /app/data/initial-admin-password.txt

# 首次启动日志
kubectl -n log logs deploy/lokistack | grep -A6 "已生成管理员账号"

# 拷到本地
kubectl -n log cp <pod-name>:/app/data/initial-admin-password.txt ./

想自己指定密码(跳过随机生成):

# Docker Compose:部署前设置环境变量
export ADMIN_INIT_PASSWORD='your-strong-password'
docker compose up -d

# Kubernetes:在 deployment 的 env 中加入 ADMIN_INIT_PASSWORD

安全说明:

项 说明
密码文件权限 0600,属主 lokistack
自动清理 admin 首次修改密码后,该文件自动删除(见 seed.CleanupInitPassword)
日志内容 仅在首次启动打印,且容器日志由运行时轮转
忘了密码 数据卷里文件仍在;若文件已删,用 ADMIN_INIT_PASSWORD 重新初始化(需清空数据卷)
环境变量优先 设置了 ADMIN_INIT_PASSWORD 时不生成随机密码、不写文件

生产部署

方式 A:带密钥文件的 Docker Compose(推荐)

# 创建密钥文件
mkdir -p secrets
openssl rand -hex 32 > secrets/jwt_secret.txt
openssl rand -hex 32 > secrets/encryption_key.txt
chmod 600 secrets/*.txt

# 使用 docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d

方式 B:Docker Swarm

# 初始化 swarm(如果尚未初始化)
docker swarm init

# 创建 secrets
echo "$(openssl rand -hex 32)" | docker secret create lokistack_jwt -
echo "$(openssl rand -hex 32)" | docker secret create lokistack_key -

# 部署
docker stack deploy -c docker-compose.prod.yml lokistack

安全清单

  • JWT_SECRET 至少 32 字符且为密码学随机
  • ENCRYPTION_KEY 至少 32 字节且为密码学随机
  • 密钥存放在 Docker Secrets 或外部保险柜中(而非 env 文件)
  • .env 与 secrets/ 已加入 .gitignore
  • 启用了 no-new-privileges:true(docker-compose 默认启用)
  • kubeconfig 以只读方式挂载(:ro)
  • SSH 密钥以只读方式挂载(:ro)
  • 已合理配置资源限制
  • 健康检查通过
  • 已配置日志轮转

从源码构建

# 构建前端 + 后端
bash build.sh

# 用本地脚本启动(开发)
./run.sh

开发模式(热重载)

# 终端 1 — 后端
cd backend
go run .          # 监听 :8080

# 终端 2 — 前端
cd frontend
npm install
npm run dev       # 打开 http://localhost:3000,API 代理到 localhost:8080

直接 go run 启动后端前,必须先 export JWT_SECRET 与 ENCRYPTION_KEY,否则 backend/config/config.go 会 log.Fatal 拒绝启动。./run.sh 会自动注入开发用默认值。

测试与 lint

cd backend && go test ./...                      # 所有 Go 测试(utils、repository)
cd backend && go test ./utils/ -run TestName -v  # 运行单个测试
cd frontend && npm run lint                      # ESLint

架构

前端(React + TypeScript + Tailwind CSS)  [开发 :3000 / 生产 :8080]
    |
    | HTTP/REST + JWT Bearer Token
    v
后端(Go + Gin)                            [:8080]
    |
    +---> JSON 文件数据库(data/opsxloki.json(兼容加载旧 lokideploy.json))
    +---> K8s CLI(kubectl / helm)
    +---> SSH 客户端(golang.org/x/crypto/ssh)
    +---> Loki 部署器(Helm Chart / YAML / Binary)

后端分层

请求流转:router → middleware(JWT)→ controller → service → repository(JsonDB)

  • config/ — GetConfig() 单例,读取环境变量,密钥缺失或长度不足时直接 log.Fatal。
  • routers/router.go — 所有路由注册在 /api/v1;注册 SPA 静态资源并用 NoRoute 回退到 index.html 支持前端路由。
  • controllers/ — 薄 Gin handler,每资源一个文件;绑定/校验入参、调用 service、用 models.SanitizedJSON(...) 包装返回。
  • services/ — 业务逻辑。部署流水线: DeploymentService.Deploy() → LokiDeployer.Deploy(),按模式(k8s-helm / k8s-yaml / host-binary)分发到 kubectl/helm 或 SSH。另有 K8sClient、ssh、storage、 cluster_info 与较新的 collector(日志采集器) 模块。
  • middleware/jwt.go — JWTAuth()、AdminAuth()。
  • utils/ — response.go(响应封装)、crypto.go(Argon2 + AES-GCM)、jwt.go。
  • seed/ — 数据库为空时写入默认 admin 用户。

数据层 — JSON 文件数据库

持久化是 单个 JSON 文件 $DATA_DIR/opsxloki.json(兼容加载旧 lokideploy.json)(默认 ./data/),没有 SQL 数据库。 repository.JsonDB 是与文件对应的内存结构:

  • 写操作由 Lock()/RLock() 保护,RequestSave() 触发 防抖(500ms)原子写(临时文件 → fsync → rename),由单一串行化的 saveWorker goroutine 执行。常规写入用 RequestSave(), 不要在 worker 可能运行时直接 Save()。
  • 优雅关停顺序很关键:停防抖定时器 → 关闭 worker 并等待 → 最后一次 doSave。顺序颠倒会丢更新。
  • 因此应用 只能单实例运行,不可水平扩展。
  • NextID(table) 为各集合分配单调递增的整数 ID。

注:repository/db.go 是当前在用的实现;models/db.go 是正在迁移、尚未清理的旧副本(仍会编译, 但已无人调用)。新增数据库代码请写到 repository。

响应封装与脱敏

  • 所有 API 响应为 { code, message, data },code == 0 表示成功。
  • 不要直接返回原始 model,必须用 models.SanitizedJSON(...) 包装以剔除密码、kubeconfig、 私钥、token 等敏感字段。

凭证安全

  • 密码用 Argon2 哈希;凭证用 AES-GCM 加密,AES 密钥由 ENCRYPTION_KEY 经 HKDF 派生。
  • ENCRYPTION_KEY 必须 >= 32 字节(早期文档写 16,现在会导致启动失败)。

容器安全模型

+-------------------+     +---------------------+
|   宿主内核        |     |   宿主文件系统      |
|                   |     |  ~/.kube (只读)     |
|  +-------------+  |     |  ~/.ssh (只读)      |
|  | 容器        |  |     |  ./data (读写)      |
|  | (lokistack |  |     +---------------------+
|  |  UID:1000)  |  |
|  | no-new-privs|  |
|  | cap_drop:ALL|  |
|  +-------------+  |
+-------------------+

API 接口

Base URL:/api/v1

方法 接口 鉴权 说明
POST /auth/login 否 登录,获取 JWT token
GET /auth/user 是 当前用户信息
POST /auth/password 是 修改密码
GET/POST /clusters 是 集群增删改查
POST /clusters/:id/validate 是 校验集群连通性
GET/POST /hosts 是 主机增删改查
POST /hosts/:id/execute 是 远程执行命令
GET/POST /storages 是 存储配置增删改查
GET/POST /deployments 是 部署增删改查
POST /deployments/:id/deploy 是 启动部署
POST /deployments/:id/stop 是 停止部署
GET /deployments/:id/logs 是 部署日志
GET /deployments/stats 是 统计信息

健康检查

容器对静态文件路径暴露健康检查:

# 容器级(内部)
wget --spider http://localhost:8080/index.html

# 宿主级
curl http://localhost:8080/index.html

# Docker inspect
docker inspect --format='{{.State.Health}}' lokistack

健康检查参数:

  • 间隔:30 秒
  • 超时:5 秒
  • 启动宽限期:15 秒(允许启动)
  • 重试次数:3

部署模式

模式 目标 说明
k8s-helm K8s 集群 官方 Grafana Helm Chart
k8s-yaml K8s 集群 原生 Kubernetes 清单
host-binary Linux 主机 通过 SSH 部署二进制 + systemd

存储后端

类型 Endpoint 示例
S3 s3.amazonaws.com
OSS oss-cn-hangzhou.aliyuncs.com
MinIO http://minio:9000
RustFS http://rustfs:8080

环境变量

必需

变量 示例 说明
JWT_SECRET $(openssl rand -hex 32) JWT 签名密钥(必需,>= 32 字符)
ENCRYPTION_KEY $(openssl rand -hex 32) AES 凭证加密密钥(必需,>= 32 字节)

可选

变量 默认值 说明
SERVER_PORT 8080 HTTP 服务端口
DATA_DIR /app/data 数据存储目录
KUBECONFIG /home/lokideploy/.kube/config kubeconfig 文件路径
KUBECONFIG_PATH ~/.kube 宿主机上 kubeconfig 的挂载源路径
SSH_KEY_PATH ~/.ssh 宿主机上 SSH 密钥的挂载源路径

Docker Secrets 支持

entrypoint 支持基于文件的 Docker Secrets 注入:

变量 文件变量 说明
JWT_SECRET JWT_SECRET_FILE 含 JWT 密钥的 secret 文件路径
ENCRYPTION_KEY ENCRYPTION_KEY_FILE 含加密密钥的 secret 文件路径

配合 Docker Compose secrets 的示例:

services:
  lokideploy:
    environment:
      JWT_SECRET_FILE: /run/secrets/jwt_secret
      ENCRYPTION_KEY_FILE: /run/secrets/encryption_key
    secrets:
      - jwt_secret
      - encryption_key

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  encryption_key:
    file: ./secrets/encryption_key.txt

安全

鉴权与授权

  • Argon2 密码哈希
  • AES-GCM 凭证加密(密钥由 ENCRYPTION_KEY 派生)
  • JWT token 鉴权
  • RBAC 基于角色的访问控制
  • API 响应经过脱敏(不暴露密码、kubeconfig、密钥)

容器安全

  • 非 root 用户:lokideploy:1000
  • 禁止提权:no-new-privileges:true
  • 丢弃能力:除必需外丢弃所有 capabilities
  • 只读挂载:kubeconfig 与 SSH 密钥只读挂载
  • 资源限制:CPU 与内存受限
  • 日志轮转:防止磁盘写满

密钥管理最佳实践

  1. 绝不 把密钥提交到版本控制
  2. 生产环境使用 Docker Secrets(Swarm/Kubernetes)
  3. 定期 轮换密钥:openssl rand -hex 32
  4. 企业部署可使用密钥保险柜(HashiCorp Vault、AWS Secrets Manager 等)
  5. 设置文件权限:对密钥文件执行 chmod 600

资源配置

默认限制(docker-compose.yml)

资源 上限 预留
CPU 2.0 核 0.25 核
内存 512 MB 64 MB
PIDs 100 -

按规模调整

# 更大规模部署(10+ 集群)
deploy:
  resources:
    limits:
      cpus: '4.0'
      memory: 1G
    reservations:
      cpus: '1.0'
      memory: 256M

故障排查

容器立即退出

# 检查密钥是否已设置
docker compose logs
# 期望错误信息:"Missing required security environment variables"

# 修复:
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
docker compose up -d

健康检查失败

# 查看容器状态
docker inspect --format='{{.State.Health}}' lokistack

# 查看日志
docker compose logs --tail=50

kubeconfig 不可访问

# 确保宿主侧权限正确
chmod 600 ~/.kube/config

# 核对挂载路径
docker exec lokideploy ls -la /home/lokideploy/.kube/

许可证

MIT License


更新日志

v2.0.0

  • 安全:非 root 容器用户(lokideploy:1000)
  • 安全:运行时强制校验 JWT_SECRET/ENCRYPTION_KEY
  • 安全:no-new-privileges 与 capabilities 丢弃
  • 安全:kubectl/helm 的 SHA256 校验和验证
  • 运维:健康检查端点
  • 运维:资源限制与预留
  • 运维:日志轮转(json-file 驱动)
  • 运维:优雅关停(SIGTERM)
  • 运维:Docker Secrets 支持
  • 运维:带 nginx 负载均衡的生产 compose 文件