← Control PlaneCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Control Plane
Snapshot Sep 30, 2026 · 23:00 UTC · version 1.0.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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.",
"included_files": [],
"skill_md_contents": "---\nname: firewall-networking\ndescription: \"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.\"\n---\n\n# Firewall & Networking\n\nDeep 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).\n\n## How rules are enforced\n\n**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.\n\n**Outbound** has two separate paths, which is why CIDR rules beat hostname rules:\n\n- **CIDR path** — traffic to `outboundAllowCIDR` ranges bypasses the sidecar and exits directly, on ALL ports unless `outboundAllowPort` is set.\n- **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.\n- `outboundBlockedCIDR` is subtracted at the network layer and beats both paths — an allowed hostname that resolves into a blocked range still fails.\n- Outbound is fully open only with the literal `0.0.0.0/0` in `outboundAllowCIDR`.\n\n## External inbound\n\n```yaml\nfirewallConfig:\n external:\n inboundAllowCIDR: # max 250 entries; deduped and sorted on save\n - 0.0.0.0/0 # or specific: 203.0.113.0/24, 198.51.100.10\n inboundBlockedCIDR: # no max; wins over the allow list\n - 192.0.2.0/24\n```\n\n## External outbound\n\n```yaml\nfirewallConfig:\n external:\n outboundAllowCIDR:\n - 198.51.100.0/24 # all ports open to this range while outboundAllowPort is unset\n outboundAllowHostname: # lowercase; single wildcard on the prefix only; max 128 chars\n - api.stripe.com\n - \"*.amazonaws.com\"\n outboundBlockedCIDR:\n - 203.0.113.7\n```\n\nSource-verified traps:\n\n- **`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).\n- **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.\n- **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.\n\n## Header filters (inbound, HTTP only)\n\nEach 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`).\n\n```yaml\nfirewallConfig:\n external:\n inboundAllowCIDR: [0.0.0.0/0]\n http:\n inboundHeaderFilter:\n - key: x-api-version\n allowedValues: [\"^v2$\"]\n - key: user-agent\n blockedValues: [\"^BadBot.*\", \"^Scraper.*\"]\n```\n\nMatching 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.\n\n## Geo filtering (country / region / city / ASN)\n\nTwo steps: enable geo headers on the workload load balancer (you pick the header names), then filter on those names:\n\n```yaml\nspec:\n loadBalancer:\n geoLocation:\n enabled: true\n headers: # at least one; names unique; values overwrite client-sent headers\n country: x-country\n firewallConfig:\n external:\n inboundAllowCIDR: [0.0.0.0/0]\n http:\n inboundHeaderFilter:\n - key: x-country\n allowedValues: [\"^US$\", \"^CA$\"]\n```\n\nThe 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.\n\n## Internal firewall (workload to workload)\n\n`internal.inboundAllowType`: `none` (default), `same-gvc`, `same-org`, or `workload-list`. The admitted identity is the calling workload itself — all its replicas.\n\n```yaml\nfirewallConfig:\n internal:\n inboundAllowType: workload-list\n inboundAllowWorkload:\n - //gvc/GVC/workload/frontend # GVC segment REQUIRED; //workload/NAME is rejected\n - /org/ORG/gvc/OTHER-GVC/workload/backend\n - cpln://internal/keda # required when a KEDA trigger source is a CP workload\n - //agent/DC-AGENT # inbound from behind a wormhole agent (native-networking)\n```\n\n- `inboundAllowWorkload` is honored under `same-gvc` too — add specific cross-GVC callers without going `same-org`.\n- Links are validated for shape only, never existence — a typo silently denies the caller.\n- 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.\n\n## Load balancers (summary)\n\n| Type | Scope | Ports | Static IPs | Wildcard hosts |\n|---|---|---|---|---|\n| Shared (default) | all workloads | HTTP/HTTPS on 80/443 | no | no |\n| Direct | per workload | TCP/UDP, externalPort 22 to 32768 | via IP set | no |\n| Dedicated | per GVC (`update_gvc`) | custom domain ports/protocols | via IP set | yes |\n\n```yaml\nspec:\n loadBalancer:\n direct:\n enabled: true\n ports:\n - externalPort: 5432 # 22 to 32768\n protocol: TCP # TCP or UDP\n containerPort: 5432\n```\n\nDirect 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`.\n\n## Verify\n\n1. `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).\n2. `list_deployments` — wait for the new version to report ready in every location.\n3. Probe inbound with `curl` from an allowed and a blocked vantage. For an outbound probe from inside the container, use the `cpln` CLI after reading the `cpln` skill.\n\n## Troubleshooting\n\n| Symptom | Cause / fix |\n|---|---|\n| 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`) |\n| Hostname egress broke after adding `outboundAllowPort` | The list replaced 80/443/445 — add 80/443 back |\n| Need outbound to port 22/25/53 | Below the allowed 80-65000 range — allow the CIDR and leave `outboundAllowPort` unset |\n| 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) |\n| Geo allow-list blocks everyone | Values are ISO codes (`^US$`) — full country names never match; echo the header to confirm |\n| `workload-list` caller still denied | Link is missing the GVC segment, or has a typo (existence is never validated) |\n| KEDA scaler cannot reach its workload trigger source | Add `cpln://internal/keda` to that workload's `inboundAllowWorkload` |\n| Firewall seems ignored for one container | `runAsUser: 1337` escapes the mesh and its firewall (see `workload`) |\n\n## Quick reference\n\n| Tool | Purpose |\n|---|---|\n| `update_workload` | Patch `firewallConfig` (send it complete) or `public` |\n| `create_workload` | Decide exposure in the create call: `public: true` or an explicit `firewallConfig` |\n| `configure_workload_load_balancer` | Set `spec.loadBalancer` (direct, geo headers, replicaDirect); `remove: true` clears it |\n| `update_gvc` | Dedicated LB, `trustedProxies`, GVC-level `ipSet` |\n| `get_resource` (kind=\"workload\") / `list_deployments` | Read back config; confirm the rollout |\n\nCLI 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`.\n\n## Related skills\n\n- **workload** — start here: types, spec shape, exposure defaults, internal DNS, LB picker\n- **ipset-load-balancing** — static IPs, direct/dedicated LB detail, replicaDirect\n- **native-networking** — wormhole agents, PrivateLink/PSC: the answer for private-network traffic\n- **cdn-rate-limiting** — CDN in front of workloads, rate limiting\n- **workload-security** — JWT authentication, mTLS hardening, direct-LB security\n\n## Documentation\n\n- [Firewall Reference](https://docs.controlplane.com/reference/workload/firewall.md)\n- [Load Balancing Reference](https://docs.controlplane.com/reference/workload/load-balancing.md)\n- [Service-to-Service Guide](https://docs.controlplane.com/guides/service-to-service.md)\n"
}SHA-256: 8ad042d802d759a26cb54becd5d1e95578de60cb355f9f8af0f1f83dbfdf1f26