fix(security): phase 1 critical fixes for public release
- Redact PostgreSQL DSN password from stdout/logs - Harden .dockerignore to exclude .ssh/, .claude/, *.db, *.local files - SSRF protection: block private/loopback/link-local IPs by default (UPTOP_ALLOW_PRIVATE_TARGETS=true to override for homelab use) - Fix email header injection via CRLF in monitor names - AES-256-GCM encryption for alert credentials at rest (UPTOP_ENCRYPTION_KEY env var, migrate-secrets subcommand) - TLS support for HTTP server (UPTOP_TLS_CERT/UPTOP_TLS_KEY) with HSTS header when TLS enabled
This commit is contained in:
@@ -3,14 +3,15 @@ package cluster
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
||||
)
|
||||
|
||||
// --- Mock Store (minimal, for monitor.NewEngine) ---
|
||||
@@ -295,7 +296,7 @@ func TestProbeExecuteChecks(t *testing.T) {
|
||||
|
||||
strict := &http.Client{}
|
||||
insecure := &http.Client{}
|
||||
results := probeExecuteChecks(context.Background(), sites, strict, insecure)
|
||||
results := probeExecuteChecks(context.Background(), sites, strict, insecure, true)
|
||||
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 results, got %d", len(results))
|
||||
@@ -329,7 +330,7 @@ func TestProbeExecuteChecks_Concurrency(t *testing.T) {
|
||||
sites = append(sites, models.Site{ID: i + 1, Type: "http", URL: srv.URL})
|
||||
}
|
||||
|
||||
results := probeExecuteChecks(context.Background(), sites, &http.Client{}, &http.Client{})
|
||||
results := probeExecuteChecks(context.Background(), sites, &http.Client{}, &http.Client{}, true)
|
||||
if len(results) != 20 {
|
||||
t.Errorf("expected 20 results, got %d", len(results))
|
||||
}
|
||||
|
||||
+22
-13
@@ -6,21 +6,23 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
||||
)
|
||||
|
||||
type ProbeConfig struct {
|
||||
NodeID string
|
||||
NodeName string
|
||||
Region string
|
||||
LeaderURL string
|
||||
SharedKey string
|
||||
Interval int
|
||||
NodeID string
|
||||
NodeName string
|
||||
Region string
|
||||
LeaderURL string
|
||||
SharedKey string
|
||||
Interval int
|
||||
AllowPrivateTargets bool
|
||||
}
|
||||
|
||||
func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
||||
@@ -29,11 +31,18 @@ func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
||||
}
|
||||
|
||||
apiClient := &http.Client{Timeout: 10 * time.Second}
|
||||
dial := monitor.SafeDialContext(cfg.AllowPrivateTargets)
|
||||
strictClient := &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
||||
DialContext: dial,
|
||||
},
|
||||
}
|
||||
insecureClient := &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, //nolint:gosec // intentional for IgnoreTLS sites
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // intentional for IgnoreTLS sites
|
||||
DialContext: dial,
|
||||
},
|
||||
}
|
||||
|
||||
if err := probeRegister(ctx, apiClient, cfg); err != nil {
|
||||
@@ -59,7 +68,7 @@ func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
||||
continue
|
||||
}
|
||||
|
||||
results := probeExecuteChecks(ctx, sites, strictClient, insecureClient)
|
||||
results := probeExecuteChecks(ctx, sites, strictClient, insecureClient, cfg.AllowPrivateTargets)
|
||||
|
||||
if len(results) > 0 {
|
||||
if err := probeReportResults(ctx, apiClient, cfg, results); err != nil {
|
||||
@@ -121,7 +130,7 @@ type probeResultItem struct {
|
||||
IsUp bool `json:"is_up"`
|
||||
}
|
||||
|
||||
func probeExecuteChecks(ctx context.Context, sites []models.Site, strict, insecure *http.Client) []probeResultItem {
|
||||
func probeExecuteChecks(ctx context.Context, sites []models.Site, strict, insecure *http.Client, allowPrivate bool) []probeResultItem {
|
||||
var mu sync.Mutex
|
||||
var results []probeResultItem
|
||||
sem := make(chan struct{}, 10)
|
||||
@@ -140,7 +149,7 @@ loop:
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
cr := monitor.RunCheck(s, strict, insecure, false)
|
||||
cr := monitor.RunCheck(s, strict, insecure, false, allowPrivate)
|
||||
mu.Lock()
|
||||
results = append(results, probeResultItem{
|
||||
SiteID: s.ID,
|
||||
|
||||
Reference in New Issue
Block a user