Firewall Rules for NKS LoadBalancer Services
An NKS cluster’s default firewall rules cover exactly two addresses: the
Kubernetes API VIP and the shared ingress VIP. A Kubernetes Service of type
LoadBalancer is assigned a third address of its own, and nothing opens it.
Because a VPC denies unmatched traffic by default and nothing rejects the
Service, this fails silently. The Service reports as healthy, kubectl shows an
assigned address, in-cluster callers reach it — and every caller outside the VPC
times out. Each LoadBalancer Service needs its own firewall rule.
What the Default Rules Cover
Section titled “What the Default Rules Cover”The NKS provisioning service does not create firewall rules at all. The
terraform-nirvana-nks
module creates them, and it creates exactly two:
| Rule | Protocol | Destination | Ports | Source |
|---|---|---|---|---|
| K8s API | tcp |
API VIP /32 |
443 |
management_cidrs |
| Shared ingress | tcp |
Ingress VIP /32 |
80, 443 |
ingress_cidrs |
Both target a single /32, not the subnet. The platform reserves a 12-address
block at the top of the VPC subnet: the API VIP is the second-to-last usable
address (the last is reserved by the platform), and the shared ingress VIP is
the first address of that block. Neither is the address your LoadBalancer
Service gets.
Two consequences worth being explicit about:
- A cluster created outside that module has no firewall rules whatsoever. If you created the cluster through the API, CLI, or dashboard, nothing — including the Kubernetes API — is reachable until you add rules.
ingress_cidrsdoes not help. It opens the shared ingress VIP, which serves KubernetesIngressresources. AServiceof typeLoadBalancerbypasses that VIP entirely.
Each LoadBalancer Service Gets Its Own Address
Section titled “Each LoadBalancer Service Gets Its Own Address”NKS discovers each Service of type LoadBalancer in the cluster and records a
load balancer object for it. These objects are read-only apart from one toggle —
you create and delete them by creating and deleting the Kubernetes Service, not
through the Nirvana API.
GET /v1/nks/clusters/{cluster_id}/load_balancers
| Field | Type | Notes |
|---|---|---|
id |
string | Load balancer UUID |
cluster_id |
string | Cluster the load balancer belongs to |
namespace |
string | Kubernetes namespace of the Service |
service_name |
string | Kubernetes Service name |
private_ip |
string or null |
Private address of the load balancer. This is what a firewall rule matches on |
public_ip |
string or null |
Public address, when one is enabled |
public_ip_enabled |
boolean | Whether a public address is enabled |
status |
string | pending, creating, updating, ready, deleting, deleted, error |
created_at |
string | RFC 3339 timestamp of when the load balancer was first discovered |
updated_at |
string | RFC 3339 timestamp |
public_ip_enabled is the only writable field, via
PATCH /v1/nks/clusters/{cluster_id}/load_balancers/{load_balancer_id}.
Write firewall rules against private_ip, not public_ip. The
destination_address field only accepts an address inside the VPC, and the
default API and ingress rules likewise target private VIPs even though the
cluster is reachable on a public address.
Find the Address
Section titled “Find the Address”private_ip and public_ip are nullable because allocation is asynchronous.
Poll until status is ready and private_ip is populated.
nirvana nks:clusters:load-balancers list \ --cluster-id 123e4567-e89b-12d3-a456-426614174000The list endpoint filters on namespace, service_name, status, and
public_ip_enabled, so you can go straight to one Service:
curl -sS -G https://api.nirvanalabs.io/v1/nks/clusters/123e4567-e89b-12d3-a456-426614174000/load_balancers \ -H "Authorization: Bearer $NIRVANA_LABS_API_KEY" \ --data-urlencode 'namespace=default' \ --data-urlencode 'service_name=my-service'{ "items": [ { "id": "123e4567-e89b-12d3-a456-426614174000", "cluster_id": "123e4567-e89b-12d3-a456-426614174000", "namespace": "default", "service_name": "my-service", "private_ip": "10.0.0.50", "public_ip": null, "public_ip_enabled": false, "status": "ready" } ]}Open It
Section titled “Open It”The rule is an ordinary firewall rule whose destination_address is the load
balancer’s private_ip as a /32. A bare host address is only network-aligned
at /32, so the mask is not optional.
curl -sS https://api.nirvanalabs.io/v1/networking/vpcs/123e4567-e89b-12d3-a456-426614174000/firewall_rules \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $NIRVANA_LABS_API_KEY" \ -d '{ "name": "allow-my-service-lb", "protocol": "tcp", "source_address": "0.0.0.0/0", "destination_address": "10.0.0.50/32", "destination_ports": ["443"], "tags": ["nks", "load-balancer"] }'nirvana networking:firewall-rules create \ --vpc-id 123e4567-e89b-12d3-a456-426614174000 \ --name allow-my-service-lb \ --protocol tcp \ --source-address 0.0.0.0/0 \ --destination-address 10.0.0.50/32 \ --destination-port 443 \ --tag nks \ --tag load-balancerRepeat --destination-port for each port.
Create returns 202 Accepted with an Operation rather than the rule
itself. Poll GET /v1/operations/{operation_id} until status is done, then
read the rule by resource_id. See the
Field Reference for
the full request and response shape.
Terraform
Section titled “Terraform”The provider has no NKS load balancer resource or data source, so Terraform cannot discover the address for you. Pass it in as a variable, sourced from the API or CLI above.
variable "vpc_id" { description = "ID of the VPC the NKS cluster runs in." type = string}
variable "load_balancer_private_ip" { description = "private_ip of the NKS load balancer, from the cluster's load_balancers endpoint." type = string}
variable "load_balancer_source_cidr" { description = "Network-aligned CIDR allowed to reach the LoadBalancer Service." type = string}resource "nirvana_networking_firewall_rule" "app_lb" { vpc_id = var.vpc_id name = "allow-my-service-lb" protocol = "tcp" source_address = var.load_balancer_source_cidr destination_address = "${var.load_balancer_private_ip}/32" destination_ports = ["443"] tags = ["nks", "load-balancer"]}This rule is yours to manage, not the module’s. The module’s
create_firewall_rules input toggles the API and ingress rules only; leaving it
at its default true does not produce this rule, and setting it to false does
not remove it.
Because the address does not exist until the Kubernetes Service is applied and reconciled, this is a two-step apply — the same shape as fetching the kubeconfig:
terraform applythe cluster, then apply your Kubernetes manifests.- Read
private_ipfrom the load balancers endpoint oncestatusisready. - Set
TF_VAR_load_balancer_private_ipandterraform applyagain.
One Rule per Service
Section titled “One Rule per Service”A firewall rule matches a single destination_address and a single protocol:
- A second LoadBalancer Service needs a second rule. It gets a different address, and a rule for the first one does not cover it.
- Several ports on one Service share one rule.
destination_portsis a list —["80", "443"]is one rule. - TCP and UDP need separate rules.
protocolaccepts one value.
Scope source_address deliberately. 0.0.0.0/0 is correct for a Service meant
to serve the public internet and wrong for everything else.
Why Is My LoadBalancer Unreachable?
Section titled “Why Is My LoadBalancer Unreachable?”A LoadBalancer Service that is healthy in Kubernetes but times out from outside the VPC is almost always a missing firewall rule. Work down this list:
- Is there a rule for this Service’s address at all? List the VPC’s rules
and check for one whose
destination_addressis the Service’sprivate_ipwith a/32. The default API and ingress rules do not cover it. This is the cause in most cases. - Does the rule point at the current address? Compare it against
private_ipfrom the load balancers endpoint. A recreated Service may have moved. - Is
private_ippopulated andstatusready? Both IP fields are null while allocation is in flight. - Does the rule use
private_iprather thanpublic_ip?destination_addressmatches on the address inside the VPC. - Is the port right?
destination_portsmust contain the port the Service listens on (spec.ports[].port), not the container’s target port. - Is
source_addresswide enough to include where you are calling from, and network-aligned?10.0.0.5/24is rejected;10.0.0.0/24is accepted. - Is the rule
ready? Creates return anOperation. If you did not poll it, the create may have failed after the request returned202. - If you want public reachability, is
public_ip_enabledtrue? A rule alone does not allocate a public address.
Two behaviours that make this harder to diagnose, and are worth knowing before you chase the wrong thing:
- Intra-cluster traffic is always allowed. A
curlfrom a pod in the cluster succeeds whether or not a rule exists, so it does not test the firewall. Test from outside the VPC. - ICMP is always allowed on Nirvana VPCs. A successful
pingto the load balancer’s address says nothing about whether its TCP ports are open.