{"record":{"id":"f1e6fc9c8704006e","repo":"jdx/mise","slug":"refusing-firewall-default-incoming-without-ssh","errorCode":null,"errorMessage":"refusing firewall default incoming {} without SSH_CONNECTION because process ancestry could not be inspected; set allow_lockout = true to acknowledge the lockout risk","messagePattern":"refusing firewall default incoming (.+?) without SSH_CONNECTION because process ancestry could not be inspected; set allow_lockout = true to acknowledge the lockout risk","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/firewall.rs","lineNumber":486,"sourceCode":"        &self,\n        rules: &[FirewallRule],\n        backend: Option<FirewallBackend>,\n        ssh_ancestor: Option<bool>,\n    ) -> Result<()> {\n        if self.state != FirewallState::Enabled\n            || self.default_incoming == FirewallPolicy::Allow\n            || self.allow_lockout\n        {\n            return Ok(());\n        }\n        let Some(connection) = &self.ssh_connection else {\n            return match ssh_ancestor {\n                Some(false) => Ok(()),\n                Some(true) => bail!(\n                    \"refusing firewall default incoming {} from an SSH-derived process without SSH_CONNECTION: mise cannot verify that remote access will survive; preserve SSH_CONNECTION or set allow_lockout = true\",\n                    self.default_incoming.ufw()\n                ),\n                None => bail!(\n                    \"refusing firewall default incoming {} without SSH_CONNECTION because process ancestry could not be inspected; set allow_lockout = true to acknowledge the lockout risk\",\n                    self.default_incoming.ufw()\n                ),\n            };\n        };\n        let mut covered = false;\n        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))","sourceCodeStart":468,"sourceCodeEnd":504,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/firewall.rs#L468-L504","documentation":"The lockout check needs to know whether the process runs under SSH to decide how strictly to validate; it inspects process ancestry (parent chain for sshd). When ancestry cannot be inspected at all (Some(None) case), mise cannot rule out a remote session and conservatively refuses the default-incoming change unless allow_lockout = true. This protects developers who apply a deny-by-default firewall from locking themselves out of a remote machine.","triggerScenarios":"Calling validate_safety_with_rules (via `mise bootstrap firewall apply`) on a system where the parent-process walk fails or is unavailable (restricted /proc, container without procfs access, sandboxed environment), with SSH_CONNECTION unset and allow_lockout false.","commonSituations":"Running inside Docker/LXC where /proc ancestry is masked; running under hardened security profiles that block reading other processes' status; environments without /proc (some BSD/macOS variants); automation frameworks that detach from the terminal.","solutions":["Set SSH_CONNECTION appropriately if you are actually remote and want the precise check.","Acknowledge the risk explicitly by setting `allow_lockout = true` in the [firewall] section.","Run from a normal local shell or standard SSH login where process ancestry is inspectable.","Add an explicit incoming TCP allow rule covering your SSH peer and port before changing the default policy."],"exampleFix":"// before (mise.toml)\n[firewall]\ndefault_incoming = \"deny\"\n\n// after\n[firewall]\ndefault_incoming = \"deny\"\nallow_lockout = true","handlingStrategy":"validation","validationCode":"if std::env::var(\"SSH_CONNECTION\").is_err() && !cfg.allow_lockout {\n    eprintln!(\"ancestry inspection unavailable: set allow_lockout = true or run from a normal SSH/local shell\");\n}","typeGuard":"fn lockout_check_decidable(ssh_connection: Option<&str>, allow_lockout: bool) -> bool {\n    ssh_connection.is_some() || allow_lockout\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"ancestry could not be inspected\") => {\n        eprintln!(\"run outside the sandbox or set allow_lockout = true\");\n    }\n    other => other?,\n}","preventionTips":["Run firewall applies from a standard login shell with accessible /proc.","Avoid sandboxed/containerized environments for privileged firewall changes.","Set allow_lockout = true deliberately only when you hold console/serial access."],"tags":["firewall","ssh","lockout-protection","process-inspection"],"backgroundTag":"missing-env-var","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"}