Skip to content

Commit 34859ee

Browse files
committed
fix(security): require ADMIN_PASSWORD instead of logging a generated one
CodeQL go/clear-text-logging (high) flagged logging the generated admin password. Logging a secret is a real leak (logs get shipped and read), so remove it: when authentication is enabled the server now requires ADMIN_PASSWORD and fails closed if it is unset, rather than seeding a well-known password or logging a generated one. When authentication is disabled (development) a random, unlogged password is seeded since it gates nothing. Compose now requires ADMIN_PASSWORD via ${ADMIN_PASSWORD:?}; docs and the k8s secret template are updated to match.
1 parent f9fe4b6 commit 34859ee

9 files changed

Lines changed: 45 additions & 45 deletions

File tree

.env.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ AUTH_ENABLED=true
1717
# openssl rand -base64 32
1818
AUTH_SECRETKEY=
1919

20-
# Initial admin password. Leave blank to have the server generate a random one
21-
# and print it once at first boot. Set it to choose your own.
20+
# Initial admin password. Required on first boot when AUTH_ENABLED=true: the
21+
# server refuses to create the admin account without it (it will not seed a
22+
# well-known password or log a generated one).
2223
# openssl rand -base64 24
2324
ADMIN_PASSWORD=
2425

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ git clone https://github.com/Veritas-Calculus/vc-terraform-registry.git
2727
cd vc-terraform-registry
2828

2929
cp .env.example .env
30-
# Edit .env and set a strong AUTH_SECRETKEY: openssl rand -base64 32
31-
# The server refuses to start in release mode without one.
30+
# Edit .env and set two required secrets before first boot:
31+
# AUTH_SECRETKEY (openssl rand -base64 32) - JWT signing key
32+
# ADMIN_PASSWORD (openssl rand -base64 24) - initial admin password
33+
# The server refuses to start in release mode without a strong signing key,
34+
# and refuses to create the admin account without ADMIN_PASSWORD.
3235

3336
docker compose up -d
3437
```
3538

3639
Then open `https://localhost:3443` (self-signed certificate; accept the warning).
37-
The Terraform CLI requires HTTPS, so use the 3443 port.
38-
39-
On first boot, if you did not set `ADMIN_PASSWORD`, the backend generates a random
40-
admin password and prints it once in the logs (`docker compose logs backend`).
40+
The Terraform CLI requires HTTPS, so use the 3443 port. Sign in as `admin` with
41+
the `ADMIN_PASSWORD` you set.
4142

4243
Ports: frontend HTTPS `3443`, frontend HTTP `3000`, backend API `127.0.0.1:8080`.
4344

README.zh-CN.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ git clone https://github.com/Veritas-Calculus/vc-terraform-registry.git
2626
cd vc-terraform-registry
2727

2828
cp .env.example .env
29-
# 编辑 .env,设置强随机 AUTH_SECRETKEY:openssl rand -base64 32
30-
# release 模式下缺少该密钥服务将拒绝启动。
29+
# 首次启动前在 .env 中设置两个必填密钥:
30+
# AUTH_SECRETKEY (openssl rand -base64 32) - JWT 签名密钥
31+
# ADMIN_PASSWORD (openssl rand -base64 24) - 初始管理员密码
32+
# release 模式下缺少强签名密钥服务将拒绝启动;未设置 ADMIN_PASSWORD 时拒绝创建管理员账号。
3133

3234
docker compose up -d
3335
```
3436

3537
随后访问 `https://localhost:3443`(自签名证书,需要在浏览器中接受安全警告)。
36-
Terraform CLI 要求使用 HTTPS,请使用 3443 端口。
37-
38-
首次启动时,若未设置 `ADMIN_PASSWORD`,后端会生成随机管理员密码并在日志中打印一次
39-
`docker compose logs backend`)。
38+
Terraform CLI 要求使用 HTTPS,请使用 3443 端口。使用用户名 `admin` 和你设置的
39+
`ADMIN_PASSWORD` 登录。
4040

