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 / |
List firewall groups |
POST / |
Create a firewall group |
GET / |
Get a firewall group |
PATCH / |
Rename a firewall group |
DELETE / |
Delete a firewall group |
GET / |
List a group's rules |
POST / |
Add a rule |
GET / |
Get a rule |
PATCH / |
Change a rule |
DELETE / |
Delete a rule |
PUT / |
Set a server's firewall groups |
How firewalls work¶
- A group has a direction, chosen when you create it:
ingressrules allow traffic to the servers, andegressrules 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/24is refused rather than read as192.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::/32becomes2001:db8::/32, and192.0.2.7becomes192.0.2.7/32. - A network matches its own address family only.
0.0.0.0/0allows 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 /
The project's groups, each with its rules and the servers it is attached to.
Create a firewall group¶
POST /
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 /
One group, with its rules and servers.
Rename a firewall group¶
PATCH /
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 /
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 /
The group's rules, lowest priority first.
Add a rule¶
POST /
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 /
One rule of the group.
Change a rule¶
PATCH /
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 /
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 /
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. |