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:
1 parent
9a46a955d4
commit
3c27e58a73
8 files changed
+163
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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` 时不生成随机密码、不写文件 |
|
||||
|
||||
---
|
||||
|
||||
## 生产部署
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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 .
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user