jdx/mise · critical

refusing firewall default incoming {} from an SSH-derived pr

Error message

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

What it means

A lockout guard: enabling a firewall with a deny-ish `default_incoming` (not Allow) drops unsolicited packets, and mise detected an SSH ancestor process but no `SSH_CONNECTION` environment variable, so it cannot learn the peer/port that must stay reachable. Rather than risk severing the session being used to run the command, it refuses. Preserve the variable or explicitly accept the risk with `allow_lockout = true`.

Source

Thrown at src/system/firewall.rs:473

        )
    }

    fn validate_safety_with_rules(
        &self,
        rules: &[FirewallRule],
        backend: Option<FirewallBackend>,
        ssh_ancestor: Option<bool>,
    ) -> Result<()> {
        if self.state != FirewallState::Enabled
            || self.default_incoming == FirewallPolicy::Allow
            || self.allow_lockout
        {
            return Ok(());
        }
        let Some(connection) = &self.ssh_connection else {
            return match ssh_ancestor {
                Some(false) => Ok(()),
                Some(true) => bail!(
                    "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",
                    self.default_incoming.ufw()
                ),
                None => bail!(
                    "refusing firewall default incoming {} without SSH_CONNECTION because process ancestry could not be inspected; set allow_lockout = true to acknowledge the lockout risk",
                    self.default_incoming.ufw()
                ),
            };
        };
        let mut covered = false;
        for rule in rules.iter().filter(|rule| {
            rule.state == FirewallRuleState::Present
                && rule.direction == FirewallDirection::Incoming
                && rule
                    .protocol
                    .is_none_or(|protocol| protocol == FirewallProtocol::Tcp)
                && rule
                    .port

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Preserve SSH_CONNECTION across escalation: `sudo -E mise bootstrap firewall apply`, or add `Defaults env_keep += "SSH_CONNECTION"` to sudoers.
  2. Run the command from the direct SSH login shell (not through cron/sudo wrappers) so the guard can instead verify a covering allow rule.
  3. If you have out-of-band console access (cloud serial console, IPMI, physical) and accept the risk, set `allow_lockout = true` under `[bootstrap.linux.firewall]`.
  4. Pre-add an incoming TCP allow rule for your management IP and port before converging to deny-by-default.

Example fix

# before
sudo mise bootstrap firewall apply
# error: refusing firewall default incoming deny from an SSH-derived process without SSH_CONNECTION ...

# after — keep the proof of reachability through sudo
sudo -E mise bootstrap firewall apply
# or in /etc/sudoers:
# Defaults env_keep += "SSH_CONNECTION"
Defensive patterns

Strategy: try-catch

Validate before calling

# pre-flight: never converge a deny-by-default firewall without reachability proof
[ -n "$SSH_CONNECTION" ] || { echo 'SSH_CONNECTION missing — apply from a direct SSH shell or set allow_lockout'; exit 1; }

Try / catch

# bash — never auto-override the lockout guard
if ! mise bootstrap firewall apply 2>fw.err; then
  if grep -q "allow_lockout" fw.err; then
    echo "lockout guard tripped — get console access before overriding"; exit 1
  fi
  cat fw.err; exit 1
fi

Prevention

When it happens

Trigger: `mise bootstrap firewall apply` (or `mise bootstrap`) with `state` enabled and `default_incoming` deny/reject, where process-ancestry inspection says an SSH session is an ancestor but the environment lacks `SSH_CONNECTION` — e.g. sudo stripping env, a cron/systemd unit spawned from an SSH session, or re-exec wrappers that sanitize the environment. Skipped entirely when `default_incoming = "allow"` or `allow_lockout = true` is set.

Common situations: Running mise through sudo in hardening playbooks (`sudo mise bootstrap` drops the var); CI/CD agents running over SSH with scrubbed env; sessions inside automation tools (Ansible, Terraform provisioners) that clear the environment; tmux sessions resurrected without the original env.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/cfc5fce98c0983c9. Report an issue: GitHub.