> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Security groups

> Stateful allow rules attached to an interface — the default posture, what a rule can name, and what the platform enforces regardless.

A security group is a named set of **stateful allow rules**. There are no deny
rules and no ordering: a packet is allowed if any rule on any group attached to
the interface allows it, and dropped otherwise.

<Warning>
  Rules do nothing until the group is attached to an interface. A group full of
  carefully written rules that no interface belongs to has no effect anywhere.
</Warning>

<CardGroup cols={2}>
  <Card title="The default posture" icon="lock" href="#the-default-posture">
    What an interface with no group does, and the one rule a new group starts
    with.
  </Card>

  <Card title="Writing rules" icon="list-checks" href="#rules">
    Required fields, the source that is not optional, and why there is no
    update.
  </Card>

  <Card title="Attaching" icon="link" href="#attaching-groups-to-an-interface">
    Membership is a set you replace, not a list you append to.
  </Card>

  <Card title="Platform rules" icon="shield-alert" href="#what-the-platform-enforces-regardless">
    Traffic that is always allowed, and the one port that is always blocked.
  </Card>
</CardGroup>

## The default posture

Two different defaults matter here, and confusing them is the usual source of
"my rules do nothing".

<Tabs>
  <Tab title="An interface in no group">
    **Default deny, both directions.** Every packet is dropped except the
    platform's own always-on allows. Attaching no security group is not the
    permissive option — it is the closed one.
  </Tab>

  <Tab title="A newly created group">
    Default deny both directions, **plus one rule**: egress, all protocols, to
    `0.0.0.0/0`. So attaching a fresh group gives an interface all outbound
    IPv4 and no inbound at all. Delete that rule if you want to lock egress
    down.
  </Tab>
</Tabs>

<Warning>
  That default egress rule is **IPv4 only**. A dual-stack interface has no
  `::/0` egress until you add an `ipv6` egress rule yourself, so a v6-enabled
  instance that works over IPv4 can fail silently over IPv6 with a group that
  looks wide open.
</Warning>

## Rules

<Tabs>
  <Tab title="Console">
    Go to **Networking → Security Groups** — **Create Security Group** makes
    one, with a **Name** and an optional **Description** — then open it and
    choose **Add inbound rule** or **Add outbound rule**. Which button you
    pressed is the rule's direction; there is no direction field in the
    dialog, and the block where you name the other end is headed **Source**
    or **Destination** to match.

    Pick a **Service** preset — **HTTPS (443)**, **SSH (22)**,
    **PostgreSQL (5432)** and so on — or leave it on **Custom** and set **IP
    Version**, **Protocol** and **Port Range** yourself. **Port Range** is one
    field taking either a single port or a range, where the API takes
    `port_min` and `port_max` separately. Then choose **CIDR** or **Security
    Group** for the source, and confirm with **Add Rule**.

    The group's **Inbound** and **Outbound** tabs list what it holds, each row
    with a **Delete rule** action.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/security-groups/{security_group_id}/rules
    {
      "direction": "ingress",
      "protocol": "tcp",
      "port_min": 443,
      "port_max": 443,
      "source_cidr": "0.0.0.0/0"
    }
    ```
  </Tab>
</Tabs>

<ResponseField name="direction" type="ingress | egress" required>
  For an `egress` rule, `source_cidr` and `source_security_group_id` name the
  **destination**. The field names do not change with the direction.
</ResponseField>

<ResponseField name="protocol" type="tcp | udp | icmp | all" required />

<ResponseField name="ethertype" type="ipv4 | ipv6">
  Defaults to `ipv4`. A rule matches one family only; write two rules for both.
</ResponseField>

<ResponseField name="port_min / port_max" type="integer">
  Required for `tcp` and `udp`, and rejected without them. Ignored — and
  cleared — for `icmp` and `all`. The range must satisfy
  `0 ≤ port_min ≤ port_max ≤ 65535`. For a single port, set both to it.
</ResponseField>

<ResponseField name="source_cidr / source_security_group_id" type="string | uuid" required>
  **Exactly one of the two is required.** They are mutually exclusive, and
  omitting both is rejected — there is no "any source" shorthand; write
  `0.0.0.0/0` or `::/0` explicitly. `source_cidr` must match the rule's
  `ethertype`.
</ResponseField>

Rules are create-and-delete only; there is no update. To change one, delete it
and create the replacement.

### Naming another group as the source

<Tabs>
  <Tab title="Console">
    In **Add inbound rule**, switch the **Source** block from **CIDR** to
    **Security Group**, then **Pick a source security group**.
  </Tab>

  <Tab title="API">
    ```json theme={null}
    { "direction": "ingress", "protocol": "tcp", "port_min": 5432, "port_max": 5432,
      "source_security_group_id": "<the app tier's group>" }
    ```
  </Tab>
</Tabs>

This means "any workload in that group", matched by the addresses of its
current members. Adding an instance to the app tier makes the database reachable
from it with no rule change, and removing one closes it again. It saves you from
maintaining CIDR lists that drift out of date every time a tier scales.

### Stateful

An allowed connection's return traffic is allowed automatically. You do not
write a mirrored rule in the opposite direction — an ingress rule for TCP 443
already lets the responses out.

## Attaching groups to an interface

Membership belongs to the interface, not to the group. There is nothing on a
security group's own page that attaches it to anything.

<Tabs>
  <Tab title="Console">
    Open the NIC under **Networking → Interfaces** and choose **Attach
    security group**; the dialog notes that the group's rules "start filtering
    this interface's traffic as soon as it is attached." Its **Security
    groups** tab lists what is attached, each row with a **Detach security
    group** action.

    The console adds and removes one group at a time and rebuilds the full
    list for you, so you do not have to think about the set semantics below.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    GET /v1/interfaces/{interface_id}/security-groups
    PUT /v1/interfaces/{interface_id}/security-groups
    { "security_group_ids": ["<sg-a>", "<sg-b>"] }
    ```
  </Tab>
