← Control PlaneCONTENT HISTORY

Update to Control Plane

Snapshot Oct 30, 2026 · 00:05 UTC · version 1.2.0

Collection source: downloaded plugin package.

WHAT CHANGED

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.

Save Control Plane and check future changes in Following.

Up to 10 plugins free · no card required.

All fields and original values.

Skill instructions

Before

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`).

After

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 JSON

SKILL.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

BEFORE
"---\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"
AFTER
"---\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