From 2dd12fc6be4880df897574571a72f6ac30fe4298 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Wed, 24 Jun 2026 10:18:36 +0000 Subject: [PATCH] feat(cli): vault session bootstrap with per-user flock + no-coredump --- cli/cmd_vault.go | 54 +++++++++++++++++++++++++++++++++++++++++++ cli/cmd_vault_test.go | 6 +++++ 2 files changed, 60 insertions(+) diff --git a/cli/cmd_vault.go b/cli/cmd_vault.go index 55ed4030..825f14da 100644 --- a/cli/cmd_vault.go +++ b/cli/cmd_vault.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "strings" + "syscall" ) // vault verbs give each unix user no-HITL access to THEIR OWN Vaultwarden vault. @@ -215,6 +216,59 @@ func writeOpLog(r opRecord) { exec.Command("logger", "-t", "homelab-vault", opLogLine(r)).Run() // best-effort } +func vaultLockPath(uid string) string { return "/run/user/" + uid + "/homelab-vault.lock" } + +// hardenProcess disables core dumps so a bw/homelab crash can't spill the master +// password to a core file. Best-effort. +func hardenProcess() { + _ = syscall.Setrlimit(syscall.RLIMIT_CORE, &syscall.Rlimit{Cur: 0, Max: 0}) +} + +// withUserLock serializes bw mutations for this user (concurrent Claude sessions +// as the same user otherwise race bw's appdata). Returns an unlock func. +func withUserLock(uid string) (func(), error) { + f, err := os.OpenFile(vaultLockPath(uid), os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + f.Close() + return nil, err + } + return func() { syscall.Flock(int(f.Fd()), syscall.LOCK_UN); f.Close() }, nil +} + +// session is one usable bw context: the env (with BW_SESSION) ready for `bw get`. +type session struct { + env []string +} + +// openSession resolves creds, ensures login, unlocks, and returns a ready env. +// Caller must hold the user lock. appdata is created on tmpfs (0700). +func openSession(run cmdRunner, user, uid string) (session, error) { + creds, err := loadCreds(run, user) + if err != nil { + return session{}, err + } + appdata := bwAppDataDir(uid) + if err := os.MkdirAll(appdata, 0700); err != nil { + return session{}, fmt.Errorf("create bw appdata %s: %w", appdata, err) + } + loginEnv := bwSecretEnv(appdata, creds, "") + // Ensure server is set and we're logged in (idempotent; ignore "already"). + _, _ = run("bw", []string{"config", "server", "https://vaultwarden.viktorbarzin.me"}, loginEnv) + if st, _ := run("bw", bwStatusArgs(), loginEnv); !strings.Contains(st, "\"status\"") || strings.Contains(st, "unauthenticated") { + if _, err := run("bw", bwLoginArgs(), loginEnv); err != nil { + return session{}, fmt.Errorf("bw login --apikey failed (API key valid? run `homelab vault setup`): %w", err) + } + } + sess, err := bwUnlock(run, loginEnv) + if err != nil { + return session{}, err + } + return session{env: bwSecretEnv(appdata, creds, sess)}, nil +} + func vaultSetup(args []string) error { return fmt.Errorf("not implemented") } func vaultStatus(args []string) error { return fmt.Errorf("not implemented") } func vaultList(args []string) error { return fmt.Errorf("not implemented") } diff --git a/cli/cmd_vault_test.go b/cli/cmd_vault_test.go index b3a20ef4..da22692b 100644 --- a/cli/cmd_vault_test.go +++ b/cli/cmd_vault_test.go @@ -190,3 +190,9 @@ func TestOpLogLineHasNoSecretOrItem(t *testing.T) { } } } + +func TestLockPath(t *testing.T) { + if got := vaultLockPath("1001"); got != "/run/user/1001/homelab-vault.lock" { + t.Fatalf("vaultLockPath = %q", got) + } +}