feat(security): 移除 admin123 硬编码默认密码 + 编译加 -trimpath

安全加固(降低二进制泄露/逆向的信息面):

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 均存在
This commit is contained in:
opsx committed 2026-10-26 17:01:09 +00:00
1 parent 9a46a955d4
commit 3c27e58a73
8 files changed
+163 -5

No files matched your search

+6
View File
@@ -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
+63 -1
View File
@@ -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 <pod-name>:/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` 时不生成随机密码、不写文件 |
---
## 生产部署
+5
View File
@@ -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"})
}
+2 -1
View File
@@ -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)
}
+75 -1
View File
@@ -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 <ns> 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)
}
+1 -1
View File
@@ -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 .
+10
View File
@@ -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
+1 -1
View File
@@ -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
}