Skip to content

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.

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_cidrs does not help. It opens the shared ingress VIP, which serves Kubernetes Ingress resources. A Service of type LoadBalancer bypasses 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.

private_ip and public_ip are nullable because allocation is asynchronous. Poll until status is ready and private_ip is populated.

Terminal window
nirvana nks:clusters:load-balancers list \
--cluster-id 123e4567-e89b-12d3-a456-426614174000

The list endpoint filters on namespace, service_name, status, and public_ip_enabled, so you can go straight to one Service:

Terminal window
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"
}
]
}

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.

Terminal window
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"]
}'
Terminal window
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-balancer

Repeat --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.

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.

variables.tf
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
}
load-balancer-firewall.tf
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:

  1. terraform apply the cluster, then apply your Kubernetes manifests.
  2. Read private_ip from the load balancers endpoint once status is ready.
  3. Set TF_VAR_load_balancer_private_ip and terraform apply again.

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_ports is a list — ["80", "443"] is one rule.
  • TCP and UDP need separate rules. protocol accepts 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.

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:

  1. Is there a rule for this Service’s address at all? List the VPC’s rules and check for one whose destination_address is the Service’s private_ip with a /32. The default API and ingress rules do not cover it. This is the cause in most cases.
  2. Does the rule point at the current address? Compare it against private_ip from the load balancers endpoint. A recreated Service may have moved.
  3. Is private_ip populated and status ready? Both IP fields are null while allocation is in flight.
  4. Does the rule use private_ip rather than public_ip? destination_address matches on the address inside the VPC.
  5. Is the port right? destination_ports must contain the port the Service listens on (spec.ports[].port), not the container’s target port.
  6. Is source_address wide enough to include where you are calling from, and network-aligned? 10.0.0.5/24 is rejected; 10.0.0.0/24 is accepted.
  7. Is the rule ready? Creates return an Operation. If you did not poll it, the create may have failed after the request returned 202.
  8. If you want public reachability, is public_ip_enabled true? 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 curl from 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 ping to the load balancer’s address says nothing about whether its TCP ports are open.