Write a network policy
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
Section titled “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: - httpApply it with rune cast like any other resource:
rune cast api.yamlThe agent picks it up via the OrderedLog watch, compiles the rules, and the next packet to the API service’s VIP is filtered against the new table. There is no restart, no reload, no settling period.
Default-deny — opt in per service
Section titled “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 }]Validate before you cast
Section titled “Validate before you cast”Catch typos before you ship them:
$ rune lint api.service.yamlservice: default/apipolicy: default/apidefault-deny ingress=true egress=falseingress rules: [0] peers=[service=default/web cidr=10.0.0.0/8] ports=[http]validate is a pure-CLI compile check (CIDR parsing, port format, peer shape). It reads a raw Service document, not a cast file wrapper, so validate the inner service body or pipe it on stdin. It doesn’t talk to the server, so you can run it in CI on every PR.
Explain what’s enforced
Section titled “Explain what’s enforced”Once a policy is in the store, render it the way the agent sees it:
$ rune get netpolicy api -n defaultservice: default/apipolicy: default/apidefault-deny ingress=true egress=falseingress 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
Section titled “Watching it bite”Every drop increments a Prometheus counter:
rune_policy_drops_total{service="api",namespace="default",policy="api-allow",reason="no_match"} 14Useful PromQL for “policy bites in the last 5 minutes”:
sum by (service, namespace, reason) ( increase(rune_policy_drops_total[5m]))A non-zero rate after a deploy usually means a new caller wasn’t accounted for in the policy. Add it to the from: list and re-cast.
v1 limitations to know about
Section titled “v1 limitations to know about”The policy engine is shipped as Phase 1. There are two limits worth understanding before you treat it as a hard security boundary:
-
Service-name selectors are same-node only.
from: { service: web }matcheswebpods 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.
These are documented in the package comment and tracked for Phase 2.
Common patterns
Section titled “Common patterns””Internal-only” service
Section titled “”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 is unreachable from anything except api and worker. External CIDRs would need an explicit cidr: source.
”Office IPs plus other services”
Section titled “”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
Section titled “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.yamlThe next packet evaluation runs without the rules, and api flips back to default-allow.
Reference
Section titled “Reference”rune get netpolicy— inspect a compiled policy. Userune lintfor offline validation.- Networking concepts — how the agent compiles and enforces rules.