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 }