feat: seed SSH users from env var and authorized_keys file (#31)
## 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: lerko/uptop#31
This commit was merged in pull request #31.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -9,7 +10,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -348,6 +351,8 @@ func runServe(args []string) {
|
|||||||
seedDemoData(s)
|
seedDemoData(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seedKeysFromEnv(s)
|
||||||
|
|
||||||
if *importKuma != "" {
|
if *importKuma != "" {
|
||||||
kb, err := importer.LoadKumaFile(*importKuma)
|
kb, err := importer.LoadKumaFile(*importKuma)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -563,3 +568,78 @@ func (c *keyCache) IsAllowed(incomingKey ssh.PublicKey) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ services:
|
|||||||
- UPTOP_HTTP_PORT=8080
|
- UPTOP_HTTP_PORT=8080
|
||||||
- UPTOP_STATUS_ENABLED=true
|
- UPTOP_STATUS_ENABLED=true
|
||||||
- UPTOP_STATUS_TITLE=System Status
|
- 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:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|||||||
Reference in New Issue
Block a user