# 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)。按下面顺序迁移,**零数据丢失**: ```bash # 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`。**旧数据不会自动迁移**,需要手动拷: ```bash # 停服务 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 步:生成密钥(必需) 启动前必须生成所需的安全密钥: ```bash # 必需: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 启动(推荐) ```bash 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 步:验证健康状态 ```bash # 容器级健康检查(内部) docker inspect --format='{{.State.Health.Status}}' lokistack # 期望输出:"healthy" # 手动检查 curl http://localhost:8080/index.html ``` ### 第 4 步:获取管理员初始密码 首次启动会生成**随机管理员密码**(不再固定 `admin123`),同时: - 打印到启动日志(**仅首次**) - 写入数据卷 `/app/data/initial-admin-password.txt`(权限 `0600`) **任选一种方式获取**(推荐方式 1 —— 日志会轮转丢失): ```bash # 方式 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 环境(同一个文件)**: ```bash # 读文件(推荐) 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 :/app/data/initial-admin-password.txt ./ ``` **想自己指定密码(跳过随机生成)**: ```bash # 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(推荐) ```bash # 创建密钥文件 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 ```bash # 初始化 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 # 构建前端 + 后端 bash build.sh # 用本地脚本启动(开发) ./run.sh ``` ### 开发模式(热重载) ```bash # 终端 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 ```bash 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` | 是 | 统计信息 | ### 健康检查 容器对静态文件路径暴露健康检查: ```bash # 容器级(内部) 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 的示例: ```yaml 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 | - | ### 按规模调整 ```yaml # 更大规模部署(10+ 集群) deploy: resources: limits: cpus: '4.0' memory: 1G reservations: cpus: '1.0' memory: 256M ``` --- ## 故障排查 ### 容器立即退出 ```bash # 检查密钥是否已设置 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 ``` ### 健康检查失败 ```bash # 查看容器状态 docker inspect --format='{{.State.Health}}' lokistack # 查看日志 docker compose logs --tail=50 ``` ### kubeconfig 不可访问 ```bash # 确保宿主侧权限正确 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 文件