firewall-networking · diff
git:20260610.5ee9b17 to git:20260612.b890dfa
92 added, 114 removed. Audit A to A.
---
name: firewall-networking
description: "Firewall rules and service-to-service communication on Control Plane. Use when the user asks about inbound/outbound rules, CIDR whitelisting, IP blocking, hostname filtering, geo-blocking, header routing, internal endpoints, or network security."
---
- # Firewall & Networking Patterns
+ # Firewall & Networking
- Deep firewall rules for `spec.firewallConfig`. The deny-by-default summary, the LB picker, internal DNS, and which tool sets `firewallConfig` vs `loadBalancer` live in the `workload` skill — this skill is the detail.
+ > **Tool availability:** some MCP tools named here live in the `full` toolset profile — if one is not advertised on this connection, tell the user to reconnect the MCP server with `?toolsets=full` (or use the `cpln` CLI fallback). Reads and deletes work on every profile via the generic `list_resources` / `get_resource` / `delete_resource` tools.
- **Precedence (both inbound and outbound):** blocked CIDRs beat allowed CIDRs; when both CIDR and hostname allow-lists are set, CIDR rules beat hostname rules.
+ Deep detail for `spec.firewallConfig` and the enforcement model behind it; the `workload` skill owns the summary (deny-by-default, exposure decided at create time, LB picker). Set `firewallConfig` with `create_workload` / `update_workload` — or `public: true`, the shortcut that opens inbound AND outbound to `0.0.0.0/0` (mutually exclusive with an explicit `firewallConfig`). A firewall change creates a new deployment version — a rolling replace, live in about a minute (`vm` workloads are the exception: firewall updates apply in place without restarting the VM).
- ## External Firewall Rules
+ ## How rules are enforced
- ### Inbound (internet → workload)
+ **Inbound** is checked per request at the mesh sidecar. It counts as fully open only when `inboundAllowCIDR` contains the literal `0.0.0.0/0` AND `inboundBlockedCIDR` is empty; anything else is allow-list mode. Blocked beats allowed; a bare IP means /32. Header and geo filters apply to HTTP traffic only — `tcp`-protocol ports are CIDR-filtered at the connection level instead.
- Allow all (`0.0.0.0/0`), or list specific CIDRs, and block CIDRs that override the allow-list:
- ```yaml
- firewallConfig:
- external:
- inboundAllowCIDR:
- - 0.0.0.0/0 # or specific: 203.0.113.0/24, 198.51.100.10/32
- inboundBlockedCIDR:
- - 192.0.2.0/24 # blocked wins over allowed
- ```
+ **Outbound** has two separate paths, which is why CIDR rules beat hostname rules:
- ### Outbound (workload → internet)
+ - **CIDR path** — traffic to `outboundAllowCIDR` ranges bypasses the sidecar and exits directly, on ALL ports unless `outboundAllowPort` is set.
+ - **Hostname path** — everything else transits the sidecar, which only admits `outboundAllowHostname` entries, matched by Host header (HTTP) or TLS SNI, on ports 80, 443, and 445 (SMB) by default.
+ - `outboundBlockedCIDR` is subtracted at the network layer and beats both paths — an allowed hostname that resolves into a blocked range still fails.
+ - Outbound is fully open only with the literal `0.0.0.0/0` in `outboundAllowCIDR`.
- CIDR allow/block (CIDR-based allows all ports):
+ ## External inbound
+
```yaml
firewallConfig:
external:
- outboundAllowCIDR:
- - 0.0.0.0/0
- outboundBlockedCIDR:
- - 10.0.0.0/8 # blocked wins over allowed
+ inboundAllowCIDR: # max 250 entries; deduped and sorted on save
+ - 0.0.0.0/0 # or specific: 203.0.113.0/24, 198.51.100.10
+ inboundBlockedCIDR: # no max; wins over the allow list
+ - 192.0.2.0/24
```
- Hostname allow-list (wildcard prefix supported). **Hostname rules only allow ports 80, 443, and 445 by default** — widen with `outboundAllowPort`:
+ ## External outbound
+
```yaml
firewallConfig:
external:
- outboundAllowHostname:
+ outboundAllowCIDR:
+ - 198.51.100.0/24 # all ports open to this range while outboundAllowPort is unset
+ outboundAllowHostname: # lowercase; single wildcard on the prefix only; max 128 chars
- api.stripe.com
- "*.amazonaws.com"
+ outboundBlockedCIDR:
+ - 203.0.113.7
```
- ### Outbound Port Control
+ Source-verified traps:
- `outboundAllowPort` restricts which ports are allowed (protocols: `http`, `https`, `tcp`):
- ```yaml
- firewallConfig:
- external:
- outboundAllowCIDR:
- - 203.0.113.0/24
- outboundAllowPort:
- - protocol: tcp
- number: 5432
- - protocol: https
- number: 8443
- ```
+ - **`outboundAllowPort` REPLACES the hostname defaults 80/443/445** — re-list 80 and 443 if you still need them. It also restricts the CIDR path to the listed ports. `protocol` is required (`http`, `https`, or `tcp` — how the proxy treats the port); `number` must be 80 to 65000 and not platform-reserved (8012, 8022, 9090, 9091, 15000, 15001, 15006, 15020, 15021, 15090, 41000).
+ - **Ports below 80 (22, 25, 53) cannot be listed.** To reach a low port, allow the CIDR and leave `outboundAllowPort` unset — the CIDR path then opens all ports.
+ - **Private ranges are silently stripped from `outboundAllowCIDR` on managed locations** (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 100.64/10, IPv6 ULA): allowing them does nothing, with no error. Reaching a VPC or datacenter takes a wormhole agent (`native-networking`). BYOK clusters keep private ranges.
- ### Header-Based Filtering (Inbound HTTP)
+ ## Header filters (inbound, HTTP only)
- Each filter matches a header `key` with either `allowedValues` (allow-list) or `blockedValues` (block-list) — **never both on the same filter**. Combine multiple filters, each on a different key:
+ Each filter names a header `key` (max 128 chars) plus exactly ONE of `allowedValues` or `blockedValues` — RE2 regexes; anchor with `^...$` (a bare `bar` also matches `barbell`).
+
```yaml
firewallConfig:
external:
- inboundAllowCIDR:
- - 0.0.0.0/0
+ inboundAllowCIDR: [0.0.0.0/0]
http:
inboundHeaderFilter:
- - key: x-api-version # allow-list: reject everything else for this header
- allowedValues:
- - "^v2$"
- - key: user-agent # block-list: allow everything else
- blockedValues:
- - "^BadBot.*$"
- - "^Scraper.*$"
+ - key: x-api-version
+ allowedValues: ["^v2$"]
+ - key: user-agent
+ blockedValues: ["^BadBot.*", "^Scraper.*"]
```
- **Header filter values are RE2 regular expressions.** Use `^...$` to match whole strings; a bare substring matches partially (`^bar$` vs. `bar` which also matches `barbell`). When multiple values are listed, the filter matches if **any** value matches — not all.
+ Matching is OR across everything: a request is rejected if ANY `blockedValues` pattern matches (checked first), and — once at least one allow filter exists — admitted only if ANY `allowedValues` pattern matches. Two allow filters on different headers are alternatives, not both-required; a request missing the header fails its allow filter. **Mesh-internal traffic (10.0.0.0/8 sources) bypasses header filters entirely** — test from outside, not from another workload.
- ### Geo-Filtering (Country / Region / City)
+ ## Geo filtering (country / region / city / ASN)
- **Two-step setup:** enable geo headers on the load balancer with custom header names, then filter on those names in `inboundHeaderFilter`:
+ Two steps: enable geo headers on the workload load balancer (you pick the header names), then filter on those names:
+
```yaml
spec:
loadBalancer:
geoLocation:
enabled: true
- headers:
+ headers: # at least one; names unique; values overwrite client-sent headers
country: x-country
- region: x-region
- city: x-city
- asn: x-asn
firewallConfig:
external:
- inboundAllowCIDR:
- - 0.0.0.0/0
+ inboundAllowCIDR: [0.0.0.0/0]
http:
inboundHeaderFilter:
- key: x-country
- allowedValues:
- - United States
- - Canada
+ allowedValues: ["^US$", "^CA$"]
```
- - You choose the header names; the workload receives the resolved values on each request.
- - At least one of `asn`, `city`, `country`, or `region` must be configured; header names must be **unique** per type.
- - Geo filtering only affects workloads with an HTTP port; no effect on TCP-only workloads.
+ The proxy resolves values from MaxMind GeoLite2 on each request: `country` is the two-letter ISO code (`US`, never `United States`), `region` the subdivision code, `city` the English city name, `asn` the AS number. Echo the headers from the app once before writing filters. HTTP ports only.
- ## Internal Firewall (Workload-to-Workload)
+ ## Internal firewall (workload to workload)
- `inboundAllowType` is one of `none`, `same-gvc`, `same-org`, `workload-list`. For `workload-list`, name the allowed workloads (cross-GVC supported):
+ `internal.inboundAllowType`: `none` (default), `same-gvc`, `same-org`, or `workload-list`. The admitted identity is the calling workload itself — all its replicas.
+
```yaml
firewallConfig:
internal:
- inboundAllowType: workload-list # or: none, same-gvc, same-org
+ inboundAllowType: workload-list
inboundAllowWorkload:
- - //gvc/my-gvc/workload/frontend
- - //gvc/other-gvc/workload/backend
+ - //gvc/GVC/workload/frontend # GVC segment REQUIRED; //workload/NAME is rejected
+ - /org/ORG/gvc/OTHER-GVC/workload/backend
+ - cpln://internal/keda # required when a KEDA trigger source is a CP workload
+ - //agent/DC-AGENT # inbound from behind a wormhole agent (native-networking)
```
- ## Service-to-Service Communication
-
- Internal DNS form: `http://WORKLOAD.GVC.cpln.local:PORT` (plain HTTP — the sidecar adds mTLS automatically, never `https://`):
- ```bash
- curl http://api-service.my-gvc.cpln.local:8080/health # same-GVC: free, low latency
- curl http://shared-service.other-gvc.cpln.local:3000/data # cross-GVC: needs same-org, egress charges
- ```
+ - `inboundAllowWorkload` is honored under `same-gvc` too — add specific cross-GVC callers without going `same-org`.
+ - Links are validated for shape only, never existence — a typo silently denies the caller.
+ - Internal calls use `http://WORKLOAD.GVC.cpln.local:PORT` (the container port) — plain `http://`, the sidecar adds mTLS. Cross-GVC calls may span locations and then incur egress charges.
- ## Load Balancers (Summary)
+ ## Load balancers (summary)
- | Type | Scope | Custom Ports | Static IPs | Wildcard Hosts |
+ | Type | Scope | Ports | Static IPs | Wildcard hosts |
|---|---|---|---|---|
- | Default (shared) | All workloads | No (HTTP/HTTPS on 80/443) | No | No |
- | Direct | Per-workload | Yes (TCP/UDP/HTTP/HTTPS/WS) | Via IP Set | No |
- | Dedicated | Per-GVC | Yes | Via IP Set | Yes |
+ | Shared (default) | all workloads | HTTP/HTTPS on 80/443 | no | no |
+ | Direct | per workload | TCP/UDP, externalPort 22 to 32768 | via IP set | no |
+ | Dedicated | per GVC (`update_gvc`) | custom domain ports/protocols | via IP set | yes |
- Minimal Direct LB (custom TCP port, per workload):
```yaml
spec:
loadBalancer:
direct:
enabled: true
ports:
- - externalPort: 5432
- protocol: TCP
+ - externalPort: 5432 # 22 to 32768
+ protocol: TCP # TCP or UDP
containerPort: 5432
```
- Full LB config, static IP reservation, and dedicated LB setup: see the **cpln-ipset-load-balancing** skill.
-
- ## Quick Reference
-
- ### Recommended: MCP
-
- `firewallConfig` (and `loadBalancer`) live in the workload spec — patch them with the typed workload tools:
+ Direct LB does not terminate TLS (the workload owns its certificates), and its traffic still passes the inbound CIDR rules. `geoLocation` and `replicaDirect` (stateful only) also live under `spec.loadBalancer`. Dedicated LB is a GVC setting (`loadBalancer.dedicated: true`, charged per location) that also carries `trustedProxies` (0 to 2 — which X-Forwarded-For hop counts as the client IP for logging) and a GVC-level `ipSet`. Static IPs and full LB detail: `ipset-load-balancing`.
- 1. `mcp__cpln__get_resource` (kind="workload") — read current `spec.firewallConfig` / `spec.loadBalancer` for a rollback baseline (`update_workload` is PATCH — only sent fields change).
- 2. `mcp__cpln__update_workload` — set `firewallConfig` (external inbound/outbound, internal, header filters). Load balancer is separate: `mcp__cpln__configure_workload_load_balancer`.
- 3. `mcp__cpln__list_deployments` — poll until ready; rules apply within about a minute and trigger a new deployment.
+ ## Verify
- Use `mcp__cpln__create_workload` to stand up a new workload with firewall rules from the start. Reserve static IPs for Direct/Dedicated LBs with `mcp__cpln__create_ipset` / `mcp__cpln__get_resource` (kind="ipset") (see **cpln-ipset-load-balancing**).
+ 1. `mcp__cpln__get_resource` (kind="workload") — read `spec.firewallConfig` before changing it, and send the COMPLETE desired `firewallConfig` on update (it replaces as a unit, not field-by-field).
+ 2. `mcp__cpln__list_deployments` — wait for the new version to report ready in every location.
+ 3. Probe: inbound with `curl` from an allowed and a blocked vantage; outbound from inside via `mcp__cpln__workload_exec` running `curl -sv https://HOST`.
- ### Fallback: CLI
+ ## Troubleshooting
- When MCP is unavailable or unauthenticated, edit the manifest and apply it. Ground the spec shape with `mcp__cpln__get_resource_schema` (or `cpln workload --help`) first:
- ```bash
- cpln workload get WORKLOAD_NAME --gvc GVC_NAME -o yaml > workload.yaml
- # edit spec.firewallConfig in workload.yaml
- cpln apply --file workload.yaml --gvc GVC_NAME
- ```
+ | Symptom | Cause / fix |
+ |---|---|
+ | Outbound to a VPC/private IP fails though its CIDR is allowed | Private ranges are stripped on managed locations — use a wormhole agent (`native-networking`) |
+ | Hostname egress broke after adding `outboundAllowPort` | The list replaced 80/443/445 — add 80/443 back |
+ | Need outbound to port 22/25/53 | Below the allowed 80-65000 range — allow the CIDR and leave `outboundAllowPort` unset |
+ | Header/geo filter not enforced in tests | Testing from another workload (10.0.0.0/8 bypasses header filters), or the port is `tcp` protocol (filters are HTTP-only) |
+ | Geo allow-list blocks everyone | Values are ISO codes (`^US$`) — full country names never match; echo the header to confirm |
+ | `workload-list` caller still denied | Link is missing the GVC segment, or has a typo (existence is never validated) |
+ | KEDA scaler cannot reach its workload trigger source | Add `cpln://internal/keda` to that workload's `inboundAllowWorkload` |
+ | Firewall seems ignored for one container | `runAsUser: 1337` escapes the mesh and its firewall (see `workload`) |
- ### MCP Tools
+ ## Quick reference
| Tool | Purpose |
|---|---|
- | `mcp__cpln__get_resource` (kind="workload") | Inspect current `spec.firewallConfig` and `spec.loadBalancer` before changing them. |
- | `mcp__cpln__update_workload` | Patch `firewallConfig` on an existing workload (PATCH — only sent fields change). |
- | `mcp__cpln__configure_workload_load_balancer` | Set/clear the workload load balancer (direct, geo headers, replicaDirect). |
- | `mcp__cpln__create_workload` | Create a new workload with firewall rules from the start (load balancer is set separately). |
- | `mcp__cpln__create_ipset` / `mcp__cpln__get_resource` (kind="ipset") | Reserve and inspect static public IPs for Direct/Dedicated load balancers and IP allow-lists. |
- | `mcp__cpln__list_deployments` | Poll deployment readiness after a firewall change lands. |
- | `mcp__cpln__get_workload_logs` | Query workload logs to diagnose connectivity issues. |
+ | `mcp__cpln__update_workload` | Patch `firewallConfig` (send it complete) or `public` |
+ | `mcp__cpln__create_workload` | Decide exposure in the create call: `public: true` or an explicit `firewallConfig` |
+ | `mcp__cpln__configure_workload_load_balancer` | Set `spec.loadBalancer` (direct, geo headers, replicaDirect); `remove: true` clears it |
+ | `mcp__cpln__update_gvc` | Dedicated LB, `trustedProxies`, GVC-level `ipSet` |
+ | `mcp__cpln__get_resource` (kind="workload") / `mcp__cpln__list_deployments` | Read back config; confirm the rollout |
+ | `mcp__cpln__workload_exec` | In-pod curl to test outbound rules |
- ### Related Skills
+ CLI fallback (no MCP, or CI/CD with `CPLN_TOKEN`): `cpln workload get WORKLOAD --gvc GVC -o yaml > w.yaml`, edit `spec.firewallConfig`, then `cpln apply --file w.yaml --gvc GVC`.
- - **cpln-workload** — Start here: the primary workload skill (types, defaults, spec shape) that routes here for firewall & exposure.
- - **cpln-cdn-rate-limiting** — CDN setup, caching policies, and rate limiting configuration
- - **cpln-ipset-load-balancing** — Static IP reservation, Direct and Dedicated load balancer configuration, and domain routing
- - **cpln-native-networking** — PrivateLink, Private Service Connect, and agent-based private connectivity
- - **cpln-workload-security** — JWT authentication, mTLS, and direct load balancer security configuration
+ ## Related skills
+ - **workload** — start here: types, spec shape, exposure defaults, internal DNS, LB picker
+ - **ipset-load-balancing** — static IPs, direct/dedicated LB detail, replicaDirect
+ - **native-networking** — wormhole agents, PrivateLink/PSC: the answer for private-network traffic
+ - **cdn-rate-limiting** — CDN in front of workloads, rate limiting
+ - **workload-security** — JWT authentication, mTLS hardening, direct-LB security
+
## Documentation
- [Firewall Reference](https://docs.controlplane.com/reference/workload/firewall.md)
- [Load Balancing Reference](https://docs.controlplane.com/reference/workload/load-balancing.md)
- [Service-to-Service Guide](https://docs.controlplane.com/guides/service-to-service.md)
- - [GVC Reference](https://docs.controlplane.com/reference/gvc.md)