on master help sign in
Remove secret rotation and pre-shared federation key support
commit66a992cdc3a8ec4d93831249bfd2cd9378cdbc7d authoriko <mail@iko.soy> dateSat 25 Apr 2026, 17:20 (6 months ago) parent8728a13
1 file changed+2−89
server.knot+2−89
server.knot+2−89
@@ -16,22 +16,8 @@
1616-- Server identity and federation credentials
1717serverHost = "localhost:8080"
1818listenPort = 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.
2219maintenanceSecret = "" -- 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 = ""
2820federationScheme = "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 = ""
3521-- NOTE: listenHost is not configurable — Knot's listen primitive only accepts a port.
3622-- To restrict the bind address (e.g., 127.0.0.1 behind a reverse proxy), use
3723-- 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 ->
13191305 Just {value: token} -> check token
13201306
13211307verifyMaintenanceAuth : Text -> Bool
1322−verifyMaintenanceAuth = verifyAuthWith (\token -> verifySecret token maintenanceSecret || (maintenanceSecretPrevious != "" && verifySecret token maintenanceSecretPrevious))
1308+verifyMaintenanceAuth = verifyAuthWith (\token -> verifySecret token maintenanceSecret)
13231309
13241310-- Check if a server is on the blocklist
13251311isBlockedPeer : Text -> [Text] -> Bool
@@ -1329,75 +1315,11 @@ isBlockedPeer = \server blocked -> any blocked (\b -> b == server)
13291315isAllowedByAllowlist : Text -> [Text] -> Bool
13301316isAllowedByAllowlist = \server allowed -> count allowed == 0 || any allowed (\a -> a == server)
13311317
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.
1339−
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 ""
1344−
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)
1351−
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)
1357−
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 ->
1362− if s == "" then []
1363− else
1364− let entry = parsePskEntry s in
1365− let rest = parsePresharedKeys entry.rest in
1366− if entry.server == "" || entry.key == "" || not (tooLong entry.key 15)
1367− then rest
1368− else union [{server: normalizeServerName entry.server, key: entry.key}] rest
1369−
1370−-- Pre-parsed at startup (evaluated once since federationPresharedKeys is a constant)
1371−presharedKeyList = parsePresharedKeys federationPresharedKeys
1372−
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
1377−
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
1387−
13881318-- 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.
13921319-- scope binds the token to a request type ("gossip", "forward", "ack") so a token
13931320-- issued for one operation cannot authorize a different one.
13941321generateFederationAuth : Text -> Text -> IO {network} Text
1395−generateFederationAuth = \scope server ->
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)
1400− else do
1322+generateFederationAuth = \scope server -> do
14011323 token <- genToken
14021324 t <- now
14031325 atomic do
@@ -1411,16 +1333,11 @@ generateFederationAuth = \scope server ->
14111333-- Verify an inbound federation token. First checks if a pre-shared key is configured
14121334-- for the sender — if so, verifies via constant-time comparison (no network round-trip).
14131335-- 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.
14161336-- No retry: a failed verify rejects the request. The peer's federation rate limit + circuit breaker
14171337-- handle unreliable peers; retrying here doubles verify traffic and blocks with a 1s sleep per op,
14181338-- which a malicious peer can exploit for amplification.
14191339verifyFederationCallback : Text -> Text -> Text -> IO {network} Bool
14201340verifyFederationCallback = \fromServer authorization scope ->
1421− -- Fast path: pre-shared key verification (no network round-trip)
1422− if verifyPresharedKey fromServer authorization then yield (True {})
1423− else
14241341 case extractToken authorization of
14251342 Nothing {} -> yield (False {})
14261343 Just {value: token} -> do
@@ -6584,8 +6501,6 @@ main = do
65846501 (logWarn "maintenanceSecret is empty — /maintenance/* and /stats endpoints are disabled")
65856502 when (maintenanceSecret != "" && not (tooLong maintenanceSecret 15))
65866503 (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)")
65896504 when (matchesHostnameOrSubdomain serverHostN "localhost")
65906505 (logWarn ("serverHost is " ++ serverHost ++ " — peers cannot reach this server for federation"))
65916506 when (federationEnabled && isBadServerName serverHostN)
@@ -6593,8 +6508,6 @@ main = do
65936508 logInfo "IP binding is active — sessions created with a ClientIp header will reject requests without one"
65946509 when federationEnabled
65956510 (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"))
65986511 logInfo ("Skrepka relay server starting on port " ++ show listenPort ++ "...")
65996512 logInfo (" federation: " ++ if federationEnabled then "enabled" else "disabled")
66006513 logInfo (" prune interval: " ++ show backgroundPruneInterval ++ "ms")