Skip to content
Hoody.com

This page covers packet-level control of container traffic: what can connect in (ingress) and what the container can reach out to (egress).


Complete endpoint documentation:


The firewall runs on your server’s host kernel, completely outside the containers.

What that means:

  • Containers cannot bypass firewall rules
  • Containers cannot modify their own firewall
  • Rules survive container restarts and snapshots

Two directions:

  • Ingress - traffic coming into the container (who can connect)
  • Egress - traffic going out from the container (what the container can reach)

Three actions:

  • allow - permit matching traffic
  • reject - block and notify (ICMP/TCP unreachable)
  • drop - silently ignore (appears offline to scanners)

Three protocols: tcp, udp, icmp4

A container’s rule set is bounded two ways, and both are per container rather than per direction:

  • Count: 500 rules by default, with ingress and egress sharing that budget.
  • Size: 262,144 bytes of serialized rules by default. A long rule is roughly 2 KB, so a rule set of verbose rules can reach the byte budget before it reaches 500 rules.

Whichever binds first stops the write. An add over the count returns 400 CONTAINER_FIREWALL_RULE_LIMIT, and one over the size returns 400 CONTAINER_FIREWALL_BYTES_LIMIT; both messages carry the current value and the limit. Remove or shorten rules rather than retrying. Both defaults are operator-configurable, so a self-hosted deployment may be set higher or lower.


Terminal window
# Add an ingress rule
hoody firewall ingress create -c $CONTAINER_ID --action allow --protocol tcp --description "Allow SSH from office" --destination-port 22 --source 203.0.113.50/32
Terminal window
# Add an egress rule
hoody firewall egress create -c $CONTAINER_ID --action drop --protocol tcp --description "Block all TCP egress" --destination 0.0.0.0/0 --destination-port 1-65535

Use case: closing a container’s outbound TCP. Traffic on other protocols needs its own rules, DNS to the container’s host is accepted ahead of these rules, and established connections keep flowing.

Allow HTTPS to a specific API. On its own this rule changes nothing, because unmatched traffic is already allowed; it takes effect once the drops below leave a hole for it:

Terminal window
# Add an egress rule
hoody firewall egress create -c $CONTAINER_ID --action allow --protocol tcp --description "Allow HTTPS to API" --destination-port 443 --destination 198.51.100.0/24

{"destination_port": "80"}

CIDR notation:

  • /32 - a single IP (203.0.113.50/32)
  • /24 - 256 IPs (203.0.113.0/24)
  • /0 - all IPs (0.0.0.0/0)

Order is fixed by action, not by the order you create rules in: every drop rule is evaluated first, then every reject rule, then every allow rule. Traffic that matches nothing is allowed, because the bridge default stays open.

Two consequences decide how you write rules:

  • An allow rule cannot carve an exception out of an overlapping drop. Allowing SSH from your office and then dropping port 22 from 0.0.0.0/0 drops your office too.
  • An allow rule on its own restricts nothing, since unmatched traffic was already allowed. It only documents intent, or re-permits traffic a narrower drop does not cover.

So an allowlist has to be written as drops that exclude what you keep. Ports make that easy: to leave only 443 open, drop 1-442 and 444-65535 rather than 1-65535. Sources do not: the complement of one network is many ranges, so where you need “only this source”, use a mechanism with allowlist semantics instead, such as a program sandbox’s ingress_allow_from for one daemon program, or proxy permissions for an HTTP service.

The pair below shows the trap. The allow is created first, but the drop still wins:

POST Allow SSH from office (specific)
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request
POST Drop SSH from every source, the office included
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Creating them in the other order changes nothing: the drop is evaluated first either way, so SSH from the office is dropped as well. To leave SSH reachable from one network only, drop the complement of that network as shown under restrict a port to one source network. The SSH gateway is the other route in, but it authenticates by public key rather than by source address, so it is a different control, not a source allowlist.


Close PostgreSQL at the firewall. The allow records the source you mean to keep; the drop that follows is evaluated first and closes the port for that source too:

POST Allow PostgreSQL from backend IP
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Drop attempts on that port. This drop is evaluated before the allow above, so it closes the port for everyone, the allowed source included; keep it only if you want the port closed at the firewall and reached another way:

POST Drop PostgreSQL from every source
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Allow HTTP/HTTPS from anywhere:

POST Allow HTTP/HTTPS from anywhere
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Close SSH at the firewall. As above, the drop is evaluated before the allow, so the office range is closed as well; reach SSH through the SSH gateway instead:

POST Allow SSH from office
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Drop SSH. Like every drop, it is evaluated before the allow above and therefore also closes the office range; reach SSH through the SSH gateway instead of opening it here:

POST Drop SSH from every source
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

Block all egress:

