From d8a2cab90fe51116070d9ccf428201a795e9f603 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Wed, 27 May 2026 21:15:00 +0000 Subject: [PATCH] feat: seed SSH users from env var and authorized_keys file (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Docker onboarding was broken — no way to add first SSH user without `docker attach` to TUI. Now reads SSH public keys from two sources on startup: - `UPTOP_ADMIN_KEY` env var — single key for quick single-user setup - `UPTOP_KEYS` file path — authorized_keys format for team setup Dockerfile already sets `UPTOP_KEYS=/data/authorized_keys` and compose mounts `./data:/data`, so the flow is: ``` echo "ssh-ed25519 AAAA... me@host" > ./data/authorized_keys docker compose up -d ssh -p 23234 localhost ``` ### Behavior - Skips keys already in DB (idempotent across restarts) - All seeded users get admin role - Username parsed from key comment (e.g. `tyler@macbook` → `tyler`) - Comments and blank lines in keys file are ignored ### Tested - UPTOP_ADMIN_KEY seeds single admin user - UPTOP_KEYS file seeds multiple users with correct usernames - Second startup skips existing keys (no duplicates) - Build and all tests pass Reviewed-on: https://gitea.lerkolabs.com/lerko/uptop/pulls/31 --- cmd/uptop/main.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 ++ 2 files changed, 82 insertions(+) diff --git a/cmd/uptop/main.go b/cmd/uptop/main.go index 659d509..65ad2f4 100644 --- a/cmd/uptop/main.go +++ b/cmd/uptop/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "context" "errors" "flag" @@ -9,7 +10,9 @@ import ( "net/url" "os" "os/signal" + "path/filepath" "strconv" + "strings" "sync" "syscall" "time" @@ -348,6 +351,8 @@ func runServe(args []string) { seedDemoData(s) } + seedKeysFromEnv(s) + if *importKuma != "" { kb, err := importer.LoadKumaFile(*importKuma) if err != nil { @@ -563,3 +568,78 @@ func (c *keyCache) IsAllowed(incomingKey ssh.PublicKey) bool { } return false } + +func seedKeysFromEnv(s store.Store) { + var keys []string + + if v := os.Getenv("UPTOP_ADMIN_KEY"); v != "" { + keys = append(keys, strings.TrimSpace(v)) + } + + if path := os.Getenv("UPTOP_KEYS"); path != "" { + f, err := os.Open(filepath.Clean(path)) + if err == nil { + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + keys = append(keys, line) + } + _ = f.Close() + } + } + + if len(keys) == 0 { + return + } + + existing, err := s.GetAllUsers() + if err != nil { + fmt.Fprintf(os.Stderr, "warning: could not check existing users: %v\n", err) + return + } + + existingKeys := make(map[string]bool) + for _, u := range existing { + existingKeys[u.PublicKey] = true + } + + added := 0 + for i, key := range keys { + if existingKeys[key] { + continue + } + + username := usernameFromKey(key, i, len(existing)+added) + if err := s.AddUser(username, key, "admin"); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to seed user %q: %v\n", username, err) + continue + } + fmt.Printf("Seeded admin user %q from %s\n", username, seedSource(i, len(keys), os.Getenv("UPTOP_ADMIN_KEY") != "")) + added++ + } +} + +func usernameFromKey(key string, index, totalExisting int) string { + parts := strings.Fields(key) + if len(parts) >= 3 { + comment := parts[2] + if at := strings.Index(comment, "@"); at > 0 { + return comment[:at] + } + return comment + } + if index == 0 && totalExisting == 0 { + return "admin" + } + return fmt.Sprintf("user-%d", totalExisting+1) +} + +func seedSource(index, total int, hasEnvKey bool) string { + if hasEnvKey && index == 0 { + return "UPTOP_ADMIN_KEY" + } + return "UPTOP_KEYS" +} diff --git a/docker-compose.yml b/docker-compose.yml index 9c71634..19452af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,5 +14,7 @@ services: - UPTOP_HTTP_PORT=8080 - UPTOP_STATUS_ENABLED=true - UPTOP_STATUS_TITLE=System Status + # SSH access: add your public key via env var or authorized_keys file + # - UPTOP_ADMIN_KEY=ssh-ed25519 AAAA... you@host volumes: - ./data:/data