> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Lock it down with a policy

> Write a network policy against traffic you have actually observed, dry-run it before it can drop anything, and verify afterwards that it covers what you meant.

By the end of this page a CiliumNetworkPolicy is in force, you saw exactly which observed connections it would have allowed and denied *before* applying it, and you have checked that the coverage map agrees with your intent.

This page starts where [See what is talking to what](https://docs.celum.io/get-started/see-the-traffic.md) ended: a traffic graph you trust. That order is not decoration — a policy written against intent rather than observation is how production traffic gets dropped at 3am.

## What this changes on your cluster

A `CiliumNetworkPolicy` in a namespace, and — the whole point — traffic that was flowing may stop flowing. This is the most consequential page in this sequence, which is why the product makes a dry-run available and this page treats it as mandatory.

## Before you start

* **Flow Telemetry**, from onboarding. Without observed traffic a dry-run has nothing to check your draft against, and degrades to an advisory.
* **Permission** `security:Read` for the dry-run and **`security:Write`** to apply. They are deliberately different: anyone may ask "what would this do".

## The shape of it

**Draft → dry-run → read the warnings → apply → verify coverage.** Skipping the middle two is the entire risk.

## Step 1 — Draft it against what you saw

Start from the graph, not from an architecture diagram. The topology view's `policy` filter answers the reverse question — which edges a policy covers — so you can iterate between the two.

Topology's **author** mode builds the draft from the graph: the edges you pick go into the **Policy cart**.

![Security Topology in author mode for namespace proj-team-k, with the window, level and verdict filters, the 48-hour timeline, counts of workloads, edges, denied flows and problem ports, and a Policy cart reading 0 edges across 0 workloads with Show drift and Learn mode buttons](https://mintcdn.com/celum-e0119be0/sA7S2Ajgv1l2CH1V/images/get-started/security-author-mode.png?fit=max\&auto=format\&n=sA7S2Ajgv1l2CH1V\&q=85\&s=0e619c9d2d72fc72a5463de7fec18465)

Topology in author mode, with an empty Policy cart

## Step 2 — Dry-run it

> [tab] Dashboard
>
> Security → **Network Policies**, or **Topology** in **author** mode for namespace-scoped authoring; Policy Studio's old address leads there. The dry-run runs before the apply button means anything.

> [tab] API
>
> `POST /api/supervisors/{name}/security/policies/dry-run`. It is authorised as a **read** (`security:Read`) because it changes nothing.

> [tab] MCP
>
> `dry_run_policy`, and `dry_run_cluster_policy` for a policy aimed inside a guest cluster.

![The Network Policies tab with counts of 33 policies, 5 namespaces, 129 rules and 33 authored in Celum, filters by kind and source, the New policy, Policy Studio and Refresh buttons, and the cluster-wide policies and the first project namespace listed with their ingress and egress rule counts](https://mintcdn.com/celum-e0119be0/sA7S2Ajgv1l2CH1V/images/get-started/security-network-policies.png?fit=max\&auto=format\&n=sA7S2Ajgv1l2CH1V\&q=85\&s=844fbf1edf7bd739245cf39565db2015)

Security → Network Policies: every policy the supervisor enforces, grouped by namespace, with who authored it

The result sorts your observed edges into four buckets:

| Bucket | Meaning |
| - | - |
| `wouldAllow` | edges the policy permits |
| `wouldDeny` | edges it does not match, and which therefore stop |
| `wouldBlock` | edges an explicit **deny** rule removes — access another policy grants |
| `unaffected` | edges outside its scope |

`wouldBlock` is kept separate from `wouldDeny` on purpose: a deny rule takes away access something else granted, which is categorically different from an allow-list simply not covering an edge.

> [!WARNING]
>
> **An empty result can mean "no analysis ran".** Read the `mode` field before reading the buckets.
>
> `mode: "verdicts"` means a specific affected workload was given and the edges were evaluated against its observed traffic. `mode: "lint"` means none was, so you got a whole-namespace blast-radius advisory and `edgesAnalysed` is **0 by design**. Without checking, an empty verdict list from lint mode reads as *"this policy affects nothing"* — the exact opposite of what it means.

## Step 3 — Read the warnings, especially these two

The dry-run also returns warnings, each `danger` or `warn`. Two of them catch mistakes that are almost impossible to see by reading the YAML.

**"Selects the entire namespace"** — an empty `endpointSelector: {}` default-denies the whole namespace, not just the workload you had in mind. It escalates from `warn` to **`danger`** when critical infrastructure is in scope, because **KubeVirt VMs and CAPI guest nodes silently lose bootstrap traffic**. The fix it names: scope the selector to specific pods, or add explicit allows for in-cluster and node traffic (`fromEntities: cluster, host, remote-node`).

**"This policy restricts nothing yet"** — the counter-intuitive one. **Cilium unions allow-lists.** If a blanket rule already allows every endpoint in the namespace, applying a tighter-looking policy beside it refuses nothing at all; it merely turns L7 parsing on for the ports it names. The draft looks restrictive and is inert until the blanket rule is narrowed, which is a *replace*, not an addition.

The other two are simpler: an **HTTP rule Cilium would reject** (`danger` — the policy would not do what it says) and **L7 verdicts unavailable** (`warn` — the L7 half of the analysis could not run).

## Step 4 — Apply

> [tab] Dashboard
>
> Apply from the same screen that showed you the dry-run.

> [tab] API
>
> `POST /api/supervisors/{name}/security/policies` — `security:Write`.

> [tab] MCP
>
> `apply_security_policy`. Its own description carries the rule: *"ALWAYS run `dry_run_policy` first and show the operator what it would match — an over-broad policy silently drops production traffic."*

**Result:** the policy is listed, and the coverage map begins reflecting it.

## Inside a guest cluster, the blast radius is larger

A guest gets its own dry-run — identities from the guest's own API server, observed edges from the supervisor's flow store filtered on the guest's cluster label.

> [!WARNING]
>
> Run it. The tool's reasoning is the right one to carry: **a guest cluster is a tenant's whole world**, so an over-broad policy there takes out everything they run, not one namespace of it.

## Verify

Read the coverage map and confirm the edges you meant to protect now show as covered.

Expect some that are protected to *not* show as covered — that is not a failure of your policy, it is one of the coverage map's structural blind spots, and [See what is talking to what](https://docs.celum.io/get-started/see-the-traffic.md) lists all three. The short version: a workload with no recognised name label is not indexed, and a plain `networking.k8s.io/v1` NetworkPolicy is not counted even though Cilium enforces it.

The stronger check is behavioural. Watch the traffic graph over a window that spans the apply: edges that changed verdict are the policy working, and edges that disappeared are worth explaining before you move on.

## If you need to undo it

`DELETE /api/supervisors/{name}/security/policies` removes it, under the same `security:Write`.

Deleting a policy restores whatever the surrounding policies allow — which, because allow-lists union, may be more than you expect and may be nothing at all if a default-deny remains.

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| The dry-run returned nothing, so the policy looks harmless | check `mode`. In `lint` mode no edges are analysed, by design |
| Applying a tight-looking policy changed nothing | a blanket allow already exists. Cilium unions allow-lists — narrow the existing rule instead of adding beside it |
| VMs or guest cluster nodes lost connectivity after an apply | an empty `endpointSelector` default-denied the whole namespace. The dry-run flags this as `danger` |
| The policy is applied and the edge still paints unprotected | a coverage blind spot, not an enforcement failure |
| The L7 half of the dry-run was missing | the "L7 verdicts unavailable" warning says why |
| A policy that dry-ran cleanly broke a tenant cluster | it was dry-run against the supervisor. A guest needs the guest dry-run |

## Next

> [card] [Network policies](https://docs.celum.io/security/policies.md)
>
> Per-policy detail, drift detection, and the coverage model in full.

> [card] [Forensics](https://docs.celum.io/security/forensics.md)
>
> Who reached a service, and where a workload phones home.

> [card] [Permissions model](https://docs.celum.io/concepts/permissions-model.md)
>
> The other kind of policy — who may do this at all.

> [card] [Projects](https://docs.celum.io/concepts/projects.md)
>
> Namespace-level isolation, which a new project gets by default.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.