Skip to content

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:

"security_groups": ["web-tier", "monitoring"]

Step 4 — Verify the rules

GET /cloud/security-groups/<sg-id>/rules?project_id=<project-uuid>
Authorization: Bearer <token>

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.

DELETE /cloud/security-groups/<sg-id>?project_id=<project-uuid>
Authorization: Bearer <token>

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_id mutual exclusion.