/ Docs Guides / Networking Elasticity guide ← All guides
Networking

East-west is yours. North-south is ours.

IG1 tenant networks live on one shared fabric segment, and only 10.57.8.0/24 is routed by that fabric. Everything inside the tenant boundary — networks, subnets, security groups, routers, floating IPs, load balancers — is self-service and east-west by default. The internet boundary is platform-provided: public exposure goes through the managed .75 edge, per hostname — requested for any address you own, or switched on in the same call for a load balancer marked internet-facing. This page explains each piece, and — where IG1 deliberately differs from what AWS habits expect — why.

1 · Networks and subnets

A network plus its subnet is one create on the curated surfaces: ig1 network vpc create --cidr 10.10.0.0/24 makes the Neutron network and the subnet together (IPv4 only). CIDRs are validated strictly before Neutron sees them: Neutron itself accepts 10.240.0.7/24 by silently meaning 10.240.0.0/24, so a typo in the host octet yields a subnet nobody asked for — IG1 refuses it instead. On raw REST the resources stay separate (/v1/openstack/network/v2.0/networks, …/subnets), exactly as Neutron serves them.

2 · Security groups are default-deny — on purpose

On AWS, the default security group lets members talk to each other and lets everything out. On IG1, egress is the same (allow-all) — outbound traffic inside the tenant boundary works — but a new group allows no ingress at all. An instance launched without an explicit rule set is unreachable, on purpose: agents and pipelines have published SSH to the internet as a side effect of "make me a VM", and default-deny is the posture that makes that impossible by accident.

3 · Your tenant router carries the gateway — you do not attach one yourself

Every tenant is provisioned with a network, a subnet and a router that is uplinked to the external network (SNAT on; floating IPs attach through it) — since 2026-08-15 the platform's external segment is one shared L2 and per-tenant gateways coexist (operations log, gotcha 151; before that, two outages — gotchas 107 and 121 — came from a second gateway-bearing router on what turned out to be three host-local segments). External addresses stay a platform-governed resource: additional routers you create are east-west (attach their subnets to your uplinked tenant router for north-south), and the ability to self-assemble a second internet gateway does not exist, at three depths:

Router interfaces — plumbing between your own subnets — are fully self-service (add_router_interface / remove_router_interface). Your east-west topology is entirely yours; the internet boundary is entirely ours. That trade is what lets a shared-fabric private cloud make an availability promise at all.

4 · Floating IPs — public by default since 2026-08-26

allocate / associate / disassociate / release map exactly onto the Elastic IP verbs — reachability included. A floating IP allocated today comes from public-network (185.255.84.64/26): attach it to an instance, open the port you need in its security group, and the address answers from the internet — SSH included. A floating IP is billed €0.005/hour — AWS's exact public-IPv4 rate — returns to the pool when you release it, and your tier's quota caps how many you can hold at once (1 / 2 / 4 by tier, raised on request like AWS). Two pools exist, and every surface picks the right one for you: public-network for anything that must answer from the internet; the legacy 10.168.210.0/24 (internal, site-only) stays for the platform's own tenant and for anything the .75 edge forwards to — the edge cannot route to the public range, which is why its targets keep the legacy pool.

