From 3c27e58a73a09024dbf19f6ce99d2dc98d6e8cf4 Mon Sep 17 00:00:00 2001 From: opsx Date: Mon, 26 Oct 2026 17:01:09 +0000 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E7=A7=BB=E9=99=A4=20admin123?= =?UTF-8?q?=20=E7=A1=AC=E7=BC=96=E7=A0=81=E9=BB=98=E8=AE=A4=E5=AF=86?= =?UTF-8?q?=E7=A0=81=20+=20=E7=BC=96=E8=AF=91=E5=8A=A0=20-trimpath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安全加固(降低二进制泄露/逆向的信息面): 1. 初始密码随机化 - 首次启动生成 16 位随机密码(大小写+数字,排除 0/O/1/l/I 易混淆字符) - 写入数据卷 initial-admin-password.txt(0600)并打印到首次启动日志 - admin 首次改密后自动删除该文件(seed.CleanupInitPassword) - 支持 ADMIN_INIT_PASSWORD 环境变量指定(自动化部署 / 固定密码场景) - 生成失败直接 Fatal 退出,不再降级到公开默认口令 2. 移除硬编码 admin123 - main.go 不再打印 "Default login: admin / admin123" - 二进制内 admin123 命中:2 → 0 3. -trimpath - build.sh / run.sh 编译加 -trimpath - 二进制内 /app/backend/ 源码路径命中:66 → 0 4. 文档 - README 新增「第 4 步:获取管理员初始密码」 (docker compose exec / logs / docker cp / named volume 四种方式 + K8s 对应命令) - .env.example 补 ADMIN_INIT_PASSWORD - kubernetes/deployment.yaml 加 env 注释说明 验证(strings 对比加固前后二进制): admin123 2 → 0 /app/backend/ 66 → 0 initial-admin-password.txt / ADMIN_INIT_PASSWORD / CleanupInitPassword 均存在 --- .env.example | 6 +++ README.md | 64 ++++++++++++++++++++++++++++++- backend/controllers/auth.go | 5 +++ backend/main.go | 3 +- backend/seed/seed.go | 76 ++++++++++++++++++++++++++++++++++++- build.sh | 2 +- kubernetes/deployment.yaml | 10 +++++ run.sh | 2 +- 8 files changed, 163 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 9b33f39..21d7823 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,12 @@ JWT_SECRET= # Generate with: openssl rand -hex 32 ENCRYPTION_KEY= +# ---- Optional: 首次启动的管理员密码 ---- +# 不设置则自动生成 16 位随机密码,写入数据卷 initial-admin-password.txt 并打印到首次启动日志 +# 设置后跳过随机生成(适合自动化部署 / 需要固定密码的场景) +# 登录后请立即修改;admin 改密成功后自动删除 initial-admin-password.txt +ADMIN_INIT_PASSWORD= + # ---- Optional: Paths ---- # Host path to kubeconfig directory (default: ~/.kube) KUBECONFIG_PATH=~/.kube diff --git a/README.md b/README.md index 65cb18d..423969c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ docker compose ps docker compose logs -f # 打开 http://localhost:8080 -# 默认账号:admin / admin123 +# 管理员密码见「第 4 步」(首次启动随机生成,不再是固定 admin123) ``` ### 第 3 步:验证健康状态 @@ -80,6 +80,68 @@ docker inspect --format='{{.State.Health.Status}}' opsxloki curl http://localhost:8080/index.html ``` +### 第 4 步:获取管理员初始密码 + +首次启动会生成**随机管理员密码**(不再固定 `admin123`),同时: + +- 打印到启动日志(**仅首次**) +- 写入数据卷 `/app/data/initial-admin-password.txt`(权限 `0600`) + +**任选一种方式获取**(推荐方式 1 —— 日志会轮转丢失): + +```bash +# 方式 1(推荐):从容器内读文件 —— 日志轮转后依然可查 +docker compose exec opsxloki cat /app/data/initial-admin-password.txt + +# 方式 2:首次启动日志(只打印一次,重启后不会再有) +docker compose logs opsxloki | grep -A6 "已生成管理员账号" + +# 方式 3:容器已停止时也能用(不要求 running) +docker cp opsxloki:/app/data/initial-admin-password.txt ./ + +# 方式 4:直接读 named volume(宿主机 root) +sudo cat /var/lib/docker/volumes/opsx-loki_opsxloki-data/_data/initial-admin-password.txt +``` + +> ⚠️ **为什么不能只靠日志**:`docker logs` 只在**首次启动**打印一次;日志轮转或容器重启后, +> 数据库里已有用户,**不会再打印**。所以**文件才是可靠来源**。 +> +> 💡 容器名说明:本 compose 指定了 `container_name: opsxloki`,所以可直接用 `opsxloki`。 +> 若你的部署没有指定,用服务名形式更通用:`docker compose exec opsxloki cat ...` + +**Kubernetes 环境(同一个文件)**: + +```bash +# 读文件(推荐) +kubectl -n log exec deploy/opsx-loki -- cat /app/data/initial-admin-password.txt + +# 首次启动日志 +kubectl -n log logs deploy/opsx-loki | 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`,属主 `opsxloki` | +| 自动清理 | **admin 首次修改密码后,该文件自动删除**(见 `seed.CleanupInitPassword`) | +| 日志内容 | 仅在首次启动打印,且容器日志由运行时轮转 | +| 忘了密码 | 数据卷里文件仍在;若文件已删,用 `ADMIN_INIT_PASSWORD` 重新初始化(需清空数据卷) | +| 环境变量优先 | 设置了 `ADMIN_INIT_PASSWORD` 时不生成随机密码、不写文件 | + --- ## 生产部署 diff --git a/backend/controllers/auth.go b/backend/controllers/auth.go index 596760e..2cfa3a5 100644 --- a/backend/controllers/auth.go +++ b/backend/controllers/auth.go @@ -4,6 +4,7 @@ import ( "opsxloki/dto" "opsxloki/models" "opsxloki/pkg/jwt" + "opsxloki/seed" "opsxloki/services" "opsxloki/utils" @@ -97,5 +98,9 @@ func (ctrl *AuthController) ChangePassword(c *gin.Context) { utils.Error(c, 400, err.Error()) return } + // admin 首次改密后删除初始密码明文文件(幂等,非 admin 不触发) + if claims.Username == "admin" { + seed.CleanupInitPassword() + } utils.Success(c, gin.H{"message": "password changed"}) } diff --git a/backend/main.go b/backend/main.go index 318084f..3a2d51a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -54,7 +54,8 @@ func main() { go func() { log.Printf("opsx-loki starting on port %s", cfg.ServerPort) log.Printf("API: http://localhost:%s/api/v1", cfg.ServerPort) - log.Printf("Default login: admin / admin123") + // 初始密码:首次启动日志中的「已生成管理员账号」,或数据卷 initial-admin-password.txt + log.Printf("首次部署请查看启动日志或 %s/initial-admin-password.txt 获取 admin 初始密码", cfg.DataDir) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("Server failed: %v", err) } diff --git a/backend/seed/seed.go b/backend/seed/seed.go index 24979e1..1e4ea9f 100644 --- a/backend/seed/seed.go +++ b/backend/seed/seed.go @@ -1,11 +1,40 @@ package seed import ( + "crypto/rand" + "log" + "os" + "path/filepath" + "opsxloki/models" "opsxloki/pkg/crypto" "opsxloki/repository" ) +// InitPasswordFile 首次启动生成的随机管理员密码,存放于数据卷根目录。 +// 用户名 admin 首次登录并修改密码后,该文件会被自动删除(见 CleanupInitPassword)。 +const InitPasswordFile = "initial-admin-password.txt" + +// randomPassword 生成 n 位随机密码(大小写字母 + 数字,排除 0/O/1/l/I 等易混淆字符) +func randomPassword(n int) (string, error) { + const charset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" + buf := make([]byte, n) + if _, err := rand.Read(buf); err != nil { + return "", err + } + for i := range buf { + buf[i] = charset[int(buf[i])%len(charset)] + } + return string(buf), nil +} + +func dataDir() string { + if d := os.Getenv("DATA_DIR"); d != "" { + return d + } + return "/app/data" +} + func Seed() { db := repository.GetDB() db.RLock() @@ -13,15 +42,60 @@ func Seed() { db.RUnlock() if !hasUsers { - hash, _ := crypto.HashPassword("admin123") + // 优先级:环境变量 ADMIN_INIT_PASSWORD > 随机生成 + pwd := os.Getenv("ADMIN_INIT_PASSWORD") + generated := false + if pwd == "" { + var err error + if pwd, err = randomPassword(16); err != nil { + // 不降级到默认口令:宁可启动失败,也不留公开默认密码 + log.Fatalf("[SEED] 生成随机初始密码失败: %v;请通过 ADMIN_INIT_PASSWORD 环境变量指定初始密码", err) + } + generated = true + } + + hash, _ := crypto.HashPassword(pwd) repository.GetDB().CreateUser(&models.User{ Username: "admin", Password: hash, Nickname: "Administrator", Role: "admin", }) + + if generated { + path := filepath.Join(dataDir(), InitPasswordFile) + if err := os.WriteFile(path, []byte(pwd+"\n"), 0600); err != nil { + log.Printf("[SEED] 写入初始密码文件失败: %v", err) + } + log.Printf("==================================================") + log.Printf(" 首次启动:已生成管理员账号") + log.Printf(" 用户名 : admin") + log.Printf(" 密 码 : %s", pwd) + log.Printf(" 已保存 : %s", path) + log.Printf(" 获取方式:") + log.Printf(" docker compose exec <服务名> cat %s", path) + log.Printf(" kubectl -n exec deploy/opsx-loki -- cat %s", path) + log.Printf(" ⚠️ 首次登录后请立即修改密码(改密后本文件自动删除)") + log.Printf("==================================================") + } else { + log.Printf("[SEED] 管理员初始密码来自环境变量 ADMIN_INIT_PASSWORD") + } } // 内置告警规则:有 managed 数据源(部署过 Loki)时补齐通用模板(默认禁用) SeedAlertRules() } + +// CleanupInitPassword 在 admin 首次修改密码后调用,删除明文密码文件。 +// 幂等:文件不存在时静默返回。 +func CleanupInitPassword() { + path := filepath.Join(dataDir(), InitPasswordFile) + if _, err := os.Stat(path); err != nil { + return + } + if err := os.Remove(path); err != nil { + log.Printf("[SEED] 删除初始密码文件失败: %v", err) + return + } + log.Printf("[SEED] 初始密码文件已删除: %s", path) +} diff --git a/build.sh b/build.sh index 2422b24..7491c75 100755 --- a/build.sh +++ b/build.sh @@ -102,7 +102,7 @@ export GOPROXY=${GOPROXY:-$GOPROXY_DEFAULT} BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') GIT_COMMIT=$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") -$GO_CMD build \ +$GO_CMD build -trimpath \ -ldflags="-s -w -X main.buildTime=${BUILD_TIME} -X main.gitCommit=${GIT_COMMIT} -extldflags '-static'" \ -o bin/opsxloki . diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml index 2880044..08ec35c 100644 --- a/kubernetes/deployment.yaml +++ b/kubernetes/deployment.yaml @@ -36,6 +36,16 @@ spec: - name: http containerPort: 8080 env: + # 可选:指定 admin 初始密码。不设置则自动生成 16 位随机密码, + # 写入 PVC 的 initial-admin-password.txt 并打印到首次启动日志。 + # 获取:kubectl -n log exec deploy/opsx-loki -- cat /app/data/initial-admin-password.txt + # 自定义: + # - name: ADMIN_INIT_PASSWORD + # valueFrom: + # secretKeyRef: + # name: opsxloki-secrets + # key: ADMIN_INIT_PASSWORD + # optional: true - name: DATA_DIR value: /app/data - name: JWT_SECRET diff --git a/run.sh b/run.sh index 5212a20..f2afdf0 100755 --- a/run.sh +++ b/run.sh @@ -112,7 +112,7 @@ start_backend() { fi if [ "$NEED_BUILD" = "1" ]; then warning "编译后端二进制..." - CGO_ENABLED=0 GOPROXY=https://goproxy.cn,direct $GO_CMD build -ldflags="-s -w" -o "$BACKEND_BIN" . || { + CGO_ENABLED=0 GOPROXY=https://goproxy.cn,direct $GO_CMD build -trimpath -ldflags="-s -w" -o "$BACKEND_BIN" . || { error "后端编译失败" return 1 }