</Tabs>

<Warning>
  `PUT` **replaces** the whole membership atomically. It is a set, not an
  append: send the full list you want, because anything you leave out is
  detached. Sending `{"security_group_ids": []}` strips every group off the
  interface, which leaves it dropping everything.

  This is the trap the console hides — read the current list with the `GET`,
  add or drop your one id, and send the whole thing back.
</Warning>

Every id has to be a group your account owns. When several groups apply to one
interface, their allow rules are **unioned** — an interface is at least as open
as its most permissive group. You cannot subtract with a security group; to
narrow an interface, remove the group that allows the traffic.

Deleting a group is refused while any interface still belongs to it. Empty the
membership on each interface first. `name` is immutable; `description` and
`tags` can be patched.

## What the platform enforces regardless

A small band of rules sits above yours. You cannot override or remove them
through this API.

### Always allowed

DHCP, IPv6 neighbor discovery, and the link-local metadata endpoint at
`169.254.169.254`. Every instance needs these to lease its address and to reach
instance metadata at boot, so they survive a default-deny posture — otherwise a
correctly-locked-down instance could never finish booting.

### Always blocked: outbound TCP 25 to the internet

Direct-to-MX SMTP from a compromised or rented instance is the fastest way for
an address range to land on a spam blocklist, and the cost of that lands on
everyone sharing the range. The envelope carries your address, so the block is
on port 25 specifically.

<Note>
  The submission ports **465, 587 and 2525 are not blocked**. Mail handed to an
  authenticated provider leaves from that provider's addresses carrying that
  provider's reputation, so blocking those buys nothing and breaks a great deal
  of software that only speaks SMTP.

  The port 25 drop also excludes private destinations, so your own mail server
  inside the VPC stays reachable from another instance.
</Note>

## Quotas and access control

`security_groups` is a regional quota against your organization.
`rules_per_security_group` is a per-resource quota — it caps the rules on each
individual group, not the total across them.

A security group's CRN is name-based, so a [policy](/iam/policies) can name
`crn:network:sa-saopaulo-1:my-account:security-group/web-*`. Rules have no name
of their own and authorize against their **parent group's** CRN and tags, so
`network:CreateSecurityGroupRule` is granted on a group rather than on `*` —
which also means a tag condition on the group governs who can add rules to it.

## Next

<CardGroup cols={2}>
  <Card title="Interfaces" icon="network" href="/networking#interfaces">
    What a NIC is and how it gets its security groups.
  </Card>

  <Card title="Gateways and routing" icon="route" href="/networking/gateways">
    Security groups decide the last hop. Routing decides whether the packet
    ever arrives.
  </Card>
</CardGroup>
