Skip to content

How to Configure NSG Rules

CloudSpells uses Network Security Groups (NSGs) as the primary security mechanism. NSGs are role-based policies: one NSG represents the security posture of a class of resources (all web servers, all databases, all load balancers). The same NSG can be shared across many instances, and one instance can hold multiple NSGs.


Predefined roles

The fastest path is to use a predefined role constant. Roles encode which subnet tier a resource belongs to and what ambient network access it needs.

Constant Subnet tier Egress Typical use
INTERNET_EDGE Public Internet GW (via route table; no ambient NSG egress rules) Load balancers, internet-facing VMs
APP_SERVER Private NAT + Services Application servers, APIs
DATABASE Secure Services only Databases, secret stores
CACHE Private NAT + Services Redis, Kafka, Memcached
MANAGEMENT Management Services only Monitoring agents, tooling
from cloudspells.providers.oci.nsg import Nsg, HTTP, HTTPS, SSH
from cloudspells.providers.oci.roles import INTERNET_EDGE, APP_SERVER, DATABASE

lb_nsg  = Nsg("load-balancer", role=INTERNET_EDGE, ports=[HTTP, HTTPS],
               vcn=vcn, compartment_id=compartment_id)

web_nsg = Nsg("web-backend",   role=APP_SERVER,
               vcn=vcn, compartment_id=compartment_id)

db_nsg  = Nsg("database",      role=DATABASE,
               vcn=vcn, compartment_id=compartment_id)

When you pass role=, CloudSpells automatically:

  • Adds ambient ingress/egress rules to the NSG (service egress, NAT egress — depending on role)
  • Accumulates the matching rules into the VCN's security list for that subnet tier
  • Records the subnet tier so ComputeInstance can infer subnet placement without an explicit subnet= argument

Declaring traffic relationships with serves()

Nsg.serves(target, port) generates the full bilateral rule set for a directed traffic relationship in a single call:

  • Egress from this NSG to target on port
  • Ingress on target from this NSG on port
  • (By default) SSH management channel in both directions — omit with with_ssh=False
  • Cross-subnet security list rules when the two NSGs are in different tiers
# LB → web backend (port 8080) + SSH management
lb_nsg.serves(web_nsg, port=8080)

# Web backend → database (PostgreSQL) + SSH management
web_nsg.serves(db_nsg, port=5432)

# Data-only path — no SSH management channel
web_nsg.serves(db_nsg, port=6379, with_ssh=False)

Why serves() instead of individual allow_* calls

A two-tier relationship (LB → app server) requires four NSG rules and potentially four security list rules. Writing them individually is tedious and error-prone — one missing rule causes a silent connectivity failure. serves() generates all required rules from the relationship intent.


Port constants

from cloudspells.providers.oci.nsg import SSH, HTTP, HTTPS, POSTGRES, MYSQL, ORACLE_DB, REDIS, KAFKA, NFS

# SSH=22, HTTP=80, HTTPS=443, POSTGRES=5432, MYSQL=3306
# ORACLE_DB=1521, REDIS=6379, KAFKA=9092, NFS=2049

Or construct a custom port:

from cloudspells.providers.oci.nsg import tcp_port
my_port = tcp_port(8443)

Attaching NSGs to instances

Pass the NSG to ComputeInstance via nsg=. The subnet is inferred from the role:

from cloudspells.providers.oci.compute import ComputeInstance

web = ComputeInstance(
    name="web",
    compartment_id=compartment_id,
    vcn=vcn,
    nsg=web_nsg,   # subnet=SUBNET_PRIVATE inferred from APP_SERVER role
)

Full three-tier example

from cloudspells.providers.oci.nsg import Nsg, HTTP, HTTPS, SSH, POSTGRES
from cloudspells.providers.oci.roles import INTERNET_EDGE, APP_SERVER, DATABASE
from cloudspells.providers.oci.compute import ComputeInstance

# ── NSGs ──────────────────────────────────────────────────────────────────────

lb_nsg = Nsg(
    "load-balancer",
    role=INTERNET_EDGE,
    ports=[HTTP, HTTPS, SSH],
    vcn=vcn,
    compartment_id=compartment_id,
)

web_nsg = Nsg(
    "web-backend",
    role=APP_SERVER,
    vcn=vcn,
    compartment_id=compartment_id,
)

db_nsg = Nsg(
    "database",
    role=DATABASE,
    vcn=vcn,
    compartment_id=compartment_id,
)

# ── Relationships ─────────────────────────────────────────────────────────────

lb_nsg.serves(web_nsg, port=HTTP)       # LB → app server (port 80 + SSH)
web_nsg.serves(db_nsg, port=POSTGRES)   # app server → DB  (port 5432 + SSH)

# ── Instances ─────────────────────────────────────────────────────────────────

lb  = ComputeInstance("lb",  compartment_id=compartment_id, vcn=vcn, nsg=lb_nsg)
web = ComputeInstance("web", compartment_id=compartment_id, vcn=vcn, nsg=web_nsg)
db  = ComputeInstance("db",  compartment_id=compartment_id, vcn=vcn, nsg=db_nsg)

Custom roles

If a predefined role does not match your use-case, compose one from Role:

from cloudspells.providers.oci.roles import Role
from cloudspells.providers.oci.network import SUBNET_PRIVATE

# Private-tier proxy — internet + service egress, SSH delivered via Bastion
proxy_role = Role(
    subnet_tier=SUBNET_PRIVATE,
    egress_internet=True,
    egress_services=True,
    accept_management_ssh=False,   # SSH via Bastion, not upstream NSG
)

proxy_nsg = Nsg("proxy", role=proxy_role, vcn=vcn, compartment_id=compartment_id)

Low-level rule methods

For cases that serves() does not cover, use the individual allow methods directly:

Method Signature Purpose
allow_from_cidr (label, port, cidr) Inbound TCP from a CIDR
allow_to_cidr (label, cidr) Outbound all-protocol egress to a CIDR
allow_from_nsg (label, nsg, port) Inbound TCP from another NSG
allow_to_nsg (label, nsg, port) Outbound TCP to another NSG
allow_to_services (label) Egress to Oracle Services CIDR (all protocols)
allow_icmp_from_cidr (label, cidr, icmp_type, code) Inbound ICMP from a CIDR
add_rule (label, *, direction, protocol, ...) Raw rule — full protocol/direction control

All methods accept an optional description keyword argument for the OCI Console label.

from cloudspells.providers.oci.nsg import INTERNET

# Allow inbound HTTPS from any IP
my_nsg.allow_from_cidr("https-in", HTTPS, INTERNET)

# Allow all-protocol outbound to a specific CIDR (on-premises VPN)
my_nsg.allow_to_cidr("vpn-out", "192.168.100.0/24")

# Allow ICMP type 3 code 4 (path-MTU discovery) from the internet
my_nsg.allow_icmp_from_cidr("pmtu-in", INTERNET, icmp_type=3, code=4)

# Raw rule — UDP DNS egress (not covered by any convenience helper)
from cloudspells.providers.oci.nsg import UDP, udp_port, DNS
my_nsg.add_rule(
    "dns-out",
    direction="EGRESS", protocol=UDP,
    destination="0.0.0.0/0", destination_type="CIDR_BLOCK",
    udp_options=udp_port(DNS),
)