From 3055c413efa7c6816e42231c0ca08344a6445249 Mon Sep 17 00:00:00 2001 From: Turbootzz Date: Fri, 24 Oct 2025 19:01:39 +0200 Subject: [PATCH] added CLI improvments to docker --- .env.example | 11 +++- Dockerfile | 22 ++++--- cmd/api/main.go | 11 ++-- internal/config/config.go | 6 -- internal/vaultwarden/client.go | 12 ---- internal/vaultwarden/init.go | 30 +++++++--- internal/vaultwarden/session.go | 100 -------------------------------- 7 files changed, 47 insertions(+), 145 deletions(-) delete mode 100644 internal/vaultwarden/session.go diff --git a/.env.example b/.env.example index 4812dcc..8766c59 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,15 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000 # URL of your Vaultwarden instance (no trailing slash) VAULTWARDEN_URL=https://vault.yourdomain.com -# Authentication -VAULTWARDEN_ACCESS_TOKEN=your-session-token-here +# Authentication - Choose ONE method: +# Method 1: API Key (Recommended - bypasses 2FA, auto-login on startup) +# Get your API key from: Account Settings -> Security -> Keys -> View API Key +VAULTWARDEN_CLIENT_ID=user.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +VAULTWARDEN_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxx +VAULTWARDEN_PASSWORD=your-master-password + +# Method 2: Pre-generated session token (Alternative - will expire) +# VAULTWARDEN_ACCESS_TOKEN=your-session-token-here # Performance & Caching # How long to cache secrets in memory (reduces API calls) diff --git a/Dockerfile b/Dockerfile index 8eec826..1c482e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,17 +25,12 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ ./cmd/api # Final stage - minimal runtime image -FROM alpine:3.19 +FROM node:24-alpine -# Install CA certificates for HTTPS, wget for healthcheck, and Bitwarden CLI -RUN apk --no-cache add ca-certificates wget nodejs npm && \ +# Install CA certificates and wget +RUN apk --no-cache add ca-certificates wget && \ npm install -g @bitwarden/cli && \ - apk del npm && \ - rm -rf /root/.npm - -# Create non-root user for security -RUN addgroup -g 1000 appuser && \ - adduser -D -u 1000 -G appuser appuser + npm cache clean --force # Set working directory WORKDIR /app @@ -43,11 +38,14 @@ WORKDIR /app # Copy binary from builder COPY --from=builder /build/vaultwarden-api . -# Change ownership to non-root user -RUN chown -R appuser:appuser /app +# Change ownership to existing node user +RUN chown -R node:node /app + +# Set Bitwarden CLI data directory to /tmp (writable) +ENV BITWARDENCLI_APPDATA_DIR=/tmp/.bitwarden # Switch to non-root user -USER appuser +USER node # Expose port EXPOSE 8080 diff --git a/cmd/api/main.go b/cmd/api/main.go index 0da6856..091187c 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -31,12 +31,13 @@ func main() { // Initialize Vaultwarden client var vaultClient *vaultwarden.Client - vaultwardenEmail := os.Getenv("VAULTWARDEN_EMAIL") + vaultwardenClientID := os.Getenv("VAULTWARDEN_CLIENT_ID") + vaultwardenClientSecret := os.Getenv("VAULTWARDEN_CLIENT_SECRET") vaultwardenPassword := os.Getenv("VAULTWARDEN_PASSWORD") - if vaultwardenEmail != "" && vaultwardenPassword != "" { - logger.Info.Println("Initializing Bitwarden CLI with credentials") - sessionToken, err := vaultwarden.InitializeBitwardenCLI(cfg.VaultwardenURL, vaultwardenEmail, vaultwardenPassword) + if vaultwardenClientID != "" && vaultwardenClientSecret != "" && vaultwardenPassword != "" { + logger.Info.Println("Initializing Bitwarden CLI with API key") + sessionToken, err := vaultwarden.InitializeBitwardenCLI(cfg.VaultwardenURL, vaultwardenClientID, vaultwardenClientSecret, vaultwardenPassword) if err != nil { logger.Error.Fatalf("Failed to initialize Bitwarden CLI: %v", err) } @@ -45,7 +46,7 @@ func main() { logger.Warn.Println("Using provided session token (will expire)") vaultClient = vaultwarden.NewClient(cfg.VaultwardenURL, cfg.VaultwardenToken, cfg.CacheTTL) } else { - logger.Error.Fatal("No authentication method configured. Set VAULTWARDEN_EMAIL+VAULTWARDEN_PASSWORD or VAULTWARDEN_ACCESS_TOKEN") + logger.Error.Fatal("No authentication configured. Set VAULTWARDEN_CLIENT_ID+VAULTWARDEN_CLIENT_SECRET+VAULTWARDEN_PASSWORD") } // Initialize handlers diff --git a/internal/config/config.go b/internal/config/config.go index fa5e4da..98b4e65 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -57,12 +57,6 @@ func Load() (*Config, error) { return nil, fmt.Errorf("VAULTWARDEN_URL is required") } - hasAPIKeys := cfg.VaultwardenClientID != "" && cfg.VaultwardenSecret != "" - hasSessionToken := cfg.VaultwardenToken != "" - if !hasAPIKeys && !hasSessionToken { - return nil, fmt.Errorf("either VAULTWARDEN_CLIENT_ID+VAULTWARDEN_CLIENT_SECRET or VAULTWARDEN_ACCESS_TOKEN is required") - } - // Validate and normalize URL parsedURL, err := url.Parse(cfg.VaultwardenURL) if err != nil { diff --git a/internal/vaultwarden/client.go b/internal/vaultwarden/client.go index 656abc4..e725eac 100644 --- a/internal/vaultwarden/client.go +++ b/internal/vaultwarden/client.go @@ -5,8 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" - "strings" "sync" "time" @@ -72,13 +70,6 @@ func NewClient(baseURL, token string, cacheTTL time.Duration) *Client { go cache.startCleanup(cacheTTL) } - if token == "" { - if sessionData, err := os.ReadFile("/tmp/bw_session"); err == nil { - token = strings.TrimSpace(string(sessionData)) - logger.Info.Println("Loaded session token from /tmp/bw_session") - } - } - return &Client{ baseURL: baseURL, token: token, @@ -203,10 +194,7 @@ func (c *Client) fetchSecret(name string) (string, error) { return "", fmt.Errorf("failed to decode response: %w", err) } - logger.Info.Printf("Found %d ciphers from Vaultwarden API", len(cipherResp.Data)) - for _, cipher := range cipherResp.Data { - logger.Info.Printf("Cipher name: '%s' (looking for: '%s')", cipher.Name, name) if cipher.Name == name { return c.extractSecretValue(cipher) } diff --git a/internal/vaultwarden/init.go b/internal/vaultwarden/init.go index 9febae1..baeb45e 100644 --- a/internal/vaultwarden/init.go +++ b/internal/vaultwarden/init.go @@ -1,36 +1,50 @@ package vaultwarden import ( + "context" "fmt" "os" "os/exec" "strings" + "sync" + "time" "github.com/thijsherman/vaultwarden-api/pkg/logger" ) -func InitializeBitwardenCLI(serverURL, email, password string) (string, error) { +var bitwardenInitMutex sync.Mutex + +func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) (string, error) { + bitwardenInitMutex.Lock() + defer bitwardenInitMutex.Unlock() + logger.Info.Println("Initializing Bitwarden CLI...") - cmd := exec.Command("bw", "config", "server", serverURL) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "bw", "config", "server", serverURL) if output, err := cmd.CombinedOutput(); err != nil { return "", fmt.Errorf("config server failed: %w - %s", err, output) } - cmd = exec.Command("bw", "login", email, "--passwordenv", "BW_PASSWORD", "--raw") - cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password)) + cmd = exec.CommandContext(ctx, "bw", "login", "--apikey") + cmd.Env = append(os.Environ(), + fmt.Sprintf("BW_CLIENTID=%s", clientID), + fmt.Sprintf("BW_CLIENTSECRET=%s", clientSecret), + ) output, err := cmd.CombinedOutput() outputStr := strings.TrimSpace(string(output)) - if err != nil && !strings.Contains(outputStr, "already logged in") { + if err != nil && !strings.Contains(outputStr, "You are logged in!") { return "", fmt.Errorf("login failed: %w - %s", err, outputStr) } - cmd = exec.Command("bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw") + cmd = exec.CommandContext(ctx, "bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw") cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password)) - sessionToken, err := cmd.Output() + sessionToken, err := cmd.CombinedOutput() if err != nil { - return "", fmt.Errorf("unlock failed: %w", err) + return "", fmt.Errorf("unlock failed: %w - %s", err, sessionToken) } token := strings.TrimSpace(string(sessionToken)) diff --git a/internal/vaultwarden/session.go b/internal/vaultwarden/session.go deleted file mode 100644 index d41353f..0000000 --- a/internal/vaultwarden/session.go +++ /dev/null @@ -1,100 +0,0 @@ -package vaultwarden - -import ( - "fmt" - "os" - "os/exec" - "sync" - "time" - - "github.com/thijsherman/vaultwarden-api/pkg/logger" -) - -type SessionManager struct { - mu sync.RWMutex - sessionToken string - serverURL string - email string - password string - refreshTicker *time.Ticker - stopChan chan bool -} - -func NewSessionManager(serverURL, email, password string) *SessionManager { - sm := &SessionManager{ - serverURL: serverURL, - email: email, - password: password, - stopChan: make(chan bool), - } - - if err := sm.login(); err != nil { - logger.Warn.Printf("Initial login failed: %v - will retry", err) - } - - sm.startAutoRefresh() - return sm -} - -func (sm *SessionManager) GetToken() string { - sm.mu.RLock() - defer sm.mu.RUnlock() - return sm.sessionToken -} - -func (sm *SessionManager) login() error { - sm.mu.Lock() - defer sm.mu.Unlock() - - cmd := exec.Command("bw", "config", "server", sm.serverURL) - if err := cmd.Run(); err != nil { - return fmt.Errorf("config server failed: %w", err) - } - - if sm.password != "" { - cmd = exec.Command("bw", "login", sm.email, "--passwordenv", "BW_PASSWORD") - cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", sm.password)) - output, err := cmd.CombinedOutput() - if err != nil && string(output) != "" && !contains(string(output), "already logged in") { - return fmt.Errorf("login failed: %w - %s", err, output) - } - } - - cmd = exec.Command("bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw") - cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", sm.password)) - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("unlock failed: %w", err) - } - - sm.sessionToken = string(output) - logger.Info.Println("Successfully obtained Bitwarden session token") - return nil -} - -func (sm *SessionManager) startAutoRefresh() { - sm.refreshTicker = time.NewTicker(6 * time.Hour) - - go func() { - for { - select { - case <-sm.refreshTicker.C: - logger.Info.Println("Refreshing Bitwarden session...") - if err := sm.login(); err != nil { - logger.Error.Printf("Session refresh failed: %v", err) - } - case <-sm.stopChan: - return - } - } - }() -} - -func (sm *SessionManager) Stop() { - sm.refreshTicker.Stop() - sm.stopChan <- true -} - -func contains(s, substr string) bool { - return len(s) >= len(substr) && s[:len(substr)] == substr -}