Improve bootstrap token UX for easier discovery
The bootstrap token security requirement was added proactively but lacked discoverability, causing user friction during first-run setup. These improvements make the token easier to find while maintaining the security benefit. Improvements: - Display bootstrap token prominently in startup logs with ASCII box (previously: single line log message) - Add `pulse bootstrap-token` CLI command to display token on demand (Docker: docker exec <container> /app/pulse bootstrap-token) - Improve error messages in quick-setup API to show exact commands for retrieving token when missing or invalid - Error messages now include both Docker and bare metal examples User experience improvements: - Token visible in `docker logs` output immediately - Clear instructions printed with token - Helpful error messages if token is wrong/missing - CLI helper for operators who need to retrieve token later Security unchanged: - Bootstrap token still required for first-run setup - Token still auto-deleted after successful setup - No bypass mechanism added Related to discussion about bootstrap token UX friction.
This commit is contained in:
parent
80acc5ae72
commit
dd1d222ad0
4 changed files with 102 additions and 5 deletions
78
cmd/pulse/bootstrap.go
Normal file
78
cmd/pulse/bootstrap.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var bootstrapTokenCmd = &cobra.Command{
|
||||
Use: "bootstrap-token",
|
||||
Short: "Display the bootstrap setup token",
|
||||
Long: `Display the bootstrap setup token required for first-time setup.
|
||||
|
||||
This token is generated on first boot and must be entered in the web UI
|
||||
to unlock the initial setup wizard. The token is automatically deleted
|
||||
after successful setup completion.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
showBootstrapToken()
|
||||
},
|
||||
}
|
||||
|
||||
func showBootstrapToken() {
|
||||
// Determine data path (same logic as main server)
|
||||
dataPath := os.Getenv("PULSE_DATA_PATH")
|
||||
if dataPath == "" {
|
||||
if os.Getenv("PULSE_DOCKER") == "true" {
|
||||
dataPath = "/data"
|
||||
} else {
|
||||
dataPath = "/var/lib/pulse"
|
||||
}
|
||||
}
|
||||
|
||||
tokenPath := filepath.Join(dataPath, ".bootstrap_token")
|
||||
|
||||
// Read token file
|
||||
data, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("╔═══════════════════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ NO BOOTSTRAP TOKEN FOUND ║")
|
||||
fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣")
|
||||
fmt.Println("║ Possible reasons: ║")
|
||||
fmt.Println("║ • Initial setup has already been completed ║")
|
||||
fmt.Println("║ • Authentication is configured (token auto-deleted) ║")
|
||||
fmt.Println("║ • Server hasn't started yet (token not generated) ║")
|
||||
fmt.Printf("║ • Token file not found: %-44s║\n", tokenPath)
|
||||
fmt.Println("╚═══════════════════════════════════════════════════════════════════════╝")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Error reading bootstrap token: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(string(data))
|
||||
if token == "" {
|
||||
fmt.Println("Error: Bootstrap token file is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Display token prominently
|
||||
fmt.Println("╔═══════════════════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ BOOTSTRAP TOKEN FOR FIRST-TIME SETUP ║")
|
||||
fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣")
|
||||
fmt.Printf("║ Token: %-61s ║\n", token)
|
||||
fmt.Printf("║ File: %-61s ║\n", tokenPath)
|
||||
fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣")
|
||||
fmt.Println("║ Instructions: ║")
|
||||
fmt.Println("║ 1. Copy the token above ║")
|
||||
fmt.Println("║ 2. Open Pulse in your web browser ║")
|
||||
fmt.Println("║ 3. Paste the token into the unlock screen ║")
|
||||
fmt.Println("║ 4. Complete the admin account setup ║")
|
||||
fmt.Println("║ ║")
|
||||
fmt.Println("║ This token will be automatically deleted after successful setup. ║")
|
||||
fmt.Println("╚═══════════════════════════════════════════════════════════════════════╝")
|
||||
}
|
||||
|
|
@ -48,6 +48,8 @@ func init() {
|
|||
rootCmd.AddCommand(configCmd)
|
||||
// Add version command
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
// Add bootstrap-token command
|
||||
rootCmd.AddCommand(bootstrapTokenCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
|
|
|
|||
|
|
@ -87,9 +87,16 @@ func (r *Router) initializeBootstrapToken() {
|
|||
r.bootstrapTokenPath = path
|
||||
|
||||
if created {
|
||||
log.Warn().
|
||||
Str("token_path", path).
|
||||
Msg("Bootstrap setup token generated; retrieve it from the host to complete initial authentication")
|
||||
// Display token prominently for easy discovery
|
||||
log.Warn().Msg("╔═══════════════════════════════════════════════════════════════════════╗")
|
||||
log.Warn().Msg("║ BOOTSTRAP TOKEN REQUIRED FOR FIRST-TIME SETUP ║")
|
||||
log.Warn().Msg("╠═══════════════════════════════════════════════════════════════════════╣")
|
||||
log.Warn().Msgf("║ Token: %-61s ║", token)
|
||||
log.Warn().Msgf("║ File: %-61s ║", path)
|
||||
log.Warn().Msg("╠═══════════════════════════════════════════════════════════════════════╣")
|
||||
log.Warn().Msg("║ Copy this token and paste it into the unlock screen in your browser. ║")
|
||||
log.Warn().Msg("║ This token will be automatically deleted after successful setup. ║")
|
||||
log.Warn().Msg("╚═══════════════════════════════════════════════════════════════════════╝")
|
||||
} else {
|
||||
log.Info().
|
||||
Str("token_path", path).
|
||||
|
|
|
|||
|
|
@ -173,7 +173,12 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc {
|
|||
}
|
||||
|
||||
if providedToken == "" {
|
||||
http.Error(w, "Bootstrap setup token required", http.StatusUnauthorized)
|
||||
errorMsg := fmt.Sprintf("Bootstrap setup token required. Retrieve it from the host:\n\n"+
|
||||
"Docker: docker exec <container> cat /data/.bootstrap_token\n"+
|
||||
"Docker: docker exec <container> /app/pulse bootstrap-token\n"+
|
||||
"Bare metal: cat %s\n"+
|
||||
"Bare metal: pulse bootstrap-token", r.bootstrapTokenPath)
|
||||
http.Error(w, errorMsg, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +186,12 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc {
|
|||
log.Warn().
|
||||
Str("ip", clientIP).
|
||||
Msg("Rejected quick setup with invalid bootstrap token")
|
||||
http.Error(w, "Invalid bootstrap setup token", http.StatusUnauthorized)
|
||||
errorMsg := fmt.Sprintf("Invalid bootstrap setup token. Retrieve the correct token from the host:\n\n"+
|
||||
"Docker: docker exec <container> cat /data/.bootstrap_token\n"+
|
||||
"Docker: docker exec <container> /app/pulse bootstrap-token\n"+
|
||||
"Bare metal: cat %s\n"+
|
||||
"Bare metal: pulse bootstrap-token", r.bootstrapTokenPath)
|
||||
http.Error(w, errorMsg, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue