Per-instance firewalling with security groups¶
This guide walks through creating a security group, adding rules, and attaching it to a server to control inbound and outbound traffic.
Time to complete: ~10 minutes
Prerequisites: An organization, a project, and a bearer token. You will need the UUID of an existing server or a plan to create one.
How security groups work¶
A security group is a named collection of allow rules. Every rule either permits ingress (inbound) or egress (outbound) traffic matching the given protocol, port range, and source/destination. Traffic not matching any rule is dropped.
When you create a group, Neutron automatically adds two default egress rules (allow-all IPv4 egress and allow-all IPv6 egress). Any ingress you need — SSH, HTTP, HTTPS, ICMP — must be explicitly added.
Servers reference groups by name. One server can belong to multiple groups; the effective firewall is the union of all attached groups' rules.
Step 1 — Create a security group¶
POST /cloud/security-groups?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "web-tier",
"description": "Allow HTTP, HTTPS, and SSH from anywhere"
}
Note the id from the response — you will use it to add rules and can reference the group by name when creating servers.
Step 2 — Add ingress rules¶
Allow SSH (port 22)¶
POST /cloud/security-groups/<sg-id>/rules?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"direction": "ingress",
"protocol": "tcp",
"port_range_min": 22,
"port_range_max": 22,
"remote_ip_prefix": "0.0.0.0/0"
}
Allow HTTP (port 80)¶
POST /cloud/security-groups/<sg-id>/rules?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"direction": "ingress",
"protocol": "tcp",
"port_range_min": 80,
"port_range_max": 80,
"remote_ip_prefix": "0.0.0.0/0"
}
Allow HTTPS (port 443)¶
POST /cloud/security-groups/<sg-id>/rules?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"direction": "ingress",
"protocol": "tcp",
"port_range_min": 443,
"port_range_max": 443,
"remote_ip_prefix": "0.0.0.0/0"
}
Restrict a rule to a specific CIDR¶
Replace 0.0.0.0/0 with a tighter prefix, for example 203.0.113.0/24, to allow SSH only from a specific network.
Allow traffic from another security group¶
Instead of a CIDR, you can reference another group's UUID in remote_group_id. This is useful for allowing inter-tier traffic (e.g. app servers reaching the database tier) without hard-coding IP addresses:
POST /cloud/security-groups/<db-sg-id>/rules?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"direction": "ingress",
"protocol": "tcp",
"port_range_min": 5432,
"port_range_max": 5432,
"remote_group_id": "<app-sg-id>"
}
remote_ip_prefix and remote_group_id are mutually exclusive — set only one.
Step 3 — Attach to a server¶
Pass the security group names in the security_groups array when creating the server:
POST /cloud/servers?project_id=<project-uuid>
Authorization: Bearer <token>
Content-Type: application/json
{
"project_id": "<project-uuid>",
"name": "web-01",
"flavor_id": "s.2",
"image_id": "<image-uuid>",
"network_ids": ["<network-uuid>"],
"security_groups": ["web-tier"]
}
To attach multiple groups, list them all:
Step 4 — Verify the rules¶
The response includes the rules you added plus Neutron's two default egress rules.
Removing a rule¶
Rules are immutable — to change one, delete the existing rule and add a new one:
DELETE /cloud/security-groups/<sg-id>/rules/<rule-id>?project_id=<project-uuid>
Authorization: Bearer <token>
Returns 204 No Content.
Deleting a security group¶
A group that is still attached to servers returns 422 Unprocessable Entity. Remove the group from all servers before deleting it.
Using the CLI¶
The bnerd CLI provides a security-groups command group (aliases: sg, secgroups):
# list all groups in the current project
bnerd security-groups list
# show a group and its embedded rules
bnerd security-groups get <sg-id>
# list rules for a group
bnerd security-groups rules <sg-id>
# create a group
bnerd security-groups create web-tier --description "Web servers"
# add a rule (--direction is required)
bnerd security-groups rule-add <sg-id> \
--direction ingress \
--protocol tcp \
--port-min 443 \
--port-max 443 \
--remote-ip 0.0.0.0/0
# add a rule targeting another group
bnerd security-groups rule-add <db-sg-id> \
--direction ingress \
--protocol tcp \
--port-min 5432 \
--port-max 5432 \
--remote-group <app-sg-id>
# remove a rule
bnerd security-groups rule-delete <sg-id> <rule-id>
# update name or description
bnerd security-groups update <sg-id> --name "new-name"
# delete a group (must not be in use)
bnerd security-groups delete <sg-id>
All commands support -o json and -o yaml for scripting. See cli.docs.bnerd.com for the full command reference.
Using Terraform¶
Resources¶
bnerd_security_group — manages a security group. name and description can be updated in-place. project_id changes force replacement. The embedded security_group_rules attribute is read-only (a JSON string); manage individual rules with bnerd_security_group_rule.
bnerd_security_group_rule — manages a single rule. All fields are immutable after creation — any change forces destroy and recreate.
Data sources¶
| Data source | Description |
|---|---|
bnerd_security_groups | List all groups in a project |
bnerd_security_group | Fetch a single group by UUID |
bnerd_security_group_rules | List rules for a group |
Example — web-tier group with rules and a server¶
resource "bnerd_security_group" "web" {
project_id = var.project_id
name = "web-tier"
description = "Allow HTTP, HTTPS, and SSH inbound"
}
resource "bnerd_security_group_rule" "ssh" {
security_group_id = bnerd_security_group.web.id
project_id = var.project_id
direction = "ingress"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
}
resource "bnerd_security_group_rule" "http" {
security_group_id = bnerd_security_group.web.id
project_id = var.project_id
direction = "ingress"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
}
resource "bnerd_security_group_rule" "https" {
security_group_id = bnerd_security_group.web.id
project_id = var.project_id
direction = "ingress"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
}
resource "bnerd_server" "web" {
project_id = var.project_id
name = "web-01"
image_id = data.bnerd_image.ubuntu.id
flavor_id = data.bnerd_flavor.small.id
networks = [{ uuid = var.network_id, port = null }]
# Reference by name — must match the group's name attribute
security_groups = [bnerd_security_group.web.name]
}
Note that bnerd_server.security_groups takes a list of names, not UUIDs. Use .name rather than .id when referencing a managed group.
remote_ip_prefix and remote_group_id are mutually exclusive — the provider enforces this at plan time.
Using the dashboard¶
The Security Groups pages in the b'nerd dashboard provide a visual interface for all operations covered in this guide:
- List view — shows all groups in the project with their rule counts.
- Detail view — shows a group's rules in a table with direction, protocol, port range, and remote source.
- Create/edit forms — create a group or update its name and description.
- Rule editor — add or remove rules, with inline validation of the
remote_ip_prefix/remote_group_idmutual exclusion.
Related pages¶
- Security Groups API reference
- Servers — attach groups at server creation
- Terraform Provider
- CLI