jdx/mise · error

refusing firewall default incoming

Error message

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

What it means

Over SSH with a new default incoming policy, mise verifies that an incoming TCP allow rule covering your peer and server port appears BEFORE any rule whose action does not preserve access (drop/reject). If a blocking rule named earlier in the list would match your SSH traffic first, applying would cut your session, so it refuses for nftables and ufw, where first-match ordering applies.

Solutions

  1. Move the SSH allow rule above the blocking rule in the rules list.
  2. Narrow the blocking rule so it does not match your SSH peer/port (e.g. restrict its source/destination/interface).
  3. Make the blocking rule's action preserve access for established sessions, or accept the risk with allow_lockout = true.

Example fix

// before
[[firewall.rules]]
name = "deny-all"
direction = "incoming"
action = "deny"

[[firewall.rules]]
name = "allow-ssh"
port = 22
protocol = "tcp"
action = "allow"

// after
[[firewall.rules]]
name = "allow-ssh"
port = 22
protocol = "tcp"
action = "allow"

[[firewall.rules]]
name = "deny-all"
direction = "incoming"
action = "deny"
Defensive patterns

Strategy: validation

Validate before calling

// ensure an allow rule for the SSH peer/port precedes any non-preserving rule
fn ssh_allow_is_first(rules: &[FirewallRule], peer: IpAddr, port: u16) -> bool {
    for r in rules {
        let covers = r.direction == Direction::Incoming
            && r.action.preserves_access()
            && r.matches(peer, port);
        let blocks = r.direction == Direction::Incoming && !r.action.preserves_access();
        if covers { return true; }
        if blocks && r.could_match(peer, port) { return false; }
    }
    false
}

Type guard

fn first_matching_rule_preserves(rules: &[FirewallRule], peer: IpAddr, port: u16) -> bool {
    rules.iter()
        .find(|r| r.direction == Direction::Incoming && r.could_match(peer, port))
        .map(|r| r.action.preserves_access())
        .unwrap_or(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("precedes a proven allow") => {
        eprintln!("move the SSH allow rule above the blocking rule");
    }
    other => other?,
}

Prevention

When it happens

Trigger: `mise bootstrap firewall apply` (validate_safety) over an SSH session where the ordered rules include a rule before the covering allow whose action drops/rejects traffic and whose source/destination match the connection's peer/server. Triggered for FirewallsBackend::Nftables or Ufw when the covering allow has not already been proven earlier (covered == false).

Common situations: Inserting a catch-all deny at the top of the rules list and the SSH allow below it; reordering rules during a config cleanup; copying a hardened firewall profile that denies everything except LAN ranges while you SSH from a WAN address.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at src/system/firewall.rs:511

        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
                    .is_none_or(|port| port.contains(connection.server_port))
                && rule
                    .source
                    .is_none_or(|source| source.contains(&connection.peer))
                && rule
                    .destination
                    .is_none_or(|destination| destination.contains(&connection.server))
        }) {
            if !rule.action.preserves_access() {
                match backend {
                    Some(FirewallBackend::Nftables | FirewallBackend::Ufw) if !covered => bail!(
                        "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",
                        self.default_incoming.ufw(),
                        rule.name,
                        connection.peer,
                        connection.server_port
                    ),
                    Some(FirewallBackend::Firewalld) => bail!(
                        "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",
                        self.default_incoming.ufw(),
                        rule.name,
                        connection.peer,
                        connection.server_port
                    ),
                    // An automatic backend is validated again after it is
                    // resolved.
                    _ => {}
                }
                continue;

View on GitHub (pinned to 533346cc37)