{"record":{"id":"34b5e5ff07039f33","repo":"jdx/mise","slug":"refusing-firewall-default-incoming-over-ssh-bl-34b5e5","errorCode":null,"errorMessage":"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","messagePattern":"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","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/firewall.rs","lineNumber":518,"sourceCode":"                    .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;\n            }\n            // SSH_CONNECTION does not identify the ingress interface. An\n            // interface-constrained allow cannot prove that it preserves this\n            // session, so keep looking for an unrestricted covering allow.\n            if rule.interface.is_none() {\n                covered = true;\n                if matches!(","sourceCodeStart":500,"sourceCodeEnd":536,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/firewall.rs#L500-L536","documentation":"Same lockout scenario as the nftables/ufw ordering error but for firewalld: a blocking rule earlier in the list also matches your SSH peer and server port, and firewalld's rich-rule/zone semantics cannot guarantee your later allow rule takes precedence. mise refuses the change rather than risk severing the SSH session.","triggerScenarios":"`mise bootstrap firewall apply` (validate_safety) over SSH with backend = \"firewalld\" where an earlier rule whose action does not preserve access matches connection.peer/server_port, and no earlier proven allow exists.","commonSituations":"Porting an nftables-style deny-first rule list to firewalld; broad drop rules in the public zone shadowing a rich allow rule; migrating hardened configs between backends without reordering.","solutions":["Narrow the blocking rule so it does not cover your SSH peer and server port.","Add an explicit high-priority allow for your SSH peer/port that firewalld will honor ahead of the block (rich rule ordering).","Set allow_lockout = true if you accept the lockout risk and have out-of-band access."],"exampleFix":"// before\n[[firewall.rules]]\nname = \"block-untrusted\"\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 = \"block-untrusted\"\ndirection = \"incoming\"\naction = \"deny\"","handlingStrategy":"validation","validationCode":"if backend == \"firewalld\" {\n    let shadows = rules.iter().take_while(|r| !r.action.preserves_access() || !r.matches(peer, port))\n        .any(|r| !r.action.preserves_access() && r.could_match(peer, port));\n    if shadows { eprintln!(\"firewalld: narrow the blocking rule or add a higher-priority SSH allow\"); }\n}","typeGuard":"fn no_firewalld_shadowing(rules: &[FirewallRule], peer: IpAddr, port: u16) -> bool {\n    !rules.iter().any(|r| !r.action.preserves_access() && r.could_match(peer, port))\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"firewalld cannot guarantee\") => {\n        eprintln!(\"narrow the blocking rule or add a prioritized SSH allow rich rule\");\n    }\n    other => other?,\n}","preventionTips":["Model firewalld rules with priority-aware rich rules rather than porting nftables-style lists.","Verify with `firewall-cmd --list-all` which zone rules exist before switching backends.","Test with a dry run on firewalld hosts; ordering guarantees differ from nftables/ufw."],"tags":["firewall","ssh","firewalld","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"}