feat/security #2
@@ -22,6 +22,9 @@ func TestDefaults(t *testing.T) {
|
|||||||
if !cfg.GuestUploadsEnabled || !cfg.APIEnabled || !cfg.ZipDownloadsEnabled || !cfg.OneTimeDownloadsEnabled {
|
if !cfg.GuestUploadsEnabled || !cfg.APIEnabled || !cfg.ZipDownloadsEnabled || !cfg.OneTimeDownloadsEnabled {
|
||||||
t.Fatal("expected default guest/API/download toggles to be enabled")
|
t.Fatal("expected default guest/API/download toggles to be enabled")
|
||||||
}
|
}
|
||||||
|
if !cfg.SecurityEnabled {
|
||||||
|
t.Fatal("expected security features to be enabled by default")
|
||||||
|
}
|
||||||
if cfg.AdminUsername != "admin" {
|
if cfg.AdminUsername != "admin" {
|
||||||
t.Fatalf("unexpected admin username: %s", cfg.AdminUsername)
|
t.Fatalf("unexpected admin username: %s", cfg.AdminUsername)
|
||||||
}
|
}
|
||||||
@@ -39,6 +42,7 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000")
|
t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000")
|
||||||
t.Setenv("WARPBOX_ADMIN_USERNAME", "root")
|
t.Setenv("WARPBOX_ADMIN_USERNAME", "root")
|
||||||
t.Setenv("WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "true")
|
t.Setenv("WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "true")
|
||||||
|
t.Setenv("WARPBOX_SECURITY_ENABLED", "false")
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,6 +67,9 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
if !cfg.OneTimeDownloadRetryOnFailure {
|
if !cfg.OneTimeDownloadRetryOnFailure {
|
||||||
t.Fatal("expected one-time retry-on-failure env override to be applied")
|
t.Fatal("expected one-time retry-on-failure env override to be applied")
|
||||||
}
|
}
|
||||||
|
if cfg.SecurityEnabled {
|
||||||
|
t.Fatal("expected security features toggle from environment to be applied")
|
||||||
|
}
|
||||||
if cfg.Source(SettingAPIEnabled) != SourceEnv {
|
if cfg.Source(SettingAPIEnabled) != SourceEnv {
|
||||||
t.Fatalf("expected API setting source to be env, got %s", cfg.Source(SettingAPIEnabled))
|
t.Fatalf("expected API setting source to be env, got %s", cfg.Source(SettingAPIEnabled))
|
||||||
}
|
}
|
||||||
@@ -191,6 +198,7 @@ func clearConfigEnv(t *testing.T) {
|
|||||||
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
||||||
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
||||||
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
||||||
|
"WARPBOX_SECURITY_ENABLED",
|
||||||
} {
|
} {
|
||||||
t.Setenv(name, "")
|
t.Setenv(name, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var Definitions = []SettingDefinition{
|
|||||||
{Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
{Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
||||||
{Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
{Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
||||||
{Key: SettingActivityRetentionSeconds, EnvName: "WARPBOX_ACTIVITY_RETENTION_SECONDS", Label: "Activity retention seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60},
|
{Key: SettingActivityRetentionSeconds, EnvName: "WARPBOX_ACTIVITY_RETENTION_SECONDS", Label: "Activity retention seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60},
|
||||||
|
{Key: SettingSecurityEnabled, EnvName: "WARPBOX_SECURITY_ENABLED", Label: "Security features enabled", Type: SettingTypeBool, Editable: true},
|
||||||
{Key: SettingSecurityIPWhitelist, EnvName: "WARPBOX_SECURITY_IP_WHITELIST", Label: "Security IP whitelist", Type: SettingTypeText, Editable: true},
|
{Key: SettingSecurityIPWhitelist, EnvName: "WARPBOX_SECURITY_IP_WHITELIST", Label: "Security IP whitelist", Type: SettingTypeText, Editable: true},
|
||||||
{Key: SettingSecurityAdminIPWhitelist, EnvName: "WARPBOX_SECURITY_ADMIN_IP_WHITELIST", Label: "Security admin IP whitelist", Type: SettingTypeText, Editable: true},
|
{Key: SettingSecurityAdminIPWhitelist, EnvName: "WARPBOX_SECURITY_ADMIN_IP_WHITELIST", Label: "Security admin IP whitelist", Type: SettingTypeText, Editable: true},
|
||||||
{Key: SettingTrustedProxyCIDRs, EnvName: "WARPBOX_TRUSTED_PROXY_CIDRS", Label: "Trusted proxy CIDRs", Type: SettingTypeText, Editable: true},
|
{Key: SettingTrustedProxyCIDRs, EnvName: "WARPBOX_TRUSTED_PROXY_CIDRS", Label: "Trusted proxy CIDRs", Type: SettingTypeText, Editable: true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func Load() (*Config, error) {
|
|||||||
ThumbnailBatchSize: 10,
|
ThumbnailBatchSize: 10,
|
||||||
ThumbnailIntervalSeconds: 30,
|
ThumbnailIntervalSeconds: 30,
|
||||||
ActivityRetentionSeconds: 7 * 24 * 60 * 60,
|
ActivityRetentionSeconds: 7 * 24 * 60 * 60,
|
||||||
|
SecurityEnabled: true,
|
||||||
SecurityLoginWindowSeconds: 10 * 60,
|
SecurityLoginWindowSeconds: 10 * 60,
|
||||||
SecurityLoginMaxAttempts: 8,
|
SecurityLoginMaxAttempts: 8,
|
||||||
SecurityBanSeconds: 30 * 60,
|
SecurityBanSeconds: 30 * 60,
|
||||||
@@ -91,6 +92,7 @@ func Load() (*Config, error) {
|
|||||||
{SettingOneTimeDownloadRetryFail, "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", &cfg.OneTimeDownloadRetryOnFailure},
|
{SettingOneTimeDownloadRetryFail, "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", &cfg.OneTimeDownloadRetryOnFailure},
|
||||||
{SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled},
|
{SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled},
|
||||||
{SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled},
|
{SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled},
|
||||||
|
{SettingSecurityEnabled, "WARPBOX_SECURITY_ENABLED", &cfg.SecurityEnabled},
|
||||||
}
|
}
|
||||||
for _, item := range envBools {
|
for _, item := range envBools {
|
||||||
if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil {
|
if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil {
|
||||||
@@ -209,6 +211,7 @@ func (cfg *Config) captureDefaults() {
|
|||||||
cfg.captureDefaultValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize))
|
cfg.captureDefaultValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize))
|
||||||
cfg.captureDefaultValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds))
|
cfg.captureDefaultValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds))
|
||||||
cfg.captureDefaultValue(SettingActivityRetentionSeconds, strconv.FormatInt(cfg.ActivityRetentionSeconds, 10))
|
cfg.captureDefaultValue(SettingActivityRetentionSeconds, strconv.FormatInt(cfg.ActivityRetentionSeconds, 10))
|
||||||
|
cfg.captureDefaultValue(SettingSecurityEnabled, formatBool(cfg.SecurityEnabled))
|
||||||
cfg.captureDefaultValue(SettingSecurityIPWhitelist, cfg.SecurityIPWhitelist)
|
cfg.captureDefaultValue(SettingSecurityIPWhitelist, cfg.SecurityIPWhitelist)
|
||||||
cfg.captureDefaultValue(SettingSecurityAdminIPWhitelist, cfg.SecurityAdminIPWhitelist)
|
cfg.captureDefaultValue(SettingSecurityAdminIPWhitelist, cfg.SecurityAdminIPWhitelist)
|
||||||
cfg.captureDefaultValue(SettingTrustedProxyCIDRs, cfg.TrustedProxyCIDRs)
|
cfg.captureDefaultValue(SettingTrustedProxyCIDRs, cfg.TrustedProxyCIDRs)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const (
|
|||||||
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
|
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
|
||||||
SettingDataDir = "data_dir"
|
SettingDataDir = "data_dir"
|
||||||
SettingActivityRetentionSeconds = "activity_retention_seconds"
|
SettingActivityRetentionSeconds = "activity_retention_seconds"
|
||||||
|
SettingSecurityEnabled = "security_enabled"
|
||||||
SettingSecurityIPWhitelist = "security_ip_whitelist"
|
SettingSecurityIPWhitelist = "security_ip_whitelist"
|
||||||
SettingSecurityAdminIPWhitelist = "security_admin_ip_whitelist"
|
SettingSecurityAdminIPWhitelist = "security_admin_ip_whitelist"
|
||||||
SettingTrustedProxyCIDRs = "trusted_proxy_cidrs"
|
SettingTrustedProxyCIDRs = "trusted_proxy_cidrs"
|
||||||
@@ -108,6 +109,7 @@ type Config struct {
|
|||||||
ThumbnailBatchSize int
|
ThumbnailBatchSize int
|
||||||
ThumbnailIntervalSeconds int
|
ThumbnailIntervalSeconds int
|
||||||
ActivityRetentionSeconds int64
|
ActivityRetentionSeconds int64
|
||||||
|
SecurityEnabled bool
|
||||||
SecurityIPWhitelist string
|
SecurityIPWhitelist string
|
||||||
SecurityAdminIPWhitelist string
|
SecurityAdminIPWhitelist string
|
||||||
TrustedProxyCIDRs string
|
TrustedProxyCIDRs string
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ func (cfg *Config) assignBool(key string, value bool, source Source) {
|
|||||||
cfg.RenewOnAccessEnabled = value
|
cfg.RenewOnAccessEnabled = value
|
||||||
case SettingRenewOnDownloadEnabled:
|
case SettingRenewOnDownloadEnabled:
|
||||||
cfg.RenewOnDownloadEnabled = value
|
cfg.RenewOnDownloadEnabled = value
|
||||||
|
case SettingSecurityEnabled:
|
||||||
|
cfg.SecurityEnabled = value
|
||||||
}
|
}
|
||||||
cfg.setValue(key, formatBool(value), source)
|
cfg.setValue(key, formatBool(value), source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ func (app *App) handleAdminLoginPost(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
ip := app.clientIP(ctx)
|
ip := app.clientIP(ctx)
|
||||||
guard := app.securityGuard
|
guard := app.securityGuard
|
||||||
if guard == nil {
|
if app.securityFeaturesEnabled() && guard == nil {
|
||||||
guard = security.NewGuard()
|
guard = security.NewGuard()
|
||||||
app.securityGuard = guard
|
app.securityGuard = guard
|
||||||
}
|
}
|
||||||
if !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) {
|
if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) {
|
||||||
app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil)
|
app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil)
|
||||||
ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{
|
ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{
|
||||||
"ErrorMessage": "Too many failed attempts. Try again later.",
|
"ErrorMessage": "Too many failed attempts. Try again later.",
|
||||||
@@ -80,7 +80,7 @@ func (app *App) handleAdminLoginPost(ctx *gin.Context) {
|
|||||||
password := ctx.PostForm("password")
|
password := ctx.PostForm("password")
|
||||||
|
|
||||||
if username != app.config.AdminUsername || password != app.config.AdminPassword {
|
if username != app.config.AdminUsername || password != app.config.AdminPassword {
|
||||||
if !guard.IsAdminWhitelisted(ip) {
|
if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) {
|
||||||
banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds)
|
banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds)
|
||||||
app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)})
|
app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)})
|
||||||
if banned {
|
if banned {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -27,14 +28,24 @@ type adminSecurityActionRequest struct {
|
|||||||
BanUntil string `json:"ban_until"`
|
BanUntil string `json:"ban_until"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) reloadSecurityConfig() {
|
func (app *App) reloadSecurityConfig() error {
|
||||||
|
if app == nil || app.config == nil {
|
||||||
|
return fmt.Errorf("app or config is nil")
|
||||||
|
}
|
||||||
|
if !app.securityFeaturesEnabled() {
|
||||||
|
if app.securityGuard != nil {
|
||||||
|
_ = app.securityGuard.Close()
|
||||||
|
}
|
||||||
|
app.securityGuard = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if app.securityGuard == nil {
|
if app.securityGuard == nil {
|
||||||
app.securityGuard = security.NewGuard()
|
app.securityGuard = security.NewGuard()
|
||||||
}
|
}
|
||||||
if app.config != nil {
|
if err := app.securityGuard.EnableBanPersistence(filepath.Join(app.config.DBDir, "bans.badger")); err != nil {
|
||||||
_ = app.securityGuard.EnableBanPersistence(filepath.Join(app.config.DBDir, "bans.badger"))
|
return fmt.Errorf("enable ban persistence: %w", err)
|
||||||
}
|
}
|
||||||
_ = app.securityGuard.Reload(security.Config{
|
if err := app.securityGuard.Reload(security.Config{
|
||||||
IPWhitelist: app.config.SecurityIPWhitelist,
|
IPWhitelist: app.config.SecurityIPWhitelist,
|
||||||
AdminIPWhitelist: app.config.SecurityAdminIPWhitelist,
|
AdminIPWhitelist: app.config.SecurityAdminIPWhitelist,
|
||||||
LoginWindowSeconds: app.config.SecurityLoginWindowSeconds,
|
LoginWindowSeconds: app.config.SecurityLoginWindowSeconds,
|
||||||
@@ -45,7 +56,14 @@ func (app *App) reloadSecurityConfig() {
|
|||||||
UploadWindowSeconds: app.config.SecurityUploadWindowSeconds,
|
UploadWindowSeconds: app.config.SecurityUploadWindowSeconds,
|
||||||
UploadMaxRequests: app.config.SecurityUploadMaxRequests,
|
UploadMaxRequests: app.config.SecurityUploadMaxRequests,
|
||||||
UploadMaxBytes: app.config.SecurityUploadMaxBytes,
|
UploadMaxBytes: app.config.SecurityUploadMaxBytes,
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("reload guard config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) securityFeaturesEnabled() bool {
|
||||||
|
return app != nil && app.config != nil && app.config.SecurityEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) logActivity(kind string, severity string, message string, ctx *gin.Context, meta map[string]string) {
|
func (app *App) logActivity(kind string, severity string, message string, ctx *gin.Context, meta map[string]string) {
|
||||||
@@ -85,6 +103,10 @@ func (app *App) createAlert(title string, severity string, group string, code st
|
|||||||
|
|
||||||
func (app *App) securityMiddleware() gin.HandlerFunc {
|
func (app *App) securityMiddleware() gin.HandlerFunc {
|
||||||
return func(ctx *gin.Context) {
|
return func(ctx *gin.Context) {
|
||||||
|
if !app.securityFeaturesEnabled() {
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
if app.securityGuard == nil {
|
if app.securityGuard == nil {
|
||||||
ctx.Next()
|
ctx.Next()
|
||||||
return
|
return
|
||||||
@@ -104,6 +126,10 @@ func (app *App) securityMiddleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) handleNoRoute(ctx *gin.Context) {
|
func (app *App) handleNoRoute(ctx *gin.Context) {
|
||||||
|
if !app.securityFeaturesEnabled() {
|
||||||
|
ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if app.securityGuard == nil {
|
if app.securityGuard == nil {
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
|
ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
|
||||||
return
|
return
|
||||||
@@ -148,6 +174,10 @@ func (app *App) handleAdminActivity(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) handleAdminSecurity(ctx *gin.Context) {
|
func (app *App) handleAdminSecurity(ctx *gin.Context) {
|
||||||
|
if !app.securityFeaturesEnabled() {
|
||||||
|
ctx.String(http.StatusNotFound, "Security features are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
events := []activity.Event{}
|
events := []activity.Event{}
|
||||||
alertsList := []alerts.Alert{}
|
alertsList := []alerts.Alert{}
|
||||||
if app.activityStore != nil {
|
if app.activityStore != nil {
|
||||||
@@ -215,6 +245,10 @@ func (app *App) recordManualBanAction(ctx *gin.Context, kind string, message str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) handleAdminSecurityAction(ctx *gin.Context) {
|
func (app *App) handleAdminSecurityAction(ctx *gin.Context) {
|
||||||
|
if !app.securityFeaturesEnabled() {
|
||||||
|
ctx.JSON(http.StatusNotFound, gin.H{"error": "Security features are disabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if app.securityGuard == nil {
|
if app.securityGuard == nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Security guard unavailable"})
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Security guard unavailable"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ func setupAdminSecurityTest(t *testing.T) (*App, *gin.Engine) {
|
|||||||
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
|
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
|
||||||
securityGuard: security.NewGuard(),
|
securityGuard: security.NewGuard(),
|
||||||
}
|
}
|
||||||
app.reloadSecurityConfig()
|
if err := app.reloadSecurityConfig(); err != nil {
|
||||||
|
t.Fatalf("reload security config: %v", err)
|
||||||
|
}
|
||||||
t.Cleanup(func() { _ = app.securityGuard.Close() })
|
t.Cleanup(func() { _ = app.securityGuard.Close() })
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|||||||
@@ -269,7 +269,9 @@ func (app *App) applySettingsOverrideSet(values map[string]string) ([]adminSetti
|
|||||||
|
|
||||||
app.config = nextCfg
|
app.config = nextCfg
|
||||||
applyBoxstoreRuntimeConfig(app.config)
|
applyBoxstoreRuntimeConfig(app.config)
|
||||||
app.reloadSecurityConfig()
|
if err := app.reloadSecurityConfig(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
rows, _ := app.buildAdminSettingsRows()
|
rows, _ := app.buildAdminSettingsRows()
|
||||||
return rows, warnings, nil
|
return rows, warnings, nil
|
||||||
}
|
}
|
||||||
@@ -437,7 +439,7 @@ func settingsCategoryForKey(key string) string {
|
|||||||
return "downloads"
|
return "downloads"
|
||||||
case config.SettingRenewOnAccessEnabled, config.SettingDefaultGuestExpirySecs, config.SettingMaxGuestExpirySecs, config.SettingOneTimeDownloadRetryFail:
|
case config.SettingRenewOnAccessEnabled, config.SettingDefaultGuestExpirySecs, config.SettingMaxGuestExpirySecs, config.SettingOneTimeDownloadRetryFail:
|
||||||
return "retention"
|
return "retention"
|
||||||
case config.SettingSecurityIPWhitelist, config.SettingSecurityAdminIPWhitelist, config.SettingSecurityLoginWindowSecs, config.SettingSecurityLoginMaxAttempts, config.SettingSecurityBanSeconds, config.SettingSecurityScanWindowSecs, config.SettingSecurityScanMaxAttempts:
|
case config.SettingSecurityEnabled, config.SettingSecurityIPWhitelist, config.SettingSecurityAdminIPWhitelist, config.SettingSecurityLoginWindowSecs, config.SettingSecurityLoginMaxAttempts, config.SettingSecurityBanSeconds, config.SettingSecurityScanWindowSecs, config.SettingSecurityScanMaxAttempts:
|
||||||
return "security"
|
return "security"
|
||||||
case config.SettingActivityRetentionSeconds:
|
case config.SettingActivityRetentionSeconds:
|
||||||
return "activity"
|
return "activity"
|
||||||
@@ -476,6 +478,7 @@ func settingsDescription(key string) string {
|
|||||||
config.SettingThumbnailIntervalSeconds: "Delay between thumbnail worker passes.",
|
config.SettingThumbnailIntervalSeconds: "Delay between thumbnail worker passes.",
|
||||||
config.SettingDataDir: "Root data path. Locked because moving storage roots live is risky.",
|
config.SettingDataDir: "Root data path. Locked because moving storage roots live is risky.",
|
||||||
config.SettingActivityRetentionSeconds: "How long activity events stay stored before automatic prune.",
|
config.SettingActivityRetentionSeconds: "How long activity events stay stored before automatic prune.",
|
||||||
|
config.SettingSecurityEnabled: "Master switch for security middleware, automated bans, suspicious path detection, and upload throttling.",
|
||||||
config.SettingSecurityIPWhitelist: "Comma-separated IPs that bypass generic security bans and rate-limits.",
|
config.SettingSecurityIPWhitelist: "Comma-separated IPs that bypass generic security bans and rate-limits.",
|
||||||
config.SettingSecurityAdminIPWhitelist: "Comma-separated IPs allowed to bypass admin login brute-force controls.",
|
config.SettingSecurityAdminIPWhitelist: "Comma-separated IPs allowed to bypass admin login brute-force controls.",
|
||||||
config.SettingSecurityLoginWindowSecs: "Window used for failed admin login counting.",
|
config.SettingSecurityLoginWindowSecs: "Window used for failed admin login counting.",
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ func clearAdminSettingsEnv(t *testing.T) {
|
|||||||
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
||||||
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
||||||
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
||||||
|
"WARPBOX_SECURITY_ENABLED",
|
||||||
"WARPBOX_SECURITY_IP_WHITELIST",
|
"WARPBOX_SECURITY_IP_WHITELIST",
|
||||||
"WARPBOX_SECURITY_ADMIN_IP_WHITELIST",
|
"WARPBOX_SECURITY_ADMIN_IP_WHITELIST",
|
||||||
"WARPBOX_TRUSTED_PROXY_CIDRS",
|
"WARPBOX_TRUSTED_PROXY_CIDRS",
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func Run(addr string) error {
|
|||||||
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
|
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
|
||||||
securityGuard: security.NewGuard(),
|
securityGuard: security.NewGuard(),
|
||||||
}
|
}
|
||||||
app.reloadSecurityConfig()
|
if err := app.reloadSecurityConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.Use(app.securityMiddleware())
|
router.Use(app.securityMiddleware())
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ func (app *App) maxRequestBodyBytes() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) enforceUploadRateLimit(ctx *gin.Context, size int64) bool {
|
func (app *App) enforceUploadRateLimit(ctx *gin.Context, size int64) bool {
|
||||||
|
if !app.securityFeaturesEnabled() || app.securityGuard == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
ip := app.clientIP(ctx)
|
ip := app.clientIP(ctx)
|
||||||
if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) {
|
if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
1
run.sh
1
run.sh
@@ -26,6 +26,7 @@ export WARPBOX_BOX_POLL_INTERVAL_MS="${WARPBOX_BOX_POLL_INTERVAL_MS:-5000}"
|
|||||||
export WARPBOX_THUMBNAIL_BATCH_SIZE="${WARPBOX_THUMBNAIL_BATCH_SIZE:-10}"
|
export WARPBOX_THUMBNAIL_BATCH_SIZE="${WARPBOX_THUMBNAIL_BATCH_SIZE:-10}"
|
||||||
export WARPBOX_THUMBNAIL_INTERVAL_SECONDS="${WARPBOX_THUMBNAIL_INTERVAL_SECONDS:-30}"
|
export WARPBOX_THUMBNAIL_INTERVAL_SECONDS="${WARPBOX_THUMBNAIL_INTERVAL_SECONDS:-30}"
|
||||||
export WARPBOX_ACTIVITY_RETENTION_SECONDS="${WARPBOX_ACTIVITY_RETENTION_SECONDS:-604800}"
|
export WARPBOX_ACTIVITY_RETENTION_SECONDS="${WARPBOX_ACTIVITY_RETENTION_SECONDS:-604800}"
|
||||||
|
export WARPBOX_SECURITY_ENABLED="${WARPBOX_SECURITY_ENABLED:-true}"
|
||||||
export WARPBOX_SECURITY_IP_WHITELIST="${WARPBOX_SECURITY_IP_WHITELIST:-}"
|
export WARPBOX_SECURITY_IP_WHITELIST="${WARPBOX_SECURITY_IP_WHITELIST:-}"
|
||||||
export WARPBOX_SECURITY_ADMIN_IP_WHITELIST="${WARPBOX_SECURITY_ADMIN_IP_WHITELIST:-}"
|
export WARPBOX_SECURITY_ADMIN_IP_WHITELIST="${WARPBOX_SECURITY_ADMIN_IP_WHITELIST:-}"
|
||||||
export WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS="${WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS:-600}"
|
export WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS="${WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS:-600}"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
if (!dataNode || !body || !searchInput) return;
|
if (!dataNode || !body || !searchInput) return;
|
||||||
const events = parseData();
|
const events = parseData();
|
||||||
|
const initialQuery = new URLSearchParams(window.location.search).get("q");
|
||||||
|
if (initialQuery) searchInput.value = initialQuery;
|
||||||
|
|
||||||
function parseData() {
|
function parseData() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
selected: new Set(),
|
selected: new Set(),
|
||||||
activeID: null
|
activeID: null
|
||||||
};
|
};
|
||||||
|
const initialQuery = new URLSearchParams(window.location.search).get("q");
|
||||||
|
if (initialQuery) searchInput.value = initialQuery;
|
||||||
|
|
||||||
function parseData() {
|
function parseData() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user