Update to Control Plane
Snapshot Oct 30, 2026 · 00:05 UTC · version 1.2.0
Collection source: downloaded plugin package.
Instructions updated for firewall-networking
Skill instructions — Now says: `update_workload`, or use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to...
Observed in instructions or declared skills. Runtime behavior has not been tested.
Up to 10 plugins free · no card required.
All fields and original values.
Skill instructions
Oct 28, 2026 · 18:05 UTC
`update_workload` — or `public: true`, the shortcut that opens inbound AND outbound to `0.0.0.0/0` (mutually exclusive with an explicit `firewallConfig`).
Oct 30, 2026 · 00:05 UTC
`update_workload`, or use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to `0.0.0.0/0` (on `update_workload` it adds `0.0.0.0/0` to both external allow lists and k...
Compare saved observations
Download comparison JSONSKILL.md text changes
− removed · + added
--- before +++ after @@ -5,7 +5,7 @@ # Firewall & Networking -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). +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 use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to `0.0.0.0/0` (on `update_workload` it adds `0.0.0.0/0` to both external allow lists and keeps every other rule), and `public: false` on `update_workload` removes only `0.0.0.0/0` and `::/0` from external inbound. 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). ## How rules are enforced
Full technical diff · 1 changed fields
changed /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- `mcp__cpln__allow_workload_access` admits named callers without hand-editing this block.\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. `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).\n2. `mcp__cpln__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"
"---\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 use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to `0.0.0.0/0` (on `update_workload` it adds `0.0.0.0/0` to both external allow lists and keeps every other rule), and `public: false` on `update_workload` removes only `0.0.0.0/0` and `::/0` from external inbound. 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- `mcp__cpln__allow_workload_access` admits named callers without hand-editing this block.\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. `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).\n2. `mcp__cpln__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"
Full snapshot data
{
"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": [],
"name": "firewall-networking",
"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 use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to `0.0.0.0/0` (on `update_workload` it adds `0.0.0.0/0` to both external allow lists and keeps every other rule), and `public: false` on `update_workload` removes only `0.0.0.0/0` and `::/0` from external inbound. 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- `mcp__cpln__allow_workload_access` admits named callers without hand-editing this block.\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. `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).\n2. `mcp__cpln__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"
}SHA-256 of public snapshot: 2661735e07cc2d1e6a3e72849ce536006c80abb44c3754548ee67e0ef5764075