Lock bootstrap admin token until password is changed.
Format / gofmt (push) Successful in 7s
CI / Build (push) Successful in 14s
Format / gofmt (pull_request) Successful in 7s
CI / Build (pull_request) Successful in 13s
CI / Go Tests (push) Successful in 50s
CI / Go Tests (pull_request) Successful in 48s

Closes #2: bootstrap JWTs cannot mutate admin APIs except change-password, production requires ADMIN_PASSWORD on first boot, admin binds loopback.
This commit is contained in:
s1d3sw1ped_bot
2026-09-01 03:54:09 +00:00
parent 6aabb76c16
commit 13070a275d
13 changed files with 445 additions and 40 deletions
+104
View File
@@ -0,0 +1,104 @@
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"helix-proxy/internal/auth"
"helix-proxy/internal/config"
"helix-proxy/internal/store"
"golang.org/x/crypto/bcrypt"
)
const (
adminEmail = "admin@example.com"
defaultAdminPassword = "password"
testAdminPassword = "helix-test-admin"
)
// applyInitialAdminPassword sets the admin hash from ADMIN_PASSWORD on first boot.
// Production refuses to start if the password is still unset. Development may
// leave the password empty (bootstrap lock) when ADMIN_PASSWORD is not set.
func applyInitialAdminPassword(st store.Store) error {
u, ok := st.GetUserByEmail(adminEmail)
if !ok {
return fmt.Errorf("admin user missing")
}
if u.Password != "" {
return nil
}
envPW := os.Getenv("ADMIN_PASSWORD")
if envPW != "" {
if envPW == defaultAdminPassword {
return fmt.Errorf("ADMIN_PASSWORD cannot be the well-known default")
}
h, err := bcrypt.GenerateFromPassword([]byte(envPW), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("hash ADMIN_PASSWORD: %w", err)
}
u.Password = string(h)
if err := st.UpdateUser(u); err != nil {
return err
}
slog.Info("admin password set from ADMIN_PASSWORD")
return nil
}
if config.IsDevelopment() {
slog.Warn("admin password unset; bootstrap lock active until a non-default password is stored")
return nil
}
return fmt.Errorf("ADMIN_PASSWORD is required on first boot; the default password is not permitted")
}
func testLoginPassword() string {
if p := os.Getenv("ADMIN_PASSWORD"); p != "" {
return p
}
return defaultAdminPassword
}
func adminPasswordUnset(st store.Store) bool {
u, ok := st.GetUserByEmail(adminEmail)
return !ok || u.Password == ""
}
func bootstrapPathAllowed(r *http.Request) bool {
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
return true
}
p := strings.TrimSuffix(r.URL.Path, "/")
if r.Method == http.MethodPost && (p == "/api/login" || p == "/api/users/me/password") {
return true
}
return false
}
// bootstrapLock rejects mutating admin API (except login + change-password)
// while the default password is still in effect, or when the JWT is a bootstrap token.
func bootstrapLock(st store.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if bootstrapPathAllowed(r) {
next.ServeHTTP(w, r)
return
}
claims, ok := auth.GetUserFromContext(r.Context())
if !ok {
next.ServeHTTP(w, r)
return
}
if claims.Bootstrap || adminPasswordUnset(st) {
http.Error(w, "password change required", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}