If your workload is HTTP(S), you may not need an address at all: the edge publishes a hostname with TLS for €0.02/exposure-hour (the same class of service as AWS's Application Load Balancer) — one exposure can front any number of backends.

5 · Load balancing — one call, L4, health-checked by default, internal by default

Load balancing is Octavia with the OVN provider driver, reached at /v1/openstack/load-balancer/v2/lbaas/. Octavia's native model is five objects created one at a time; a caller driving them singly eventually creates the load balancer and fails on the listener, leaving a VIP that balances nothing and still bills. So every curated surface ships the composite: one create builds VIP + listener + pool + health monitor + members, or rolls back what it made, newest first — and names exactly what survived if the rollback itself failed. ig1 network lb create, the ig1_loadbalancer Terraform resource and the MCP create_load_balancer tool are all this composite.

Open the listener port — the security group the composite manages

Octavia's OVN provider creates the VIP like any other Neutron port: port security on, and the tenant's default security group attached — the group that denies every ingress packet (§2). So a load balancer built through any surface used to come up ACTIVE, with every member ONLINE, and answer nothing on its own listener port: the worst failure shape there is, because every status you can read says it is fine. AWS never asks this of you — an ALB makes you attach a security group and its wizard opens the listener port in it; a classic NLB has no security group at all and simply answers — so we stopped asking it too. After the graph is ACTIVE, and before the internet-facing hops below, every curated surface runs three more calls:

# 1. one group per load balancer, named after it
POST /v1/openstack/network/v2.0/security-groups
     {"security_group": {"name": "<lb-name>-lb",
                         "description": "listener ports for load balancer <lb-name> (managed by IG1)"}}
# 2. one ingress rule per DISTINCT listener port; the protocol follows the listener (TCP or UDP)
POST /v1/openstack/network/v2.0/security-group-rules
     {"security_group_rule": {"security_group_id": "<sg>", "direction": "ingress",
                              "protocol": "tcp", "port_range_min": 443, "port_range_max": 443,
                              "remote_ip_prefix": "0.0.0.0/0", "ethertype": "IPv4"}}
# 3. attach it BESIDE what the VIP port already carries — the port is read first, never clobbered
PUT  /v1/openstack/network/v2.0/ports/<vip_port_id>
     {"port": {"security_groups": ["<existing…>", "<sg>"]}}

Publish a load balancer to the internet

One toggle, default off, shaped like AWS's NLB scheme (internal or internet-facing): the portal wizard's Internet-facing checkbox, the REST body's internet_facing, ig1 network lb create --internet-facing, the provider's internet_facing attribute and the MCP create_load_balancer parameter of the same name. It is not a new service and not a new route: it composes three calls that already exist, in this order, once the load-balancer graph is ACTIVE.

# 1. a floating IP on the VIP port — 10.168.210.x, internal; on its own this publishes NOTHING
POST /v1/openstack/network/v2.0/floatingips
     {"floatingip": {"floating_network_id": "<external-net>", "port_id": "<lb.vip_port_id>"}}
# 2. an edge exposure targeting that floating IP (ownership verified server-side — it is yours)
POST /v1/edge/exposures
     {"name": "<lb-name>", "target_ip": "<floating_ip_address>", "target_port": <listener port>}
#    → {"hostname": "<name>-<tenant-slug>.10.57.8.75.nip.io", "status": "pending"}
# 3. nothing else — the edge terminates TLS and forwards HTTPS to the floating IP.

What "internet-facing" means here, said once. Reachable from outside the tenant through the platform edge at 10.57.8.75, published under the generated .nip.io hostname — which is the fabric/VPN reach, not the internet, because that hostname resolves to a private address. The edge itself has been reachable from the internet since 2026-08-25 (185.255.84.178, direct-attached to the controllers), and the way to use that reach is to attach a domain you own (§6.1): the same VIP, answered under a public name with a publicly-trusted certificate. internet_facing builds the plumbing; the custom domain is what makes it public.

6 · The .75 edge — the north-south door

Public exposure is self-service, per hostname, through the managed edge gateway at 10.57.8.75 — the one address block the fabric routes, and so the one door every public path uses: an exposure you create yourself, or the one an internet-facing load balancer creates for you (§5). An exposure is {name, target_ip, target_port, protocol}; creating one publishes {name}-{tenant-slug}.10.57.8.75.nip.io, TLS terminated at the edge for each exposure, forwarded to the target inside your tenant network.

POST   /v1/edge/exposures             # create (tier 1)
GET    /v1/edge/exposures             # list yours, with derived status + the gateway block
DELETE /v1/edge/exposures/{id}        # un-publish (tier 2 — this takes a production hostname DOWN)

POST   /v1/edge/exposures/{id}/domain # claim a domain YOU own (tier 1) — reserves, routes nothing
POST   /v1/edge/exposures/{id}/verify # read the TXT challenge back (tier 1) — THIS starts the routing
DELETE /v1/edge/exposures/{id}/domain # release it (tier 2)

6.1 · Your own domain

An exposure can also serve a name you bought — app.example.com from GoDaddy, Gandi, Cloudflare, anywhere. It is additive: the derived .nip.io hostname keeps working, and releasing the domain later does not take your app down.

The order is the whole trick, and getting it wrong looks like an outage:

  1. Claim the name (POST …/domain, or --domain on ig1 edge expose). This reserves it and mints a token. Nothing is served yet.
  2. Publish the TXT record the answer gives you: _ig1-challenge.app.example.com = the token.
  3. Verify (POST …/verify). We look the record up; this is the step that puts the name on the edge.
  4. Then point app.example.com at the address the answer names.
The certificate, and why this is the name that gets a real one. A verified domain is served over TLS with a certificate this platform obtains for it, and since 2026-08-25 that is a publicly-trusted Let's Encrypt certificate, ordered automatically once the name resolves to us — no visitor installs anything. The mechanism explains why this name and not the .nip.io one: a public CA proves control by connecting to the name being certified, over plain HTTP-01. Your A record is what makes that connection land on our front door, so pointing it here is both what makes the exposure work and what makes the certificate obtainable. We never hold a credential for your zone, and never need one.

Two consequences worth planning around. The order matters twice — the certificate is ordered after step 4, so a name verified but not yet pointed at us has no certificate to serve. And certificates are a shared budget: Let's Encrypt rate-limits per account, which is why custom domains start at the Standard tier and are capped per tier. Any list still tells you the reachability state you are in (gateway.internet_reachable) — it reads true here today.

7 · DNS — self-service records, honestly scoped

DNS is Designate, curated at /v1/dns/zones — zones and recordsets are tenant self-service on all four surfaces: REST, ig1 dns zone|record, the ig1_dns_zone / ig1_dns_record Terraform resources, and the console. The MCP surface carries zone listing, record CRUD (A, AAAA, CNAME, TXT, MX), and — since 2026-08-26 — the delegation check and the zone import below (get_dns_delegation, import_dns_records), so an agent can now finish a domain move rather than stopping at the half that does not prove anything. Terraform reads the delegation as state on the ig1_dns_zone data source (delegation_status, delegation_missing); the import is a one-shot migration action and has no Terraform form by design. Two rules Designate enforces that trip newcomers: record names are fully qualified, trailing dot included (the curated surfaces refuse a name without it rather than let "www" silently become something else), and a record can only be as fresh as its TTL — lower the TTL before a change, not after.

What resolves where — this changed on 2026-08-25, and the old answer was the opposite. cloud.ig1.com is delegated to this cloud and resolves for the whole internet. The parent zone hands out two nameservers — ns1.cloud.ig1.com (185.255.84.178) and ns2.cloud.ig1.com (185.255.84.179), on two different hosts — both glued to public addresses that answer from outside. Check it from anywhere: dig api.cloud.ig1.com @8.8.8.8 returns the address.

This page said "delegated, hardened, serving, and resolving for nobody" for eight days, and that was true: the original delegation on 2026-08-17 pointed at an RFC 1918 glue address, which no public resolver will chase. The exit was named then and taken since — one glue address something outside can reach.

What follows for you: a zone you create here is a zone the internet can be pointed at. Designate accepts it, serves it from the same public nameservers, and the practical limit is no longer who can reach us but what the record points at — an A record naming a floating IP still names a private address wherever it is queried from. The records that are meaningful off-lab are the ones pointing at your custom-domain edge exposures (§6.1), which is also the path that earns a publicly-trusted certificate.

Moving a domain you already run elsewhere is a first-class flow rather than a support ticket, on every surface including the console:

GET  /v1/dns/zones/{zone_id}/delegation   # what to publish, and whether it took (tier 0)
POST /v1/dns/zones/{zone_id}/import       # paste a zone file or a provider's table (tier 1)

Two things remain true regardless. The zone is hardened: zone transfer is refused (AXFR → Transfer failed), recursion is REFUSED, and version.bind answers "not disclosed" — so it will not serve as a resolver for your workloads, by design. And the platform's own per-instance and per-floating-IP sink records stay unpublished: putting them in a public zone would make every VM's name and address enumerable. Your zones are yours to publish; ours are not a directory of your estate.

8 · Where each surface carries it

FeatureRESTCLITerraformMCP
Networks + subnets/v1/openstack/network/v2.0/…ig1 network vpcig1_networkcreate_network, create_subnet, …
Security groups…/security-groupsig1 network sgig1_security_groupcreate_security_group, rules
Routers…/routers (gateway attach: tenant 403)—ig1_router (gateway = operator)create_router (no gateway param)
Floating IPs…/floatingipsig1 network fipig1_floating_ipallocate/associate/…
Load balancers/v1/openstack/load-balancer/v2/lbaas/… (+ the listener security group; + floating IP + exposure when internet-facing)ig1 network lb (--internet-facing, --manage-security-group)ig1_loadbalancer (internet_facing, manage_security_group)composite + member health + internet_facing + manage_security_group
Edge exposures/v1/edge/exposuresig1 edgeig1_edge_exposurecreate_edge_exposure, list, delete
DNS/v1/dns/zones (+ /delegation, /import)ig1 dns zone|recordig1_dns_zone (+ delegation_status), ig1_dns_recordzones + records CRUD, get_dns_delegation, import_dns_records

A dash is a real absence, not a hidden feature: what is not on a surface today is not on this table. The parity gate keeps the surfaces that do carry a feature in lockstep.