Adjust cookie security for proxy/HTTP dev

This commit is contained in:
Rishikanth Chandrasekaran
2026-01-29 18:57:03 -08:00
parent 96dfb637ae
commit c0ee91cca9
8 changed files with 115 additions and 34 deletions

View File

@@ -36,7 +36,8 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
echo "placeholder" > internal/web/dist/dummy_asset; \
fi; \
\
trap 'echo ""; echo "🛑 Stopping development servers..."; kill 0; exit 0' INT TERM; \
pids=""; \
trap 'echo ""; echo "🛑 Stopping development servers..."; for pid in $$pids; do kill $$pid 2>/dev/null || true; done; wait $$pids 2>/dev/null || true; exit 0' INT TERM; \
\
echo "🧠 Starting ASR engine..."; \
ASR_ENGINE_SOCKET=$${ASR_ENGINE_SOCKET:-/tmp/scriberr-asr.sock}; \
@@ -47,6 +48,7 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
( cd asr-engines/scriberr-asr-onnx && uv sync ) || true; \
fi; \
( cd asr-engines/scriberr-asr-onnx && uv run asr-engine-server --socket $$ASR_ENGINE_SOCKET ) & \
pids="$$pids $$!"; \
else \
echo "⚠️ 'uv' not found. ASR engine will not start. Install uv or run make asr-engine-dev separately."; \
fi; \
@@ -54,13 +56,16 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
if [ "$$USE_GO_RUN" = true ]; then \
echo "🔧 Starting Go backend (standard run)..."; \
ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" go run cmd/server/main.go & \
pids="$$pids $$!"; \
else \
echo "🔥 Starting Go backend (with Air live reload)..."; \
ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" air & \
pids="$$pids $$!"; \
fi; \
\
echo "⚛️ Starting React frontend (Vite)..."; \
cd web/frontend && npm run dev & \
pids="$$pids $$!"; \
\
wait

View File

