{"record":{"id":"2f51095e9f8d8aa4","repo":"jdx/mise","slug":"refusing-firewall-default-incoming-over-ssh-no","errorCode":null,"errorMessage":"refusing firewall default incoming {} over SSH: no incoming TCP allow rule covers peer {} on server port {} with an unrestricted interface; add a covering rule or set allow_lockout = true","messagePattern":"refusing firewall default incoming (.+?) over SSH: no incoming TCP allow rule covers peer (.+?) on server port (.+?) with an unrestricted interface; add a covering rule or set allow_lockout = true","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/system/firewall.rs","lineNumber":536,"sourceCode":"                    _ => {}\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!(\n                    backend,\n                    Some(FirewallBackend::Nftables | FirewallBackend::Ufw)\n                ) {\n                    break;\n                }\n            }\n        }\n        if !covered {\n            bail!(\n                \"refusing firewall default incoming {} over SSH: no incoming TCP allow rule covers peer {} on server port {} with an unrestricted interface; add a covering rule or set allow_lockout = true\",\n                self.default_incoming.ufw(),\n                connection.peer,\n                connection.server_port\n            );\n        }\n        Ok(())\n    }\n\n    pub fn plans(&self) -> Vec<ResourcePlan> {\n        let inspection = self.inspection.as_ref();\n        let backend = inspection\n            .and_then(|inspection| inspection.backend)\n            .unwrap_or(self.backend);\n        let desired = format!(\n            \"{} via {}; incoming {}; outgoing {}; {}\",\n            match self.state {\n                FirewallState::Enabled => \"enabled\",","sourceCodeStart":518,"sourceCodeEnd":554,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/firewall.rs#L518-L554","documentation":"Final lockout check: after scanning every rule, none provides an incoming TCP allow that covers the current SSH peer and server port with an unrestricted interface. Interface-bound allows deliberately do not count (`covered` is only set for rules without an `interface` key), because mise cannot prove which interface the session uses. Switching `default_incoming` to deny would therefore cut the session, so the apply aborts.","triggerScenarios":"`mise bootstrap firewall apply` with deny-ish `default_incoming` where the only candidate allow rules are interface-restricted (e.g. `interface = \"eth0\"` while SSH arrives on wg0/ens18), target the wrong port (allowing 22 while connecting on 2222), or the wrong peer CIDR — or no incoming allow exists at all.","commonSituations":"Interface-name assumptions (ens18 vs eth0, VPN tunnels wg0/tun0 not covered); moving management SSH to a nonstandard port while the allow still lists 22; allows scoped to an office CIDR while connecting from home/VPN; first-time enablement of deny-by-default with no management rule yet.","solutions":["Add an incoming allow that matches the session and has no `interface` key: `direction = \"incoming\"`, `action = \"allow\"`, `port = 22`, `protocol = \"tcp\"`, with source equal to your IP or 0.0.0.0/0.","Remove or fix the `interface` restriction on the intended allow rule.","If connecting via VPN, allow the tunnel peer subnet (or the whole tunnel interface's traffic) without the interface key.","With out-of-band console access, set `allow_lockout = true` to proceed anyway."],"exampleFix":"# before — only an interface-bound allow exists\n[[bootstrap.linux.firewall.rules]]\nname = \"ssh\"\ninterface = \"eth0\"          # session actually arrives on wg0\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"\n\n# after — unrestricted interface, peer-scoped\n[[bootstrap.linux.firewall.rules]]\nname = \"ssh\"\nsource = \"203.0.113.5/32\"   # your peer\nport = 22\nprotocol = \"tcp\"\naction = \"allow\"","handlingStrategy":"try-catch","validationCode":"# pre-flight: confirm a current allow covers your session\n# compare `echo $SSH_CONNECTION` (peer, server port) against your rules;\n# the allow must have NO interface key and match peer + port","typeGuard":null,"tryCatchPattern":"if ! mise bootstrap firewall apply 2>fw.err; then\n  if grep -q \"no incoming TCP allow rule covers\" fw.err; then\n    echo \"add: allow incoming tcp port <SSHport> from <your-ip> without interface\"; exit 1\n  fi\n  cat fw.err; exit 1\nfi","preventionTips":["Add an unrestricted-interface TCP allow for the SSH port before ever setting default_incoming to deny.","Do not bind management allows to interface names — use peer CIDRs.","After changing your SSH port or network, update the allow rule in the same commit."],"tags":["mise","firewall","ssh","lockout","interface","bootstrap"],"backgroundTag":"firewall-lockout-protection","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}