added CLI improvments to docker
This commit is contained in:
1 parent
d7a51a0f60
commit
3055c413ef
7 files changed
+47
-145
No files matched your search
+9
-2
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user