@@ -14,9 +14,8 @@
1414-- ============================================
1515
1616-- Server identity and federation credentials
17−serverHost = "localhost:8080"
17+serverHost = "localhost"
1818listenPort = 8080
19−maintenanceSecret = "" -- set to enable /maintenance/* and /stats endpoints
2019federationScheme = "https://" -- scheme for federation HTTP calls
2120-- Ed25519 federation signing keys (hex-encoded). When both are set, outbound federation
2221-- requests are signed instead of using verify callbacks (eliminates the round-trip).
@@ -946,9 +945,8 @@ maxPeerLatencyEntries = 100
946945*droppedDeliveryReceipts : [{senderKey: Text, count: Int, lastUpdated: Timestamp}]
947946*droppedExpirationNotifs : [{senderKey: Text, count: Int, lastUpdated: Timestamp}]
948947
949−-- Runtime maintenance secret: when non-empty, used instead of compile-time maintenanceSecret.
950−-- Set via /maintenance/rotate-secret to enable zero-downtime secret rotation.
951−-- Empty = use compile-time maintenanceSecret (default).
948+-- Maintenance secret: set via /maintenance/rotate-secret at startup.
949+-- When empty, maintenance and stats endpoints are disabled.
952950*runtimeMaintenanceSecret : Text
953951
954952-- Runtime-configurable rate limit overrides (0 = use compile-time default).
@@ -1833,14 +1831,13 @@ verifyAuthWith = \check authorization ->
18331831 Nothing {} -> False {}
18341832 Just {value: token} -> check token
18351833
1836−-- Check maintenance auth: tries runtime secret first (if set), then compile-time secret.
1837−-- This allows zero-downtime rotation: set the new secret via /maintenance/rotate-secret,
1838−-- then update the compile-time config and restart at convenience.
1834+-- Check maintenance auth against the runtime secret set via /maintenance/rotate-secret.
1835+-- Returns False when no secret has been set (maintenance endpoints disabled).
18391836verifyMaintenanceAuth : Text -> IO Bool
18401837verifyMaintenanceAuth = \authorization -> do
1841− runtimeSecret <- *runtimeMaintenanceSecret
1842− let checkSecret = if runtimeSecret != "" then runtimeSecret else maintenanceSecret
1843− yield (verifyAuthWith (\token -> verifySecret token checkSecret) authorization)
1838+ secret <- *runtimeMaintenanceSecret
1839+ if secret == "" then yield False
1840+ else yield (verifyAuthWith (\token -> verifySecret token secret) authorization)
18441841
18451842-- Check if a server is on the blocklist
18461843isBlockedPeer : Text -> [Text] -> Bool
@@ -3471,7 +3468,6 @@ dedupMessageIds = \messageIds ->
34713468 if any acc (\a -> a == mid) then acc
34723469 else union [mid] acc) [] capped
34733470
3474−
34753471-- Constant-time comparison iteration count. Must be >= the longest secret
34763472-- compared: session tokens are tokenHexLen (48) hex chars, but maintenance
34773473-- secrets can be up to maxTokenLen (256) chars (validated by extractToken).
@@ -4019,7 +4015,6 @@ pruneExpired = \t -> do
40194015 set *federationActiveConnections = fold (\acc r -> acc + r.count) 0 inFlightRec
40204016 yield presenceResult
40214017
4022−
40234018-- Skip the first N items of a list (list equivalent of Text's drop).
40244019-- O(n) single pass: fold counts items and only starts accumulating after N.
40254020skipRelation = \n items ->
@@ -4096,7 +4091,6 @@ broadcastOfflineGossip = \t peerServers expiredLocal ->
40964091 let gossipEvents = mergeByKey (\a b -> a.pubkey == b.pubkey) (\x -> x.timestamp) [] (offlineGossipEvents cappedExpired)
40974092 sendBatchedGossip peerServers gossipEvents gossipOriginHops)
40984093
4099−
41004094-- Build re-broadcast gossip events from validated entries. The seenBy list includes
41014095-- the fromServer (who sent us these events) and ourselves (serverHostN), so downstream
41024096-- peers that already have this gossip via either path won't receive it again.
@@ -4123,7 +4117,6 @@ mergeGossipPresence = \presence deduped trustedOffline fromServer t ->
41234117 gossipPeerServers: collectPeerServers capResult.finalPresence (\p -> p.server),
41244118 withinCapacity: capResult.withinCapacity}
41254119
4126−
41274120-- Adaptive rate limiting: scale rate limits down when the server is under heavy load.
41284121-- Returns a scaling factor from 0 to 100 (percentage of full rate to allow).
41294122-- 100% at <=80% utilization, linearly decreasing to 10% at 100% utilization.
@@ -5688,7 +5681,6 @@ forwardCandidates = \targets messages forwards t ->
56885681 toKey: m.toKey, encryptedBlob: m.encryptedBlob, timestamp: m.timestamp,
56895682 originServer: origin, retries: 0, expiresAt: m.expiresAt, priority: m.priority, senderKey: m.senderKey})) [] targets
56905683
5691−
56925684-- Challenge-IP binding rules:
56935685-- 1. Both sides have IPs: must match (standard proxy case)
56945686-- 2. Challenge created without IP, verify without IP: accept (no-proxy case)
@@ -8758,7 +8750,7 @@ handleRotateSecret = \newSecret authorization respond -> do
87588750 authorized <- verifyMaintenanceAuth authorization
87598751 if not authorized then yield (respond {ok: False {}, error: "unauthorized"})
87608752 else do
8761− logEvent "audit" "rotate_secret" (if newSecret == "" then "revert_to_default" else "new_secret")
8753+ logEvent "audit" "rotate_secret" (if newSecret == "" then "disabled" else "new_secret")
87628754 if newSecret != "" && federationEnabled && not (tooLong newSecret 15) then do
87638755 logError "rotate-secret: rejected — secret must be at least 16 characters when federation is enabled"
87648756 yield (respond {ok: False {}, error: "secret_too_short"})
@@ -8770,7 +8762,7 @@ handleRotateSecret = \newSecret authorization respond -> do
87708762 else do
87718763 atomic do set *runtimeMaintenanceSecret = newSecret
87728764 if newSecret == ""
8773− then logInfo "rotate-secret: reverted to compile-time maintenance secret"
8765+ then logInfo "rotate-secret: maintenance secret cleared — maintenance endpoints disabled"
87748766 else logInfo "rotate-secret: maintenance secret rotated"
87758767 yield (respond {ok: True {}, error: ""})
87768768
@@ -9477,19 +9469,7 @@ main = do
94779469 atomic do
94789470 set *startedAt = t
94799471 set *serverGeneration = t * 1000 + genJitter
9480− -- Empty maintenanceSecret is the recommended deployment path: use /maintenance/rotate-secret
9481− -- at startup via deployment scripts for zero-knowledge secret management. Log at INFO level
9482− -- since operators following the recommended path shouldn't see warnings on every startup.
9483− when (maintenanceSecret == "")
9484− (logInfo "maintenanceSecret is empty — use /maintenance/rotate-secret at startup to enable /maintenance/* and /stats endpoints")
9485− when (maintenanceSecret != "" && not (tooLong maintenanceSecret 15))
9486− (logWarn "maintenanceSecret is shorter than 16 characters — use a longer secret for production deployments")
9487− when (maintenanceSecret != "" && not (tooLong maintenanceSecret 31))
9488− (logWarn "maintenanceSecret is shorter than 32 characters — 32+ character secrets recommended for production (128+ bits of entropy)")
9489− when (maintenanceSecret != "" && federationEnabled && not (tooLong maintenanceSecret 15))
9490− (logError "SECURITY: maintenanceSecret is weak and federation is enabled — maintenance endpoints control drain and peer blocking. Use a secret of at least 32 characters.")
9491− when (maintenanceSecret != "")
9492− (logWarn "SECURITY: maintenanceSecret is set as a compile-time constant. Avoid committing secrets to source control. Prefer setting maintenanceSecret to empty and using /maintenance/rotate-secret at startup via deployment scripts for zero-knowledge secret management.")
9472+ logInfo "maintenance secret not set — use /maintenance/rotate-secret at startup to enable /maintenance/* and /stats endpoints"
94939473 when (matchesHostnameOrSubdomain serverHostN "localhost")
94949474 (logWarn ("serverHost is " ++ serverHost ++ " — peers cannot reach this server for federation"))
94959475 when (federationEnabled && isBadServerName serverHostN)