POST Block all TCP egress
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Block all UDP egress
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Block all ICMP egress
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request

Rules match per protocol, so all three (tcp, udp, icmp4) are required. Otherwise ICMP egress stays open under the default-allow policy.

Use this for agent runs you want kept off the network until you say otherwise. “Off the network” here means new IPv4 connections that match these rules: DNS to the container’s host is accepted ahead of them, established and related traffic keeps flowing. Container networking is IPv4-only, so there is no IPv6 path these rules miss. An allowed destination is itself a way out, so egress rules are not proof that data stayed on the machine.

Both ports and destinations can be restricted, but only by dropping the complement of what you keep. Ports make that short; destinations mean enumerating the CIDR ranges that surround the addresses you allow.

Rule order is fixed and does not follow creation order: drop rules are evaluated first, then reject rules, then allow rules. An allow rule therefore cannot carve an exception out of an overlapping drop, so a deny range must exclude the ports or destinations you allow.

Allow only what your app needs (e.g., your API):

POST Allow HTTPS to your API
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request

Allow DNS:

POST Allow DNS lookups
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request

Block everything else. Firewall rules are per-protocol, so a drop is needed for each of tcp, udp and icmp4, and each drop must leave out the ports you allowed above: dropping 1-65535 would close 443 too, because drops are evaluated before allows.

POST Block all other TCP traffic
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Block all other UDP traffic
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Block all ICMP traffic
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request

What is left open is new TCP to port 443 and UDP to port 53, at any destination, plus host DNS and established connections. These rules restrict ports: the allow rules above record which destination you had in mind, but a drop cannot be narrowed by them. To restrict destinations as well, the drops have to enumerate the CIDR ranges around the addresses you allow. Where traffic must be limited by destination, send it through a proxy you control.


The two are separate security layers:

LayerControlsUse For
FirewallNetwork packets (TCP/UDP/ICMP)Closing ports, dropping sources you name
Proxy PermissionsHTTP service accessUser auth, service-level control, allowing client IPs you name

The firewall controls network traffic; Proxy Permissions controls HTTP service access. Use both together to cover packets and HTTP requests. They are not disjoint paths: the proxy’s upstream connection to the container is itself traffic these rules can block.

A program sandbox is a separate layer inside the container, independent of the rules on this page. An optional program sandbox restricts one daemon program. Under network.mode: restricted Landlock limits the TCP ports it may bind and connect to, for every destination including loopback and the platform address, but not UDP or destination hosts, and refuses MPTCP, io_uring and TCP Fast Open sends so those limits cannot be bypassed; mode: none removes its external networking, UDP included, while Unix sockets reachable by path stay reachable. Setting network.udp: "deny" stops the program from creating UDP sockets under any mode. Separately, its ingress rules filter IPv4 TCP arriving on eth0 at its declared ports. By default those rules let packets whose source is loopback or the gateway through; a public request forwarded by the proxy keeps the client’s own address and is filtered.

Three layers can restrict who reaches a service, and which one to use depends on what is listening:

  • An HTTP service behind a container URL: use proxy permissions. An ip group matches one IPv4 CIDR, with /32 for a single address. Membership is not by itself a restriction: grant that group access to the service, make the effective default deny, and check that no other group admits callers from elsewhere. The effective default is the container document’s when it sets one and the project document’s otherwise, so a project set to deny is undone by a container left at allow. This is the layer to reach for, because it sees the HTTP request rather than a packet, and because it fails closed once the default is deny.
  • One daemon program’s own listening port: use the program sandbox. A non-empty ingress_allow_from takes up to 64 IPv4 CIDRs and drops other new connections arriving on eth0 at that program’s declared ports. An empty or omitted list restricts nothing. It needs a declared listening set, covers IPv4 TCP only, and lets loopback and the gateway through unless you set ingress_allow_platform: false. A public request forwarded by the proxy keeps the client’s own address, so it is filtered here.
  • The whole container, on IPv4 TCP, UDP or ICMPv4: use the rules on this page, written as drops. They have no allowlist semantics, so you drop every source range except the ones you keep, once per protocol. One rule can carry several comma-separated CIDRs, so excluding a single /24 stays manageable; excluding scattered addresses rarely does.

Both firewall layers exempt established and related connections, so a rule change reaches new connections only. Editing a sandbox policy is the exception: it reloads the program’s definition and can stop its listener, which closes what was open.

See: Proxy Permissions →


Terminal window
# List all firewall rules for a container
hoody firewall rules list -c $CONTAINER_ID
Terminal window
# Disable an ingress rule
hoody firewall ingress disable -c $CONTAINER_ID --description "Allow SSH from office"

Remove a specific rule:

