jdx/mise · error

refusing firewall default incoming

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

Before applying firewall changes, mise runs a lockout-safety check: if default incoming policy would change while the process appears to run under SSH (an SSH ancestor was detected), it requires the SSH_CONNECTION environment variable to compute which peer/port must stay reachable. SSH_CONNECTION is unset, so mise cannot prove your session survives the new default and refuses; setting allow_lockout = true overrides.

Solutions

  1. Preserve the variable so mise can verify connectivity: run with SSH_CONNECTION exported (e.g. `sudo -E mise bootstrap firewall apply` or configure sudo env_keep for SSH_CONNECTION).
  2. Re-run from a fresh direct SSH login shell where SSH_CONNECTION is set.
  3. If you accept the risk, set `allow_lockout = true` in the [firewall] config (or add an explicit allow rule for your SSH peer/port as advised in the error).

Example fix

// before (shell over SSH, env scrubbed)
sudo mise bootstrap firewall apply

// after
sudo SSH_CONNECTION="$SSH_CONNECTION" mise bootstrap firewall apply
Defensive patterns

Strategy: validation

Validate before calling

if std::env::var("SSH_CONNECTION").is_err() {
    eprintln!("SSH_CONNECTION missing; use sudo -E or set allow_lockout = true");
}
// then run: sudo SSH_CONNECTION="$SSH_CONNECTION" mise bootstrap firewall apply

Type guard

fn can_validate_lockout(allow_lockout: bool, env: &impl Fn(&str) -> Option<String>) -> bool {
    allow_lockout || env("SSH_CONNECTION").is_some()
}

Try / catch

match result {
    Err(e) if e.to_string().contains("without SSH_CONNECTION") => {
        eprintln!("re-run with sudo -E or set allow_lockout = true");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running `mise bootstrap firewall apply` (validate_safety / validate_effective_backend_request) from a shell spawned by sshd where SSH_CONNECTION was scrubbed from the environment (e.g. via `env -i`, sudo with env_reset, tmux that dropped it, or a CI runner inside SSH), with allow_lockout unset and default_incoming set to deny/reject.

Common situations: Using `sudo mise bootstrap firewall apply` where sudo strips SSH_CONNECTION; resuming a tmux/screen session that no longer exports SSH_CONNECTION; provisioning over SSH from a config-management tool that sanitizes env.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src/system/firewall.rs:482

        )
    }

    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 533346cc37)