4141
端口:前端 HTTPS `3443`、前端 HTTP `3000`、后端 API `127.0.0.1:8080`
4242

backend/cmd/server/main.go

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -141,39 +141,36 @@ func initDatabase(cfg *config.Config) (*gorm.DB, error) {
141141
db.Model(&models.User{}).Count(&userCount)
142142
if userCount == 0 {
143143
adminPassword := os.Getenv("ADMIN_PASSWORD")
144-
generated := false
145144
if adminPassword == "" {
146-
// Never fall back to a well-known password. Generate a strong random
147-
// one and surface it once in the logs so the operator can capture it.
145+
if cfg.Auth.Enabled {
146+
// Fail closed rather than seed a well-known password or log a
147+
// generated one (logging a secret is itself a leak). The operator
148+
// must choose the initial admin password explicitly.
149+
return nil, fmt.Errorf(
150+
"ADMIN_PASSWORD is not set; set it to create the initial admin account " +
151+
"(e.g. `openssl rand -base64 24`)")
152+
}
153+
// Authentication is disabled (development): the admin credential gates
154+
// nothing, so seed a random password that is never logged.
148155
adminPassword, err = generatePassword(24)
149156
if err != nil {
150157
return nil, fmt.Errorf("failed to generate admin password: %w", err)
151158
}
152-
generated = true
153159
}
154160
hashedPassword, err := auth.HashPassword(adminPassword)
155161
if err != nil {
156-
log.Printf("Warning: failed to hash admin password: %v", err)
157-
} else {
158-
adminUser := models.User{
159-
Username: "admin",
160-
Email: "admin@localhost",
161-
Password: hashedPassword,
162-
Role: "admin",
163-
}
164-
if err := db.Create(&adminUser).Error; err != nil {
165-
log.Printf("Warning: failed to create admin user: %v", err)
166-
} else if generated {
167-
log.Printf("=====================================================================")
168-
log.Printf(" Initial admin account created. Username: admin")
169-
log.Printf(" Generated password: %s", adminPassword)
170-
log.Printf(" Store it now and change it after first login. It is shown only once.")
171-
log.Printf(" Set ADMIN_PASSWORD to choose your own on first boot.")
172-
log.Printf("=====================================================================")
173-
} else {
174-
log.Printf("Initial admin user created (username: admin) from ADMIN_PASSWORD")
175-
}
162+
return nil, fmt.Errorf("failed to hash admin password: %w", err)
163+
}
164+
adminUser := models.User{
165+
Username: "admin",
166+
Email: "admin@localhost",
167+
Password: hashedPassword,
168+
Role: "admin",
169+
}
170+
if err := db.Create(&adminUser).Error; err != nil {
171+
return nil, fmt.Errorf("failed to create admin user: %w", err)
176172
}
173+
log.Printf("Initial admin user 'admin' created")
177174
}
178175

179176
log.Printf("Database initialized: %s", dbPath)

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ services:
1818
- AUTH_ENABLED=true
1919
# Supplied from .env; compose aborts if unset. Generate: openssl rand -base64 32
2020
- AUTH_SECRETKEY=${AUTH_SECRETKEY:?set AUTH_SECRETKEY in .env (openssl rand -base64 32)}
21-
# Initial admin password. If omitted, the backend generates a random one
22-
# and prints it once at first boot; set it here to choose your own.
23-
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
21+
# Initial admin password, required on first boot because auth is enabled.
22+
# Generate: openssl rand -base64 24
23+
- ADMIN_PASSWORD=${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env (openssl rand -base64 24)}
2424
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}
2525
- LOG_LEVEL=info
2626
volumes:

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Environment variables take precedence.
1515
| `DATABASE_URL` | Database connection string | `sqlite:///data/registry.db` |
1616
| `AUTH_ENABLED` | Enable authentication | `true` |
1717
| `AUTH_SECRETKEY` | JWT signing key. Required in release mode; must be a strong random value of at least 32 bytes | none |
18-
| `ADMIN_PASSWORD` | Initial admin password. If unset, a random one is generated and printed once at first boot | none |
18+
| `ADMIN_PASSWORD` | Initial admin password. Required on first boot when `AUTH_ENABLED=true`; the server will not seed a well-known password or log a generated one | none |
1919
| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed browser origins, or `*` for any | `*` |
2020
| `LOG_LEVEL` | Log level | `info` |
2121

