Write a network policy
Restrict which services can reach which using `ServiceNetworkPolicy`. Default-deny activates per-service, so you can adopt policies one workload at a time.
ServiceNetworkPolicy lets you say which clients are allowed to talk to a service. In Rune, it's embedded directly in the service spec under networkPolicy: — there is no standalone ServiceNetworkPolicy cast-file resource.
This guide walks through writing your first policy, the default-deny semantics, and the v1 limitations you should know about before relying on it for security boundaries.
The shape of a policy
service:
name: api
namespace: default
image: ghcr.io/example/api:1.4.0
scale: 2
ports:
- name: http
port: 8080
networkPolicy:
ingress:
- from:
- service: web # same namespace
- service: worker
namespace: jobs # cross-namespace
- cidr: 10.0.0.0/8 # office subnet (any source)
ports:
- http
Apply it with rune cast like any other resource:
rune cast api.yaml
The agent picks it up via the OrderedLog watch, compiles the rules, and the next connection to the API service's VIP is evaluated against them. There is no restart, no reload, no settling period.
Enforcement lives in the service proxy — the process that accepts connections on each service VIP — not in a kernel packet filter. That is the single most important thing to understand about the current implementation, because it decides exactly what a policy can and cannot stop. See the limits below.
Default-deny — opt in per service
Rune's policy stance is default-allow until a service has a networkPolicy block. The moment api carries ingress or egress rules, that direction flips to default-deny. Every other service in the cluster stays default-allow until it gets the same treatment.
This is intentional — it lets you adopt policies one workload at a time without having to write a giant cluster-wide allow-list to avoid breaking unrelated services.
In practice:
# Service A: gates api. Now api is default-deny on ingress;
# only web and the office CIDR can hit it.
service:
name: api
networkPolicy:
ingress:
- from: [{ service: web }, { cidr: 10.0.0.0/8 }]
# Service B: gates worker too.
# api stays default-deny because its own policy still exists.
# database remains default-allow because it has no networkPolicy block.
service:
name: worker
networkPolicy:
ingress:
- from: [{ service: api }]
Egress — restricting what a service may call
egress: is the mirror image: instead of who may reach this service, it says which services this one may reach.
service:
name: web
namespace: prod
networkPolicy:
egress:
- to:
- service: api # same namespace
- service: db
namespace: infra
ports: [postgres] # a port name from the destination's spec
The same default-deny rule applies per direction: the moment web has any egress: rule, it may only reach what those rules allow. Egress and ingress are evaluated together and both must pass — web being allowed to call api does not exempt it from api's own ingress rules.
Ports may be written as numbers (5432, 443/tcp) or as port names taken from the destination service's spec — postgres above resolves to whichever port that service named postgres.
Before relying on this, read the egress limit in the next section. It is narrower than it looks.
Validate before you cast
rune lint checks your spec files for YAML and schema errors before you ship them, and needs no server, so it runs happily in CI:
rune lint api.yaml
It does not compile or display your network policy — there is no offline policy compile check today. To see how the agent actually reads a policy you have to cast it and then ask the server, which is the next section.
Explain what's enforced
Once a policy is in the store, render it the way the agent sees it:
$ rune get netpolicy api -n default
service: default/api
policy: default/api
default-deny ingress=true egress=false
ingress rules:
[0] peers=[service=default/web service=jobs/worker cidr=10.0.0.0/8] ports=[http]
This is the same compiled form the in-process evaluator uses, rendered deterministically so it's diff-friendly across CI runs.
Watching it bite
Every drop increments a Prometheus counter:
rune_policy_drops_total{service="api",namespace="default",policy="api",direction="ingress",reason="no_matching_rule"} 14
Read the labels carefully, because two of them mean different things:
service/namespace— the destination: who was being connected to.policy— the service whose rules did the denying. For an ingress drop that is the destination itself; for an egress drop it is the source service, so this is the spec you should go and read.direction—ingressoregress.
Useful PromQL for "policy bites in the last 5 minutes":
sum by (service, namespace, policy, direction, reason) (
increase(rune_policy_drops_total[5m])
)
A non-zero rate after a deploy usually means a new caller wasn't accounted for. Add it to the from: (or to:) list and re-cast.
One more counter is worth an alert:
rune_policy_source_unidentified_total{service="api",namespace="default"}
This counts connections whose source the agent could not tie back to a service. Egress rules are not applied to those connections, so a sustained rise means egress has quietly stopped being enforced. A non-zero baseline is normal — the ingress controller and connections originating on the host have no instance identity.
v1 limitations to know about
The policy engine is shipped as Phase 1. Three limits are worth understanding before you treat it as a hard security boundary, and the first is the one people get wrong:
-
Egress is containment between your own services — not an exfiltration boundary. Rules are enforced in the service proxy, so they only ever see a connection that is addressed to another service's VIP. A container calling the internet directly, or dialling another container's IP, never touches the proxy and is never filtered. Enforcement also needs the agent to recognise the source, which means it does not apply to
runtime: processservices, to connections arriving from another node, or to anything else the agent cannot tie to a service — and in those cases the connection is allowed, not denied. Until then: treategress:as "which of my services may talk to which", and use a firewall outside Rune if you need to stop a workload reaching the internet. Kernel-level filtering is tracked in #194; the process-runtime gap in #197.rune get netpolicywill tell you when a service's egress rules are not being enforced. -
Service-name selectors are same-node only.
from: { service: web }matcheswebinstances scheduled on the same node as the target. Cross-node identity (tying a container IP back to its owning service across the cluster) requires the multi-node identity backbone that lands with Phase 2 and Raft. Until then: use CIDR selectors for cross-node matches, or run the talking and target services co-scheduled. -
No L7 rules. Policies match on source IP / source service / destination port. There is no path, header, or method matching. If you need that, terminate at the ingress controller and apply policy at the application layer.
A note on cidr: in an egress rule: it matches the destination service's VIP — the address your container actually dials — not an arbitrary internet address. to: [{ cidr: 0.0.0.0/0 }] therefore means "any service", not "anywhere".
Common patterns
"Internal-only" service
service:
name: postgres
image: postgres:alpine
ports:
- name: postgres
port: 5432
networkPolicy:
ingress:
- from:
- service: api
- service: worker
ports: [postgres]
After casting, the database's VIP is unreachable from anything except api and worker. External CIDRs would need an explicit cidr: source. (A caller that already knows a Postgres container's own IP can still reach it directly — see the limits above.)
"Office IPs plus other services"
service:
name: admin
image: ghcr.io/example/admin:latest
ports:
- name: http
port: 8080
networkPolicy:
ingress:
- from:
- cidr: 203.0.113.0/24 # corporate egress
- cidr: 10.0.0.0/8 # VPN subnet
- service: bastion
ports: [http]
Temporarily disable a policy
There's no enabled: false switch. To remove enforcement, delete the networkPolicy: block from the service spec and cast the service again:
rune cast api.yaml
The next connection is evaluated without the rules, and api flips back to default-allow.
Reference
rune get netpolicy— inspect a compiled policy, and see whether it is being enforced.rune lintchecks spec syntax offline, but does not compile policies.- Networking concepts — how the agent compiles and enforces rules.