{"record":{"id":"cceb3c44e00ab8a7","repo":"jdx/mise","slug":"refusing-firewall-default-incoming-over-ssh-bl","errorCode":null,"errorMessage":"refusing firewall default incoming {} over SSH: blocking rule '{}' precedes a proven allow for peer {} on server port {}; reorder or narrow the rule, or set allow_lockout = true","messagePattern":"refusing firewall default incoming (.+?) over SSH: blocking rule '(.+?)' precedes a proven allow for peer (.+?) on server port (.+?); reorder or narrow the rule, or set allow_lockout = true","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/system/firewall.rs","lineNumber":502,"sourceCode":"        for rule in rules.iter().filter(|rule| {\n            rule.state == FirewallRuleState::Present\n                && rule.direction == FirewallDirection::Incoming\n                && rule\n                    .protocol\n                    .is_none_or(|protocol| protocol == FirewallProtocol::Tcp)\n                && rule\n                    .port\n                    .is_none_or(|port| port.contains(connection.server_port))\n                && rule\n                    .source\n                    .is_none_or(|source| source.contains(&connection.peer))\n                && rule\n                    .destination\n                    .is_none_or(|destination| destination.contains(&connection.server))\n        }) {\n            if rule.action != FirewallAction::Allow {\n                match backend {\n                    Some(FirewallBackend::Nftables | FirewallBackend::Ufw) if !covered => bail!(\n                        \"refusing firewall default incoming {} over SSH: blocking rule '{}' precedes a proven allow for peer {} on server port {}; reorder or narrow the rule, or set allow_lockout = true\",\n                        self.default_incoming.ufw(),\n                        rule.name,\n                        connection.peer,\n                        connection.server_port\n                    ),\n                    Some(FirewallBackend::Firewalld) => bail!(\n                        \"refusing firewall default incoming {} over SSH: blocking rule '{}' also covers peer {} on server port {}, and firewalld cannot guarantee the allow wins; narrow the rule or set allow_lockout = true\",\n                        self.default_incoming.ufw(),\n                        rule.name,\n                        connection.peer,\n                        connection.server_port\n                    ),\n                    // An automatic backend is validated again after it is\n                    // resolved.\n                    _ => {}\n                }\n                continue;","sourceCodeStart":484,"sourceCodeEnd":520,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/firewall.rs#L484-L520","documentation":"Order-sensitive lockout guard for nftables and ufw: with deny-by-default incoming traffic, the first matching rule wins, and mise found a blocking (deny/reject) rule that matches the current SSH peer and server port before any unrestricted covering allow was seen (`covered` is still false). Applying this ruleset would drop the management session mid-apply, so mise refuses. Declaration order in the rules list is the rendered order for these backends.","triggerScenarios":"`mise bootstrap firewall apply` with deny-ish `default_incoming`, a verified `SSH_CONNECTION`, backend nftables or ufw, and a rule like `action = \"deny\", source = \"10.0.0.0/8\"` (matching the peer) declared before the allow covering that peer/port — the loop hits the non-allow rule while `covered == false` and bails.","commonSituations":"Broad deny rules (geo-blocks, LAN denies, port-sweep blocks) written above the management allow; converting legacy iptables dumps where denies came first; appending the SSH allow later in the file during refactoring.","solutions":["Move the SSH allow rule above the broad deny in the rules array — order matters for nftables/ufw rendering.","Narrow the blocking rule so it no longer matches the session: exclude your management IP from its source CIDR, or remove the SSH port from its port range.","Switch the rule to a different interface or port scope that provably cannot cover peer + server port.","With out-of-band console access, set `allow_lockout = true` to proceed despite the risk."],"exampleFix":"# before (mise.toml) — deny precedes the allow\n[[bootstrap.linux.firewall.rules]]\nname = \"block-lan\"\nsource = \"10.0.0.0/8\"\naction = \"deny\"\n\n[[bootstrap.linux.firewall.rules]]\nname = \"ssh-ops\"\nsource = \"10.0.0.8/32\"\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"\n\n# after — allow first, then the broad deny\n[[bootstrap.linux.firewall.rules]]\nname = \"ssh-ops\"\nsource = \"10.0.0.8/32\"\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"\n\n[[bootstrap.linux.firewall.rules]]\nname = \"block-lan\"\nsource = \"10.0.0.0/8\"\naction = \"deny\"","handlingStrategy":"try-catch","validationCode":"# pre-flight: an unrestricted-interface allow for the SSH port must precede any matching deny\n# (manual review of declaration order in mise.toml; the guard itself is the executable check)","typeGuard":null,"tryCatchPattern":"if ! mise bootstrap firewall apply 2>fw.err; then\n  if grep -q \"precedes a proven allow\" fw.err; then\n    # reorder rules in mise.toml: management allow above broad denies, then retry\n    echo \"reorder: put the ssh allow first in [[bootstrap.linux.firewall.rules]]\"; exit 1\n  fi\n  cat fw.err; exit 1\nfi","preventionTips":["Declare the management allow as the first rule in the file, always.","Review `mise bootstrap firewall apply --dry-run` output ordering once validation passes.","Prefer narrow denies over broad 0.0.0.0/0 blocks on management ports."],"tags":["mise","firewall","ssh","rule-ordering","lockout","nftables","ufw","bootstrap"],"backgroundTag":"firewall-lockout-protection","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}