Skip to content

Firewalls

A firewall group is a set of rules that allow traffic, and you attach groups to servers. Groups belong to the project, so one group can protect many servers, and a server can have several groups.

Endpoints

Endpoint What it does
GET /projects/{slug}/firewall-groups/ List firewall groups
POST /projects/{slug}/firewall-groups/ Create a firewall group
GET /projects/{slug}/firewall-groups/{id}/ Get a firewall group
PATCH /projects/{slug}/firewall-groups/{id}/ Rename a firewall group
DELETE /projects/{slug}/firewall-groups/{id}/ Delete a firewall group
GET /projects/{slug}/firewall-groups/{id}/rules/ List a group's rules
POST /projects/{slug}/firewall-groups/{id}/rules/ Add a rule
GET /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/ Get a rule
PATCH /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/ Change a rule
DELETE /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/ Delete a rule
PUT /projects/{slug}/servers/{id}/firewall-groups/ Set a server's firewall groups

How firewalls work

  • A group has a direction, chosen when you create it: ingress rules allow traffic to the servers, and egress rules allow traffic from them.
  • Every rule allows traffic. Traffic that no rule allows is refused, so there are no deny rules.
  • A server without groups allows all traffic. Once it has any group attached, it refuses new inbound connections that no ingress rule allows. Once it has an egress group, it also refuses outbound traffic that no egress rule allows.
  • Rules are stateful: replies to an allowed connection pass.
  • The rules of all of a server's groups add up.
  • Changes reach the servers in the background, shortly after the request has been answered.

Tip

To allow only SSH and HTTPS in, create an ingress group with a tcp rule for port 22 and one for port 443, and attach it to the server. Leave the CIDR empty to allow every address, or give one, such as 198.51.100.0/24, to allow only that network.

The firewall group object

{
  "id": 12,
  "name": "web",
  "direction": "ingress",
  "servers": ["774736"],
  "rules": [
    {"id": 1, "protocol": "tcp", "port_min": 443, "port_max": null, "source_cidr": null, "destination_cidr": null, "priority": 1000, "comment": "HTTPS"}
  ],
  "created_at": "2026-10-19T10:00:00Z"
}
Field Type Description
id integer The group's id.
name string Up to 255 characters: letters, digits, spaces, dots, underscores and hyphens, starting with a letter or a digit. Unique in the project.
direction string ingress or egress. It can't change.
servers array The ids of the servers the group is attached to.
rules array The group's rules, lowest priority first.
created_at string When the group was created.

The rule object

Every field is present, and null where it isn't set.

Field Type Description
id integer The rule's id.
protocol string tcp, udp, icmp or any.
port_min integer or null For tcp and udp only: the first port, from 1 to 65535. Leave both ports empty to allow every port.
port_max integer or null The last port of a range. Leave it empty to allow port_min alone. It needs port_min.
source_cidr string or null For an ingress group: where the traffic comes from.
destination_cidr string or null For an egress group: where the traffic goes.
priority integer From 1 to 64996, 1000 if you leave it out. It orders the rules, lowest first. Every rule allows, so the order never changes what is allowed.
comment string or null A note of up to 255 characters. It changes nothing.

A rule of an ingress group matches on source_cidr only, and one of an egress group on destination_cidr only. The other must be empty, so that a rule never looks narrower than what it allows.

Addresses

source_cidr and destination_cidr take a network in CIDR notation, such as 192.0.2.0/24 or 2001:db8::/32, or a single address, which is stored as a /32 or /128 network. Empty or null means any address.

  • Send the network address: 192.0.2.1/24 is refused rather than read as 192.0.2.0/24, so a typo can't change what the rule matches.
  • A scope id (%), quotes and whitespace are refused.
  • Addresses come back in canonical form: 2001:DB8::/32 becomes 2001:db8::/32, and 192.0.2.7 becomes 192.0.2.7/32.
  • A network matches its own address family only. 0.0.0.0/0 allows every IPv4 address and no IPv6 one: leave the field empty to allow both.

Identical rules

A group can't hold two rules that match the same traffic. Rules are the same when they have the same protocol, the same ports, and the same CIDR for the group's direction, however each is written: no ports and 1 to 65535 both mean every port, and a port_min alone is that one port. Adding or changing a rule to match another answers 409 rule_exists, with that rule's id in rule_id when it can be read back.

Groups

List firewall groups

GET /projects/{slug}/firewall-groups/

Any member · Read-only keys: yes · Returns 200 FirewallGroup[]