docs/deployment.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Ensure Docker is running, then:
66

77
```bash
88
cp .env.example .env
9-
# Set a strong AUTH_SECRETKEY (openssl rand -base64 32). Optionally set ADMIN_PASSWORD.
9+
# Set a strong AUTH_SECRETKEY (openssl rand -base64 32) and an ADMIN_PASSWORD
10+
# (openssl rand -base64 24). Both are required on first boot.
1011

1112
# Using make
1213
make start

docs/security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ a remediation note.
3838
| Sev | Finding | Fix |
3939
|-----|---------|-----|
4040
| Critical | JWT signing key was effectively unconfigurable and defaulted to a public placeholder; `docker-compose.yml` also hardcoded a public key as a literal, and the documented `.env` override was silently ignored. Anyone could forge an admin token offline. | Root cause was a Viper misconfiguration: nested keys (`auth.secretkey`) were never bound to env vars (`AUTH_SECRETKEY`), so every environment override was dropped. Added `SetEnvKeyReplacer` plus explicit `BindEnv` for all keys. Added a fail-closed guard that refuses to start in release mode on any known placeholder key or a key shorter than 32 bytes. `docker-compose.yml` now uses `${AUTH_SECRETKEY:?}` so Compose aborts when it is unset. `k8s/secret.yaml` ships with no value and documents out-of-band creation. `.env.example` blanks the key. Regression tests cover the env-override path and the guard. |
41-
| High | Default admin account `admin/admin123` was auto-created on first boot; no deployment path set `ADMIN_PASSWORD`. | If `ADMIN_PASSWORD` is unset, the server now generates a strong random password with `crypto/rand` and logs it once at first boot. The well-known fallback is gone. |
41+
| High | Default admin account `admin/admin123` was auto-created on first boot; no deployment path set `ADMIN_PASSWORD`. | When authentication is enabled, the server now requires `ADMIN_PASSWORD` and fails closed if it is unset - it never seeds a well-known password and never logs a generated one (logging a secret is itself a leak, flagged by CodeQL `go/clear-text-logging`). When authentication is disabled (development), a random unlogged password is seeded since it gates nothing. |
4242
| High | A committed key in `k8s/secret.yaml` was applied by the documented deploy command. | Value removed; the manifest documents `kubectl create secret` from a generated value. |
4343
| High | Unauthenticated mirror-protocol routes spawned one unbounded background upstream download per request (disk/bandwidth amplification DoS). | Background caching is now bounded by a semaphore (max 3 concurrent) and deduped by an in-flight set, launched via `tryStartBackgroundCache`. A burst degrades to "cached on a later request" instead of spawning unbounded goroutines. The routes stay unauthenticated so the Terraform network mirror keeps working. |
4444
| Medium | The role claim was never enforced: `RequireRole` was applied to zero routes, so any authenticated principal was fully privileged. | Write/management routes are now behind an admin group (`AuthMiddleware` + `RequireRole("admin")`). `/auth/me` stays at authenticated-only. Verified: a valid non-admin token gets 403 on writes. |

k8s/secret.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ stringData:
2121
# If you must template it, inject the values from your secret manager at apply
2222
# time; never store them in git.
2323
AUTH_SECRETKEY: "" # REQUIRED: set to a strong random value out of band
24-
ADMIN_PASSWORD: "" # OPTIONAL: initial admin password; omit to auto-generate
24+
ADMIN_PASSWORD: "" # REQUIRED on first boot with auth enabled: initial admin password
2525
---
2626
# TLS Secret for HTTPS (optional, create from your certificates)
2727
# kubectl create secret tls terraform-registry-tls \

0 commit comments

Comments
 (0)