@@ -192,7 +192,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on
> [!IMPORTANT]
> **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host.
>
> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors.
> **HTTP vs HTTPS:** Scriberr now **auto-detects** secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure).
#### Standard Deployment (CPU)
@@ -215,7 +215,8 @@ services:
- APP_ENV=production # DO NOT CHANGE THIS
# CORS: comma-separated list of allowed origins for production
# - ALLOWED_ORIGINS=https://your-domain.com
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
restart: unless-stopped
volumes:
@@ -262,7 +263,8 @@ services:
- APP_ENV=production # DO NOT CHANGE THIS
# CORS: comma-separated list of allowed origins for production
# - ALLOWED_ORIGINS=https://your-domain.com
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
volumes:
scriberr_data: {}
@@ -358,13 +360,14 @@ Replace `1000` with the value you set for `PUID`/`PGID` (default is `1000`).
If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag.
By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections.
By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers.
**Solutions:**
- **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS).
- **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`:
```yaml
environment:
- TRUST_PROXY_HEADERS=true
- SECURE_COOKIES=false
```

View File

@@ -8,6 +8,7 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
@@ -76,6 +77,11 @@ func main() {
// Load configuration
logger.Startup("config", "Loading configuration")
cfg := config.Load()
if strings.EqualFold(cfg.SecureCookiesMode, "false") {
logger.Warn("Secure cookies disabled; auth cookies will be sent over HTTP", "env", cfg.Environment)
} else if strings.EqualFold(cfg.SecureCookiesMode, "auto") && !cfg.TrustProxyHeaders {
logger.Warn("Secure cookies auto-detect without proxy headers; HTTPS required for auth cookies", "env", cfg.Environment)
}
// Register adapters with config-based paths
registerAdapters(cfg)

63
internal/api/cookies.go Normal file
View File

@@ -0,0 +1,63 @@
package api
import (
"strings"
"github.com/gin-gonic/gin"
)
func (h *Handler) cookieSecure(c *gin.Context) bool {
switch strings.ToLower(h.config.SecureCookiesMode) {
case "true", "force", "secure":
return true
case "false", "off", "insecure":
return false
default:
return requestIsSecure(c, h.config.TrustProxyHeaders)
}
}
func requestIsSecure(c *gin.Context, trustProxy bool) bool {
if c.Request.TLS != nil {
return true
}
if !trustProxy {
return false
}
if proto := firstForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" {
return strings.EqualFold(proto, "https")
}
if forwarded := c.GetHeader("Forwarded"); forwarded != "" {
if proto := forwardedProto(forwarded); proto != "" {
return strings.EqualFold(proto, "https")
}
}
return false
}
func firstForwardedProto(value string) string {
if value == "" {
return ""
}
parts := strings.Split(value, ",")
return strings.TrimSpace(parts[0])
}
func forwardedProto(header string) string {
// Example: Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
parts := strings.Split(header, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
if strings.EqualFold(strings.TrimSpace(kv[0]), "proto") {
return strings.Trim(strings.TrimSpace(kv[1]), "\"")
}
}
return ""
}

View File

@@ -1567,7 +1567,7 @@ func (h *Handler) Login(c *gin.Context) {
Path: "/",
Expires: time.Now().Add(24 * time.Hour), // Match your token duration constant
HttpOnly: true,
Secure: h.config.SecureCookies, // Use explicit secure flag
Secure: h.cookieSecure(c),
SameSite: http.SameSiteLaxMode,
})
@@ -1600,7 +1600,7 @@ func (h *Handler) Logout(c *gin.Context) {
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.config.SecureCookies,
Secure: h.cookieSecure(c),
})
// Also clear access token
http.SetCookie(c.Writer, &http.Cookie{
@@ -1611,7 +1611,7 @@ func (h *Handler) Logout(c *gin.Context) {
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.config.SecureCookies,
Secure: h.cookieSecure(c),
})
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
@@ -1753,7 +1753,7 @@ func (h *Handler) Refresh(c *gin.Context) {
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
Secure: h.config.SecureCookies,
Secure: h.cookieSecure(c),
SameSite: http.SameSiteLaxMode,
})
@@ -1781,7 +1781,7 @@ func (h *Handler) issueRefreshToken(c *gin.Context, userID uint) error {
MaxAge: int((14 * 24 * time.Hour).Seconds()),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.config.SecureCookies,
Secure: h.cookieSecure(c),
})
return nil
}

View File

@@ -33,9 +33,10 @@ type Config struct {
WhisperXEnv string
// Environment configuration
Environment string
AllowedOrigins []string
SecureCookies bool // Explicit control over Secure flag (for HTTPS deployments)
Environment string
AllowedOrigins []string
SecureCookiesMode string // "auto" (default), "true"/"false"
TrustProxyHeaders bool // Whether to trust X-Forwarded-Proto/Forwarded
// OpenAI configuration
OpenAIAPIKey string
@@ -50,26 +51,26 @@ func Load() *Config {
logger.Debug("No .env file found, using system environment variables")
}
// Default SecureCookies to true in production, false otherwise
defaultSecure := "false"
if strings.ToLower(getEnv("APP_ENV", "development")) == "production" {
defaultSecure = "true"
defaultSecureMode := strings.ToLower(getEnv("SECURE_COOKIES", "auto"))
if defaultSecureMode == "" {
defaultSecureMode = "auto"
}
return &Config{
Port: getEnv("PORT", "8080"),
Host: getEnv("HOST", "0.0.0.0"),
Environment: getEnv("APP_ENV", "development"),
AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","),
DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"),
JWTSecret: getJWTSecret(),
UploadDir: getEnv("UPLOAD_DIR", "data/uploads"),
TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"),
TempDir: getEnv("TEMP_DIR", "data/temp"),
WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"),
SecureCookies: getEnv("SECURE_COOKIES", defaultSecure) == "true",
OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""),
HFToken: getEnv("HF_TOKEN", ""),
Port: getEnv("PORT", "8080"),
Host: getEnv("HOST", "0.0.0.0"),
Environment: getEnv("APP_ENV", "development"),
AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","),
DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"),
JWTSecret: getJWTSecret(),
UploadDir: getEnv("UPLOAD_DIR", "data/uploads"),
TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"),
TempDir: getEnv("TEMP_DIR", "data/temp"),
WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"),
SecureCookiesMode: defaultSecureMode,
TrustProxyHeaders: strings.ToLower(getEnv("TRUST_PROXY_HEADERS", "true")) == "true",
OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""),
HFToken: getEnv("HF_TOKEN", ""),
}
}

View File

@@ -79,7 +79,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on
> **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host.
>
> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors.
> **HTTP vs HTTPS:** Scriberr auto-detects secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure).
### Standard Deployment (CPU)
@@ -102,7 +102,8 @@ services:
- APP_ENV=production # DO NOT CHANGE THIS
# CORS: comma-separated list of allowed origins for production
# - ALLOWED_ORIGINS=https://your-domain.com
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
restart: unless-stopped
volumes:
@@ -149,7 +150,8 @@ services:
- APP_ENV=production # DO NOT CHANGE THIS
# CORS: comma-separated list of allowed origins for production
# - ALLOWED_ORIGINS=https://your-domain.com
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
volumes:
scriberr_data: {}

View File

@@ -23,12 +23,13 @@ Replace `10001` with the value you set for `PUID`/`PGID` (default is `1000`).
If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag.
By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections.
By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers.
### Solutions:
- **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS).
- **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`:
```yaml
environment:
- TRUST_PROXY_HEADERS=true
- SECURE_COOKIES=false
```