The project's groups, each with its rules and the servers it is attached to.

Create a firewall group

POST /projects/{slug}/firewall-groups/

Any member · Read-only keys: no · Returns 201 FirewallGroup

Create an empty group, then add its rules.

curl -X POST "https://console.hosthatch.com/api/v1/projects/acme/firewall-groups/" \
  -H "Authorization: Bearer $HOSTHATCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "web", "direction": "ingress"}'

A name another group of the project has answers 409 name_taken, with that group's id in group_id. The id is left out only when the group couldn't be read back just then; list the project's groups to find it. So a create whose answer you lost is safe to send again: if it worked the first time, the group with that name is yours.

Get a firewall group

GET /projects/{slug}/firewall-groups/{id}/

Any member · Read-only keys: yes · Returns 200 FirewallGroup

One group, with its rules and servers.

Rename a firewall group

PATCH /projects/{slug}/firewall-groups/{id}/

Any member · Read-only keys: no · Returns 200 FirewallGroup

Change the group's name, the only thing about a group that can change.

{"name": "web-public"}

Other fields are ignored, so a group you read and send back whole with a new name is a rename. A different direction is refused, though: create a group with the other direction instead. A name another group has answers 409 name_taken, with its id in group_id when it can be read back.

Delete a firewall group

DELETE /projects/{slug}/firewall-groups/{id}/

Any member · Read-only keys: no · Returns 204

Delete the group and its rules. A group still attached to servers answers 409 group_in_use, with their ids in servers: detach it from them first, since removing it changes what they allow.

Rules

List a group's rules

GET /projects/{slug}/firewall-groups/{id}/rules/

Any member · Read-only keys: yes · Returns 200 FirewallRule[]

The group's rules, lowest priority first.

Add a rule

POST /projects/{slug}/firewall-groups/{id}/rules/

Any member · Read-only keys: no · Returns 201 FirewallRule

Add a rule that allows the traffic it matches.

curl -X POST "https://console.hosthatch.com/api/v1/projects/acme/firewall-groups/12/rules/" \
  -H "Authorization: Bearer $HOSTHATCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"protocol": "tcp", "port_min": 22, "source_cidr": "198.51.100.0/24", "comment": "SSH from the office"}'
{"id": 2, "protocol": "tcp", "port_min": 22, "port_max": null, "source_cidr": "198.51.100.0/24", "destination_cidr": null, "priority": 1000, "comment": "SSH from the office"}

Only protocol is required. A rule the group already has answers 409 rule_exists, with its id in rule_id. The id is left out only when the group couldn't be read back just then; list the group's rules to find it. So a create whose answer you lost is safe to send again.

Get a rule

GET /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/

Any member · Read-only keys: yes · Returns 200 FirewallRule

One rule of the group.

Change a rule

PATCH /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/

Any member · Read-only keys: no · Returns 200 FirewallRule

Change some of a rule's fields; the others keep their values. The rule that results is checked as a whole, so changing protocol to icmp or any also needs port_min and port_max set to null.

{"port_min": 8443, "comment": "HTTPS on another port"}

Delete a rule

DELETE /projects/{slug}/firewall-groups/{id}/rules/{rule_id}/

Any member · Read-only keys: no · Returns 204

Remove the rule. The traffic it allowed is refused, unless another rule allows it.

Attach groups to a server

Set a server's firewall groups

PUT /projects/{slug}/servers/{id}/firewall-groups/

Any member · Read-only keys: no · Returns 200 ServerFirewallGroups

Set the groups attached to a server. The list replaces the current one, and [] detaches every group. Each id must be one of the project's groups.

curl -X PUT "https://console.hosthatch.com/api/v1/projects/acme/servers/774736/firewall-groups/" \
  -H "Authorization: Bearer $HOSTHATCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firewall_groups": [12, 13]}'
{"firewall_groups": [12, 13]}

The server's firewall_groups field shows the groups attached to it.

Errors

Status Code When
400 validation_error A field is invalid: a name, ports, a CIDR, a changed direction, or a group id that isn't the project's.
403 read_only_key A change with a read-only key.
403 billing_account_required The project's owner has no billing account.
403 project_not_ready The project is still being set up.
404 not_found No such group, rule or server in the project, or a rule of another group.
409 name_taken Another group of the project has the name.
409 rule_exists The group already has an identical rule.
409 group_in_use Deleting a group that is attached to servers.
502 upstream_error A system behind the API failed. Send the same request again.
503 upstream_unavailable A system behind the API is unavailable for now.