Terminal window
# Remove a specific ingress rule
hoody firewall ingress delete -c $CONTAINER_ID --description "Allow SSH from office" --yes

Reset completely, removing all rules:

Terminal window
# Reset firewall to default (removes ALL rules)
hoody firewall reset -c $CONTAINER_ID --yes

Creation order does not help here, because rules are evaluated by action: an allow can never carve an exception out of a drop that covers the same traffic. To leave a port open to one network only, drop that network’s complement, which is every range except the one you keep. A single rule carries the whole list, since source accepts comma-separated CIDRs. Separate them with commas and no spaces: the API rejects anything outside digits, dots, slashes and commas with a 400.

The complement of a /8 is eight ranges. This leaves SSH reachable from 10.0.0.0/8 and closed to everyone else:

POST Close SSH except from 10.0.0.0/8
/api/v1/containers/{container_id}/firewall/ingress
Click "Run" to execute the request

The shorter the prefix you keep, the shorter the complement: a /8 needs eight ranges, a /24 needs twenty-four. Add one rule per protocol you want closed, since a TCP rule leaves UDP and ICMPv4 alone.

A rule that names addresses is IPv4 only, because the API rejects IPv6 addresses and ICMPv6 outright. Container networking is IPv4-only, so that is the whole of it: there is no second family for a rule to miss.

  • "action": "drop" = stealth (appears offline to scanners)
  • "action": "reject" = reveals existence (sends ICMP unreachable)

When blocking egress, remember to allow DNS:

POST Allow DNS for egress
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Create snapshot before firewall changes
/api/v1/containers/{container_id}/snapshots
Click "Run" to execute the request

See: Snapshots →


Does the firewall apply to traffic through the Hoody Proxy?

Section titled “Does the firewall apply to traffic through the Hoody Proxy?”

Partly. The proxy terminates the request and opens its own connection to the container, so these rules never see the original HTTP request. That upstream connection does traverse them: it leaves the host through the container’s bridge, where the same ingress rules are evaluated. For an ordinary public client it carries that client’s own address as its source; for a hairpinned request, one sent from a container on the same server, it carries the address the client dialed instead. A rule matching the service’s port or the client’s network therefore blocks a service URL as well. Use Proxy Permissions for HTTP service access control, and treat these rules as packet-level policy that applies to both paths.

Can containers modify their own firewall rules?

Section titled “Can containers modify their own firewall rules?”

No. Firewall rules are host-enforced and only modifiable via the Hoody API. Containers cannot see or change them.

How does block mode differ from egress rules?

Section titled “How does block mode differ from egress rules?”

Network Configuration block mode blackholes all outbound TCP from the container, including calls to other containers’ service URLs. Firewall egress rules are granular: allow some destinations, block others.

Do firewall rules persist through restarts?

Section titled “Do firewall rules persist through restarts?”

Yes. Rules are stored at host level and survive container restarts, pauses, and snapshots.

What if I lock myself out with firewall rules?

Section titled “What if I lock myself out with firewall rules?”

Open the hoody-terminal URL. That usually still works, because a rule that locked you out normally names the port you were using rather than the terminal’s, but it is not a bypass: a drop broad enough to cover the terminal’s port closes that route too. From there or from the API, disable or narrow the drop that is blocking you, or reset: POST /api/v1/containers/{id}/firewall/reset. Adding an allow rule does not help, since drops are evaluated first.

No. The firewall operates at the IP layer, so use CIDR notation only.


Solutions:

  1. List the rules: GET /api/v1/containers/{id}/firewall/rules
  2. Disable the blocking rule: PATCH /api/v1/containers/{id}/firewall/ingress {"state": "disabled", "description": "..."}
  3. Narrow the blocking drop so its port or source range no longer covers you, by deleting it and creating it again with the narrower range. An added allow rule cannot override it
  4. Reset the firewall: POST /api/v1/containers/{id}/firewall/reset (removes all rules)

Check:

  • Rule state: must be "state": "enabled"
  • Rule action: every drop is evaluated before every reject, and every reject before every allow, whatever order you created them in
  • Protocol: a TCP rule will not block UDP traffic

Leave the package ports out of the drop. An allow rule does not reopen what a drop covers, so narrow the drop until 443 and 53 fall outside it. The two rules below then record the ports you kept:

POST Allow HTTPS for package downloads
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request
POST Allow DNS for package resolution
/api/v1/containers/{container_id}/firewall/egress
Click "Run" to execute the request

Complete networking setup:

Related security:

What this page covered:

  • The firewall is enforced at host level, and containers cannot bypass it
  • Ingress controls inbound traffic, egress controls outbound traffic
  • Rules are evaluated in order, and the first match wins
  • Three actions: allow, reject, drop
  • The firewall is independent from Hoody Proxy Permissions