feat(cli): homelab vault setup onboarding (one-time, self-service)

This commit is contained in:
Viktor Barzin 2026-06-24 10:21:57 +00:00
parent e20033855d
commit 5a864cf19c
2 changed files with 76 additions and 1 deletions

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bufio"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -459,7 +460,71 @@ func vaultLock(args []string) error {
return nil // lock/logout best-effort; never error the caller return nil // lock/logout best-effort; never error the caller
} }
func vaultSetup(args []string) error { return fmt.Errorf("not implemented") } func vaultPutArgs(user string, c vwCreds) []string {
return []string{"kv", "patch", vwCredsPath(user),
"vaultwarden_email=" + c.Email,
"vaultwarden_master_password=" + c.MasterPassword,
"vaultwarden_client_id=" + c.ClientID,
"vaultwarden_client_secret=" + c.ClientSecret,
}
}
// promptNoEcho reads one line without terminal echo (for the master password).
func promptNoEcho(prompt string) (string, error) {
fmt.Fprint(os.Stderr, prompt)
exec.Command("stty", "-echo").Run()
defer func() { exec.Command("stty", "echo").Run(); fmt.Fprintln(os.Stderr) }()
r := bufio.NewReader(os.Stdin)
line, err := r.ReadString('\n')
return strings.TrimSpace(line), err
}
func promptLine(prompt string) (string, error) {
fmt.Fprint(os.Stderr, prompt)
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
return strings.TrimSpace(line), err
}
func vaultSetup(args []string) error {
hardenProcess()
fmt.Fprintln(os.Stderr, "One-time setup. Stored ONLY in your own Vault path; the admin never sees it.")
fmt.Fprintln(os.Stderr, "Get your API key at https://vaultwarden.viktorbarzin.me → Settings → Security → Keys → View API key.")
email, err := promptLine("Vaultwarden email: ")
if err != nil {
return err
}
clientID, err := promptLine("API key client_id (user.xxxx): ")
if err != nil {
return err
}
clientSecret, err := promptNoEcho("API key client_secret: ")
if err != nil {
return err
}
master, err := promptNoEcho("Master password: ")
if err != nil {
return err
}
if master == "" || clientID == "" || clientSecret == "" {
return fmt.Errorf("all fields are required")
}
c := vwCreds{Email: email, MasterPassword: master, ClientID: clientID, ClientSecret: clientSecret}
if _, err := realRunner("vault", vaultPutArgs(vaultCurrentUser(), c), nil); err != nil {
return fmt.Errorf("writing creds to your Vault path failed (scoped token present?): %w", err)
}
fmt.Fprintln(os.Stderr, "Stored. Verifying unlock…")
uid := vaultCurrentUID()
unlock, err := withUserLock(uid)
if err != nil {
return err
}
defer unlock()
if _, err := openSession(realRunner, vaultCurrentUser(), uid); err != nil {
return fmt.Errorf("stored, but verification failed — double-check master password / API key: %w", err)
}
fmt.Fprintln(os.Stderr, "✓ Verified. Fetches are now AFK.")
return nil
}
func vaultGet(args []string) error { func vaultGet(args []string) error {
hardenProcess() hardenProcess()

View file

@ -233,6 +233,16 @@ func TestStatusSummaryUnconfigured(t *testing.T) {
} }
} }
func TestVaultPutArgs(t *testing.T) {
got := vaultPutArgs("emo", vwCreds{Email: "e", MasterPassword: "m", ClientID: "ci", ClientSecret: "cs"})
want := []string{"kv", "patch", "secret/workstation/claude-users/emo",
"vaultwarden_email=e", "vaultwarden_master_password=m",
"vaultwarden_client_id=ci", "vaultwarden_client_secret=cs"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("vaultPutArgs = %v", got)
}
}
// getValue is the testable core: given a runner + opts, returns the secret value. // getValue is the testable core: given a runner + opts, returns the secret value.
func TestGetValueFlow(t *testing.T) { func TestGetValueFlow(t *testing.T) {
f := &fakeRunner{out: map[string]string{ f := &fakeRunner{out: map[string]string{