@@ -16,22 +16,8 @@
16-- Server identity and federation credentials
16-- Server identity and federation credentials
17serverHost = "localhost:8080"
17serverHost = "localhost:8080"
18listenPort = 8080
18listenPort = 8080
19−-- TODO: Support runtime secret rotation: accept both old and new secrets during a
20−-- transition window, so operators can rotate without restarting. E.g., add a
21−-- maintenanceSecretPrevious field that's accepted for a configurable overlap period.
22maintenanceSecret = "" -- set to enable /maintenance/* and /stats endpoints
19maintenanceSecret = "" -- set to enable /maintenance/* and /stats endpoints
23−-- Secret rotation: when non-empty, both maintenanceSecret and maintenanceSecretPrevious
24−-- are accepted. To rotate: (1) set maintenanceSecretPrevious = current secret,
25−-- (2) set maintenanceSecret = new secret, (3) redeploy, (4) after all clients update,
26−-- clear maintenanceSecretPrevious and redeploy.
27−maintenanceSecretPrevious = ""
28federationScheme = "https://" -- scheme for federation HTTP calls
20federationScheme = "https://" -- scheme for federation HTTP calls
29−-- Optional pre-shared key for federation auth. When non-empty and a peer's server name
30−-- matches, the key is used instead of callback verification — eliminates the verify
31−-- round-trip for trusted peers. Format: "server1:key1,server2:key2".
32−-- Keys must be at least 16 hex characters. Peers with pre-shared keys skip the
33−-- /federation/verify callback entirely, halving federation latency.
34−federationPresharedKeys = ""
35-- NOTE: listenHost is not configurable — Knot's listen primitive only accepts a port.
21-- NOTE: listenHost is not configurable — Knot's listen primitive only accepts a port.
36-- To restrict the bind address (e.g., 127.0.0.1 behind a reverse proxy), use
22-- To restrict the bind address (e.g., 127.0.0.1 behind a reverse proxy), use
37-- firewall rules (e.g., iptables -A INPUT -p tcp --dport 8080 -s 127.0.0.1 -j ACCEPT)
23-- firewall rules (e.g., iptables -A INPUT -p tcp --dport 8080 -s 127.0.0.1 -j ACCEPT)
@@ -1319,7 +1305,7 @@ verifyAuthWith = \check authorization ->
1319 Just {value: token} -> check token
1305 Just {value: token} -> check token
1321verifyMaintenanceAuth : Text -> Bool
1307verifyMaintenanceAuth : Text -> Bool
1322−verifyMaintenanceAuth = verifyAuthWith (\token -> verifySecret token maintenanceSecret || (maintenanceSecretPrevious != "" && verifySecret token maintenanceSecretPrevious))
1308+verifyMaintenanceAuth = verifyAuthWith (\token -> verifySecret token maintenanceSecret)
1324-- Check if a server is on the blocklist
1310-- Check if a server is on the blocklist
1325isBlockedPeer : Text -> [Text] -> Bool
1311isBlockedPeer : Text -> [Text] -> Bool
@@ -1329,75 +1315,11 @@ isBlockedPeer = \server blocked -> any blocked (\b -> b == server)
1329isAllowedByAllowlist : Text -> [Text] -> Bool
1315isAllowedByAllowlist : Text -> [Text] -> Bool
1330isAllowedByAllowlist = \server allowed -> count allowed == 0 || any allowed (\a -> a == server)
1316isAllowedByAllowlist = \server allowed -> count allowed == 0 || any allowed (\a -> a == server)
1332−-- ============================================
1333−-- Pre-shared Federation Key Helpers
1334−-- ============================================
1335−-- Parse "server1:key1,server2:key2" into a lookup-ready list.
1336−-- Keys provide HMAC-style auth: the outbound side sends "PresharedKey <key>" in the
1337−-- Authorization header; the inbound side checks safeCompare against the stored key.
1338−-- No callback round-trip needed for configured peers.
1340−-- Parse one "server:key" entry from the head of a comma-separated string.
1341−-- Returns {server, key, rest} where rest is everything after the comma (or "").
1342−parsePskEntry : Text -> {server: Text, key: Text, rest: Text}
1343−parsePskEntry = \s -> parsePskServer s ""
1345−parsePskServer : Text -> Text -> {server: Text, key: Text, rest: Text}
1346−parsePskServer = \s acc ->
1347− if s == "" then {server: acc, key: "", rest: ""}
1348− else if take 1 s == ":" then parsePskKey (drop 1 s) acc ""
1349− else if take 1 s == "," then {server: acc, key: "", rest: drop 1 s}
1350− else parsePskServer (drop 1 s) (acc ++ take 1 s)
1352−parsePskKey : Text -> Text -> Text -> {server: Text, key: Text, rest: Text}
1353−parsePskKey = \s server acc ->
1354− if s == "" then {server: server, key: acc, rest: ""}
1355− else if take 1 s == "," then {server: server, key: acc, rest: drop 1 s}
1356− else parsePskKey (drop 1 s) server (acc ++ take 1 s)
1358−-- Parse the full config string into a list of {server, key} pairs.
1359−-- Filters out entries with empty server/key or keys shorter than 16 chars.
1360−parsePresharedKeys : Text -> [{server: Text, key: Text}]
1361−parsePresharedKeys = \s ->
1364− let entry = parsePskEntry s in
1365− let rest = parsePresharedKeys entry.rest in
1366− if entry.server == "" || entry.key == "" || not (tooLong entry.key 15)
1368− else union [{server: normalizeServerName entry.server, key: entry.key}] rest
1370−-- Pre-parsed at startup (evaluated once since federationPresharedKeys is a constant)
1371−presharedKeyList = parsePresharedKeys federationPresharedKeys
1373−-- Look up a pre-shared key for a server. Returns "" if no key configured.
1374−lookupPresharedKey : Text -> Text
1375−lookupPresharedKey = \server ->
1376− fold (\acc entry -> if acc != "" then acc else if entry.server == server then entry.key else "") "" presharedKeyList
1378−-- Check if an authorization header contains a valid pre-shared key for the given server.
1379−-- Uses constant-time comparison to prevent timing side-channels.
1380−verifyPresharedKey : Text -> Text -> Bool
1381−verifyPresharedKey = \server authorization ->
1382− case extractToken authorization of
1383− Nothing {} -> False {}
1384− Just {value: token} ->
1385− let psk = lookupPresharedKey server in
1386− psk != "" && safeCompare token psk
1388-- Generate a single-use token for outbound federation auth.
1318-- Generate a single-use token for outbound federation auth.
1389−-- If a pre-shared key is configured for the target server, uses it directly
1390−-- (no token generation, no pending verification entry, no callback needed).
1391−-- Otherwise falls back to the callback verification flow.
1392-- scope binds the token to a request type ("gossip", "forward", "ack") so a token
1319-- scope binds the token to a request type ("gossip", "forward", "ack") so a token
1393-- issued for one operation cannot authorize a different one.
1320-- issued for one operation cannot authorize a different one.
1394generateFederationAuth : Text -> Text -> IO {network} Text
1321generateFederationAuth : Text -> Text -> IO {network} Text
1395−generateFederationAuth = \scope server ->
1322+generateFederationAuth = \scope server -> do
1396− -- Fast path: use pre-shared key if configured for this peer (no token generation,
1397− -- no pending verification entry, no callback round-trip on the receiving side)
1398− let psk = lookupPresharedKey server in
1399− if psk != "" then yield ("Bearer " ++ psk)
1401 token <- genToken
1323 token <- genToken
1402 t <- now
1324 t <- now
1403 atomic do
1325 atomic do
@@ -1411,16 +1333,11 @@ generateFederationAuth = \scope server ->
1411-- Verify an inbound federation token. First checks if a pre-shared key is configured
1333-- Verify an inbound federation token. First checks if a pre-shared key is configured
1412-- for the sender — if so, verifies via constant-time comparison (no network round-trip).
1334-- for the sender — if so, verifies via constant-time comparison (no network round-trip).
1413-- Otherwise falls back to callback verification via the sender's /federation/verify endpoint.
1335-- Otherwise falls back to callback verification via the sender's /federation/verify endpoint.
1414−-- Pre-shared key auth halves federation latency and removes the verify endpoint as an
1415−-- attack surface for configured peers.
1416-- No retry: a failed verify rejects the request. The peer's federation rate limit + circuit breaker
1336-- No retry: a failed verify rejects the request. The peer's federation rate limit + circuit breaker
1417-- handle unreliable peers; retrying here doubles verify traffic and blocks with a 1s sleep per op,
1337-- handle unreliable peers; retrying here doubles verify traffic and blocks with a 1s sleep per op,
1418-- which a malicious peer can exploit for amplification.
1338-- which a malicious peer can exploit for amplification.
1419verifyFederationCallback : Text -> Text -> Text -> IO {network} Bool
1339verifyFederationCallback : Text -> Text -> Text -> IO {network} Bool
1420verifyFederationCallback = \fromServer authorization scope ->
1340verifyFederationCallback = \fromServer authorization scope ->
1421− -- Fast path: pre-shared key verification (no network round-trip)
1422− if verifyPresharedKey fromServer authorization then yield (True {})
1424 case extractToken authorization of
1341 case extractToken authorization of
1425 Nothing {} -> yield (False {})
1342 Nothing {} -> yield (False {})
1426 Just {value: token} -> do
1343 Just {value: token} -> do
@@ -6584,8 +6501,6 @@ main = do
6584 (logWarn "maintenanceSecret is empty — /maintenance/* and /stats endpoints are disabled")
6501 (logWarn "maintenanceSecret is empty — /maintenance/* and /stats endpoints are disabled")
6585 when (maintenanceSecret != "" && not (tooLong maintenanceSecret 15))
6502 when (maintenanceSecret != "" && not (tooLong maintenanceSecret 15))
6586 (logWarn "maintenanceSecret is shorter than 16 characters — use a longer secret for production deployments")
6503 (logWarn "maintenanceSecret is shorter than 16 characters — use a longer secret for production deployments")
6587− when (maintenanceSecretPrevious != "")
6588− (logInfo "maintenanceSecretPrevious is set — both current and previous secrets are accepted (rotation mode)")
6589 when (matchesHostnameOrSubdomain serverHostN "localhost")
6504 when (matchesHostnameOrSubdomain serverHostN "localhost")
6590 (logWarn ("serverHost is " ++ serverHost ++ " — peers cannot reach this server for federation"))
6505 (logWarn ("serverHost is " ++ serverHost ++ " — peers cannot reach this server for federation"))
6591 when (federationEnabled && isBadServerName serverHostN)
6506 when (federationEnabled && isBadServerName serverHostN)
@@ -6593,8 +6508,6 @@ main = do
6593 logInfo "IP binding is active — sessions created with a ClientIp header will reject requests without one"
6508 logInfo "IP binding is active — sessions created with a ClientIp header will reject requests without one"
6594 when federationEnabled
6509 when federationEnabled
6595 (logWarn "SECURITY: DNS rebinding risk — federation URLs resolve DNS at fetch time, not at validation time. A peer hostname passing isBadServerName could rebind to a private IP between validation and fetch. Mitigate by blocking outbound connections to RFC1918/link-local/CGNAT ranges at the firewall level.")
6510 (logWarn "SECURITY: DNS rebinding risk — federation URLs resolve DNS at fetch time, not at validation time. A peer hostname passing isBadServerName could rebind to a private IP between validation and fetch. Mitigate by blocking outbound connections to RFC1918/link-local/CGNAT ranges at the firewall level.")
6596− when (count presharedKeyList > 0)
6597− (logInfo ("pre-shared federation keys configured for " ++ show (count presharedKeyList) ++ " peer(s) — callback verification bypassed for those peers"))
6598 logInfo ("Skrepka relay server starting on port " ++ show listenPort ++ "...")
6511 logInfo ("Skrepka relay server starting on port " ++ show listenPort ++ "...")
6599 logInfo (" federation: " ++ if federationEnabled then "enabled" else "disabled")
6512 logInfo (" federation: " ++ if federationEnabled then "enabled" else "disabled")
6600 logInfo (" prune interval: " ++ show backgroundPruneInterval ++ "ms")
6513 logInfo (" prune interval: " ++ show backgroundPruneInterval ++ "ms")