{"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":"error","filePath":"src/system/firewall.rs","lineNumber":511,"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.preserves_access() {\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":493,"sourceCodeEnd":529,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/firewall.rs#L493-L529","documentation":"Over SSH with a new default incoming policy, mise verifies that an incoming TCP allow rule covering your peer and server port appears BEFORE any rule whose action does not preserve access (drop/reject). If a blocking rule named earlier in the list would match your SSH traffic first, applying would cut your session, so it refuses for nftables and ufw, where first-match ordering applies.","triggerScenarios":"`mise bootstrap firewall apply` (validate_safety) over an SSH session where the ordered rules include a rule before the covering allow whose action drops/rejects traffic and whose source/destination match the connection's peer/server. Triggered for FirewallsBackend::Nftables or Ufw when the covering allow has not already been proven earlier (covered == false).","commonSituations":"Inserting a catch-all deny at the top of the rules list and the SSH allow below it; reordering rules during a config cleanup; copying a hardened firewall profile that denies everything except LAN ranges while you SSH from a WAN address.","solutions":["Move the SSH allow rule above the blocking rule in the rules list.","Narrow the blocking rule so it does not match your SSH peer/port (e.g. restrict its source/destination/interface).","Make the blocking rule's action preserve access for established sessions, or accept the risk with allow_lockout = true."],"exampleFix":"// before\n[[firewall.rules]]\nname = \"deny-all\"\ndirection = \"incoming\"\naction = \"deny\"\n\n[[firewall.rules]]\nname = \"allow-ssh\"\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"\n\n// after\n[[firewall.rules]]\nname = \"allow-ssh\"\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"\n\n[[firewall.rules]]\nname = \"deny-all\"\ndirection = \"incoming\"\naction = \"deny\"","handlingStrategy":"validation","validationCode":"// ensure an allow rule for the SSH peer/port precedes any non-preserving rule\nfn ssh_allow_is_first(rules: &[FirewallRule], peer: IpAddr, port: u16) -> bool {\n    for r in rules {\n        let covers = r.direction == Direction::Incoming\n            && r.action.preserves_access()\n            && r.matches(peer, port);\n        let blocks = r.direction == Direction::Incoming && !r.action.preserves_access();\n        if covers { return true; }\n        if blocks && r.could_match(peer, port) { return false; }\n    }\n    false\n}","typeGuard":"fn first_matching_rule_preserves(rules: &[FirewallRule], peer: IpAddr, port: u16) -> bool {\n    rules.iter()\n        .find(|r| r.direction == Direction::Incoming && r.could_match(peer, port))\n        .map(|r| r.action.preserves_access())\n        .unwrap_or(false)\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"precedes a proven allow\") => {\n        eprintln!(\"move the SSH allow rule above the blocking rule\");\n    }\n    other => other?,\n}","preventionTips":["Adopt the convention: allow rules first, deny/catch-all last.","Keep an SSH allow rule pinned at the top of the rules list in config.","Preview with a dry run before applying default-incoming changes.","Review rule ordering whenever editing [firewall] config."],"tags":["firewall","ssh","rule-ordering","lockout-protection"],"backgroundTag":"invalid-state-transition","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}