added CLI improvments to docker

This commit is contained in:
Turbootzz committed 2025-10-24 19:01:39 +02:00
1 parent d7a51a0f60
commit 3055c413ef
7 files changed
+47 -145

No files matched your search

+9 -2
View File
@@ -13,8 +13,15 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000
# URL of your Vaultwarden instance (no trailing slash) # URL of your Vaultwarden instance (no trailing slash)
VAULTWARDEN_URL=https://vault.yourdomain.com VAULTWARDEN_URL=https://vault.yourdomain.com
# Authentication # Authentication - Choose ONE method:
VAULTWARDEN_ACCESS_TOKEN=your-session-token-here # 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 # Performance & Caching
# How long to cache secrets in memory (reduces API calls) # How long to cache secrets in memory (reduces API calls)
+10 -12
View File
@@ -25,17 +25,12 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
./cmd/api ./cmd/api
# Final stage - minimal runtime image # Final stage - minimal runtime image
FROM alpine:3.19 FROM node:24-alpine
# Install CA certificates for HTTPS, wget for healthcheck, and Bitwarden CLI # Install CA certificates and wget
RUN apk --no-cache add ca-certificates wget nodejs npm && \ RUN apk --no-cache add ca-certificates wget && \
npm install -g @bitwarden/cli && \ npm install -g @bitwarden/cli && \
apk del npm && \ npm cache clean --force
rm -rf /root/.npm
# Create non-root user for security
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
@@ -43,11 +38,14 @@ WORKDIR /app
# Copy binary from builder # Copy binary from builder
COPY --from=builder /build/vaultwarden-api . COPY --from=builder /build/vaultwarden-api .
# Change ownership to non-root user # Change ownership to existing node user
RUN chown -R appuser:appuser /app 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 # Switch to non-root user
USER appuser USER node
# Expose port # Expose port
EXPOSE 8080 EXPOSE 8080
+6 -5
View File
@@ -31,12 +31,13 @@ func main() {
// Initialize Vaultwarden client // Initialize Vaultwarden client
var vaultClient *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") vaultwardenPassword := os.Getenv("VAULTWARDEN_PASSWORD")
if vaultwardenEmail != "" && vaultwardenPassword != "" { if vaultwardenClientID != "" && vaultwardenClientSecret != "" && vaultwardenPassword != "" {
logger.Info.Println("Initializing Bitwarden CLI with credentials") logger.Info.Println("Initializing Bitwarden CLI with API key")
sessionToken, err := vaultwarden.InitializeBitwardenCLI(cfg.VaultwardenURL, vaultwardenEmail, vaultwardenPassword) sessionToken, err := vaultwarden.InitializeBitwardenCLI(cfg.VaultwardenURL, vaultwardenClientID, vaultwardenClientSecret, vaultwardenPassword)
if err != nil { if err != nil {
logger.Error.Fatalf("Failed to initialize Bitwarden CLI: %v", err) 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)") logger.Warn.Println("Using provided session token (will expire)")
vaultClient = vaultwarden.NewClient(cfg.VaultwardenURL, cfg.VaultwardenToken, cfg.CacheTTL) vaultClient = vaultwarden.NewClient(cfg.VaultwardenURL, cfg.VaultwardenToken, cfg.CacheTTL)
} else { } 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 // Initialize handlers
-6
View File
@@ -57,12 +57,6 @@ func Load() (*Config, error) {
return nil, fmt.Errorf("VAULTWARDEN_URL is required") 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 // Validate and normalize URL
parsedURL, err := url.Parse(cfg.VaultwardenURL) parsedURL, err := url.Parse(cfg.VaultwardenURL)
if err != nil { if err != nil {
-12
View File
@@ -5,8 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"strings"
"sync" "sync"
"time" "time"
@@ -72,13 +70,6 @@ func NewClient(baseURL, token string, cacheTTL time.Duration) *Client {
go cache.startCleanup(cacheTTL) 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{ return &Client{
baseURL: baseURL, baseURL: baseURL,
token: token, token: token,
@@ -203,10 +194,7 @@ func (c *Client) fetchSecret(name string) (string, error) {
return "", fmt.Errorf("failed to decode response: %w", err) 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 { for _, cipher := range cipherResp.Data {
logger.Info.Printf("Cipher name: '%s' (looking for: '%s')", cipher.Name, name)
if cipher.Name == name { if cipher.Name == name {
return c.extractSecretValue(cipher) return c.extractSecretValue(cipher)
} }
+22 -8
View File
@@ -1,36 +1,50 @@
package vaultwarden package vaultwarden
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"time"
"github.com/thijsherman/vaultwarden-api/pkg/logger" "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...") 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 { if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("config server failed: %w - %s", err, output) return "", fmt.Errorf("config server failed: %w - %s", err, output)
} }
cmd = exec.Command("bw", "login", email, "--passwordenv", "BW_PASSWORD", "--raw") cmd = exec.CommandContext(ctx, "bw", "login", "--apikey")
cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password)) cmd.Env = append(os.Environ(),
fmt.Sprintf("BW_CLIENTID=%s", clientID),
fmt.Sprintf("BW_CLIENTSECRET=%s", clientSecret),
)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
outputStr := strings.TrimSpace(string(output)) 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) 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)) cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password))
sessionToken, err := cmd.Output() sessionToken, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return "", fmt.Errorf("unlock failed: %w", err) return "", fmt.Errorf("unlock failed: %w - %s", err, sessionToken)
} }
token := strings.TrimSpace(string(sessionToken)) token := strings.TrimSpace(string(sessionToken))
-100
View File
@@ -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
}