diff --git a/charts/zero-trust-mesh/Chart.yaml b/charts/zero-trust-mesh/Chart.yaml index f99c7dc..6efbfd4 100644 --- a/charts/zero-trust-mesh/Chart.yaml +++ b/charts/zero-trust-mesh/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: zero-trust-mesh -version: 0.1.1 +version: 0.1.2 description: Helm chart for Kubernetes NetworkPolicy + Istio zero-trust service communication appVersion: "1.0" type: application diff --git a/charts/zero-trust-mesh/README.md b/charts/zero-trust-mesh/README.md index b21098f..ad566a6 100644 --- a/charts/zero-trust-mesh/README.md +++ b/charts/zero-trust-mesh/README.md @@ -9,7 +9,7 @@ Minimal Helm chart for strict Kubernetes + Istio zero-trust communication. - STRICT mTLS via Istio `PeerAuthentication` - Default deny via Istio `AuthorizationPolicy` - Service-account based allow rules with optional HTTP methods/paths -- External egress only to approved hosts via Istio egress gateway +- External egress only to approved hosts or IP blocks ## Two deployment modes @@ -50,11 +50,15 @@ allowTo: methods: ["GET", "POST"] paths: ["/api/*"] - hosts: ["api.stripe.com"] + - ips: ["192.0.2.10"] + ports: + - number: 443 + protocol: TCP ``` ## Values design -`allowTo` is a single list with two entry types: +`allowTo` is a single list with three entry types: - Service rule: - `service` (required) @@ -68,6 +72,11 @@ allowTo: - `hosts` (list of approved external hosts) - `ports` (optional list; merged with defaults `80/HTTP` and `443/HTTPS`) - `paths` can be provided in values for future/egress-gateway routing use, but are not enforced by `ServiceEntry`-only mode +- IP rule: + - `ips` (list of approved external destination IPs or CIDR blocks) + - `ports` (optional list; defaults to `443/TCP`) + - single IPv4 addresses are rendered as `/32` CIDRs for `NetworkPolicy` `ipBlock` + - renders both an Istio `ServiceEntry` with `resolution: NONE` and a workload-scoped egress `NetworkPolicy` Source service account defaults to `workload`, or can be set with top-level `serviceAccount`. @@ -90,6 +99,7 @@ Most security defaults are now implicit in templates. Advanced overrides can sti | `allowTo[].serviceAccount` | Optional target service account override for AuthorizationPolicy naming | `allowTo[].service` | | `allowTo[].methods` / `allowTo[].paths` | Optional Istio operation filters | `["GET"]`, `["/api/*"]` | | `allowTo[].hosts` | Approved external hosts for ServiceEntry-based egress | `["api.stripe.com"]` | +| `allowTo[].ips` | Approved external destination IPs or CIDR blocks for direct IP egress | `["192.0.2.10"]` | ## Install diff --git a/charts/zero-trust-mesh/templates/_helpers.tpl b/charts/zero-trust-mesh/templates/_helpers.tpl index b88eb22..252f791 100644 --- a/charts/zero-trust-mesh/templates/_helpers.tpl +++ b/charts/zero-trust-mesh/templates/_helpers.tpl @@ -10,6 +10,26 @@ {{- regexReplaceAll "[^a-z0-9-]+" (lower .) "-" | trunc 63 | trimSuffix "-" -}} {{- end -}} +{{- define "ztm.ipCidr" -}} +{{- $ip := printf "%v" . -}} +{{- if contains "/" $ip -}} +{{- $ip -}} +{{- else if contains ":" $ip -}} +{{- printf "%s/128" $ip -}} +{{- else -}} +{{- printf "%s/32" $ip -}} +{{- end -}} +{{- end -}} + +{{- define "ztm.networkPolicyProtocol" -}} +{{- $protocol := default "TCP" . | upper -}} +{{- if or (eq $protocol "UDP") (eq $protocol "SCTP") -}} +{{- $protocol -}} +{{- else -}} +TCP +{{- end -}} +{{- end -}} + {{- define "ztm.workloadNamespace" -}} {{- $svc := .Values.serviceConfig | default (dict) -}} {{- default .Release.Namespace (default $svc.namespace .Values.namespace) -}} diff --git a/charts/zero-trust-mesh/templates/istio-serviceentries.yaml b/charts/zero-trust-mesh/templates/istio-serviceentries.yaml index b11e943..68a9f50 100644 --- a/charts/zero-trust-mesh/templates/istio-serviceentries.yaml +++ b/charts/zero-trust-mesh/templates/istio-serviceentries.yaml @@ -49,5 +49,38 @@ spec: {{- end }} {{- end }} {{- end }} +{{ if .ips }} +{{- $defaultPorts := list (dict "number" 443 "protocol" "TCP") -}} +{{- $ports := .ports | default $defaultPorts -}} +{{- $addresses := list -}} +{{- range .ips }} +{{- $addresses = append $addresses (include "ztm.ipCidr" .) -}} +{{- end -}} +{{- $hashSource := printf "%s-%s" (join "," $addresses) (toJson $ports) -}} +{{- $resourceName := printf "external-ip-%s" ($hashSource | sha256sum | trunc 10) }} +--- +apiVersion: networking.istio.io/v1 +kind: ServiceEntry +metadata: + name: {{ $resourceName }} + namespace: {{ $workloadNamespace }} +spec: + hosts: + - {{ printf "%s.ztm.local" $resourceName | quote }} + addresses: + {{- range $addresses }} + - {{ . | quote }} + {{- end }} + location: MESH_EXTERNAL + resolution: NONE + ports: + {{- range $port := $ports }} + {{- $number := required "allowTo[].ips ports[].number is required" $port.number }} + {{- $protocol := default "TCP" $port.protocol | upper }} + - number: {{ $number }} + name: {{ default (include "ztm.sanitizeName" (printf "%s-%v" ($protocol | lower) $number)) $port.name }} + protocol: {{ $protocol }} + {{- end }} +{{- end }} {{- end }} {{- end }} diff --git a/charts/zero-trust-mesh/templates/networkpolicy-ip-egress.yaml b/charts/zero-trust-mesh/templates/networkpolicy-ip-egress.yaml new file mode 100644 index 0000000..b303516 --- /dev/null +++ b/charts/zero-trust-mesh/templates/networkpolicy-ip-egress.yaml @@ -0,0 +1,40 @@ +{{- $np := .Values.networkPolicy | default (dict) -}} +{{- if and (ne $np.enabled false) .Values.allowTo }} +{{- $workloadNamespace := include "ztm.workloadNamespace" . -}} +{{- $sourceWorkload := include "ztm.workloadName" . -}} +{{- range .Values.allowTo }} +{{- if .ips }} +{{- $defaultPorts := list (dict "number" 443 "protocol" "TCP") -}} +{{- $ports := .ports | default $defaultPorts -}} +{{- $addresses := list -}} +{{- range .ips }} +{{- $addresses = append $addresses (include "ztm.ipCidr" .) -}} +{{- end -}} +{{- $hashSource := printf "%s-%s" (join "," $addresses) (toJson $ports) -}} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-{{ include "ztm.sanitizeName" (printf "%s-to-ip-%s" $sourceWorkload ($hashSource | sha256sum | trunc 10)) }} + namespace: {{ $workloadNamespace }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ $sourceWorkload }} + policyTypes: + - Egress + egress: + - to: + {{- range $addresses }} + - ipBlock: + cidr: {{ . | quote }} + {{- end }} + ports: + {{- range $port := $ports }} + {{- $number := required "allowTo[].ips ports[].number is required" $port.number }} + - port: {{ $number }} + protocol: {{ include "ztm.networkPolicyProtocol" $port.protocol }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/zero-trust-mesh/values.yaml b/charts/zero-trust-mesh/values.yaml index a90fd60..37f460a 100644 --- a/charts/zero-trust-mesh/values.yaml +++ b/charts/zero-trust-mesh/values.yaml @@ -10,6 +10,7 @@ namespaceResourcesEnabled: false # Single allowTo list: # - service rule: workload -> service # - hosts rule: approved external hosts (default ports: 80/HTTP and 443/HTTPS) +# - ips rule: approved external IP/CIDR egress (default port: 443/TCP) allowTo: - service: backend # Optional target pod selector override; defaults to: @@ -28,3 +29,12 @@ allowTo: # protocol: HTTP # - number: 443 # protocol: HTTPS + + - ips: ["192.0.2.10"] + # Single IPs are normalized to /32 for NetworkPolicy ipBlock. + # CIDRs like 198.51.100.0/24 can also be used. + # Optional custom ports/protocols for this IP group. + # Defaults to 443/TCP. + # ports: + # - number: 443 + # protocol: TCP diff --git a/examples/zero-trust-mesh/ip-egress.yaml b/examples/zero-trust-mesh/ip-egress.yaml new file mode 100644 index 0000000..9581f3a --- /dev/null +++ b/examples/zero-trust-mesh/ip-egress.yaml @@ -0,0 +1,12 @@ +# helm template ztm-ip-egress ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/ip-egress.yaml +workload: frontend +namespaceResourcesEnabled: false +allowTo: + - ips: + - 192.0.2.10 + - 198.51.100.0/24 + ports: + - number: 443 + protocol: TCP + - number: 8080 + protocol: HTTP diff --git a/examples/zero-trust-mesh/target-pod-labels.yaml b/examples/zero-trust-mesh/target-pod-labels.yaml new file mode 100644 index 0000000..1141892 --- /dev/null +++ b/examples/zero-trust-mesh/target-pod-labels.yaml @@ -0,0 +1,11 @@ +# helm template ztm-target-pod-labels ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/target-pod-labels.yaml +workload: frontend +namespaceResourcesEnabled: false +allowTo: + - service: backend + targetPodLabels: + app: backend + component: api + port: 8080 + methods: ["GET"] + paths: ["/api/*"] diff --git a/specs/013-zero-trust-ip-egress/checklists/requirements.md b/specs/013-zero-trust-ip-egress/checklists/requirements.md new file mode 100644 index 0000000..39baa45 --- /dev/null +++ b/specs/013-zero-trust-ip-egress/checklists/requirements.md @@ -0,0 +1,24 @@ +# Requirements Quality Checklist: Zero Trust IP Egress + +**Purpose**: Validate specification quality before implementation handoff +**Created**: 2026-05-08 +**Feature**: `specs/013-zero-trust-ip-egress/spec.md` + +## Content Quality + +- [x] No implementation details leak into user stories beyond chart-rendering behavior needed for acceptance. +- [x] Requirements are testable through Helm render output. +- [x] Requirements avoid ambiguous "support IPs" phrasing by defining ServiceEntry and NetworkPolicy outcomes. +- [x] Public values contract changes are documented. + +## Requirement Completeness + +- [x] User scenarios cover direct IP egress, existing behavior preservation, and discoverability. +- [x] Acceptance criteria include single IP and CIDR behavior. +- [x] Edge cases include default ports and protocol compatibility. +- [x] Success criteria are measurable with render checks and chart linting. +- [x] Constitution-required example and version bump are captured. + +## Validation Result + +Validation completed 2026-05-08. Spec is ready for `/speckit.plan`, `/speckit.tasks`, or implementation review. diff --git a/specs/013-zero-trust-ip-egress/contracts/render-contract.md b/specs/013-zero-trust-ip-egress/contracts/render-contract.md new file mode 100644 index 0000000..db6907c --- /dev/null +++ b/specs/013-zero-trust-ip-egress/contracts/render-contract.md @@ -0,0 +1,54 @@ +# Render Contract: Zero Trust IP Egress + +## Values Input + +```yaml +workload: frontend +namespaceResourcesEnabled: false +allowTo: + - ips: + - 192.0.2.10 + - 198.51.100.0/24 + ports: + - number: 443 + protocol: TCP + - number: 8080 + protocol: HTTP +``` + +## Required Rendered ServiceEntry + +The chart MUST render a `networking.istio.io/v1` `ServiceEntry` with: + +- `metadata.namespace: default` when rendered with `-n default` +- `spec.addresses` containing `"192.0.2.10/32"` and `"198.51.100.0/24"` +- `spec.location: MESH_EXTERNAL` +- `spec.resolution: NONE` +- `spec.ports` containing port `443` protocol `TCP` +- `spec.ports` containing port `8080` protocol `HTTP` + +The ServiceEntry host name may be synthetic, but it MUST be deterministic for the same values. + +## Required Rendered NetworkPolicy + +The chart MUST render a `networking.k8s.io/v1` `NetworkPolicy` with: + +- `metadata.namespace: default` when rendered with `-n default` +- `spec.podSelector.matchLabels.app.kubernetes.io/name: frontend` +- `spec.policyTypes` containing `Egress` +- `spec.egress[].to[].ipBlock.cidr` containing `"192.0.2.10/32"` and `"198.51.100.0/24"` +- `spec.egress[].ports` containing port `443` protocol `TCP` +- `spec.egress[].ports` containing port `8080` protocol `TCP`, because Kubernetes NetworkPolicy does not accept `HTTP` as a protocol + +## Default Port Contract + +When an IP egress rule omits `ports`, the rendered ServiceEntry and NetworkPolicy MUST allow `443/TCP`. + +## Regression Contract + +Adding IP egress support MUST NOT change: + +- `allowTo[].service` NetworkPolicy ingress rendering +- `allowTo[].service` AuthorizationPolicy rendering +- `allowTo[].hosts` DNS ServiceEntry rendering +- namespace baseline resources diff --git a/specs/013-zero-trust-ip-egress/data-model.md b/specs/013-zero-trust-ip-egress/data-model.md new file mode 100644 index 0000000..11fe72f --- /dev/null +++ b/specs/013-zero-trust-ip-egress/data-model.md @@ -0,0 +1,69 @@ +# Data Model: Zero Trust IP Egress + +## IpEgressRule + +An `allowTo` entry that allows a source workload to egress to literal destination IP addresses or CIDR ranges. + +```yaml +allowTo: + - ips: + - 192.0.2.10 + - 198.51.100.0/24 + ports: + - number: 443 + protocol: TCP +``` + +Fields: + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `ips` | list(string) | yes | Destination IP addresses or CIDR blocks. | +| `ports` | list(IpPort) | no | Defaults to one entry: `443/TCP`. | + +## IpPort + +A port allowed for a direct IP egress rule. + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `number` | integer | yes | Destination port rendered in ServiceEntry and NetworkPolicy. | +| `protocol` | string | no | Defaults to `TCP`; ServiceEntry uses the configured protocol, NetworkPolicy renders only Kubernetes-compatible L4 protocols. | +| `name` | string | no | Optional ServiceEntry port name override. | + +## NormalizedIpBlock + +The CIDR-form address rendered into manifests. + +Rules: + +- Input containing `/` is treated as already normalized and rendered unchanged. +- Input containing `:` and no `/` is treated as IPv6 and rendered with `/128`. +- Any other input without `/` is treated as IPv4 and rendered with `/32`. + +## Rendered Resources + +### IP ServiceEntry + +One `networking.istio.io/v1` ServiceEntry per IP egress rule. + +Key fields: + +- `metadata.namespace`: workload namespace +- `spec.hosts`: synthetic internal host name for the ServiceEntry object +- `spec.addresses`: normalized IP blocks +- `spec.location`: `MESH_EXTERNAL` +- `spec.resolution`: `NONE` +- `spec.ports`: configured IP rule ports or default `443/TCP` + +### IP Egress NetworkPolicy + +One `networking.k8s.io/v1` NetworkPolicy per IP egress rule. + +Key fields: + +- `metadata.namespace`: workload namespace +- `spec.podSelector.matchLabels.app.kubernetes.io/name`: source workload +- `spec.policyTypes`: `Egress` +- `spec.egress[].to[].ipBlock.cidr`: normalized IP blocks +- `spec.egress[].ports`: configured IP rule ports with Kubernetes-compatible protocols diff --git a/specs/013-zero-trust-ip-egress/plan.md b/specs/013-zero-trust-ip-egress/plan.md new file mode 100644 index 0000000..c32fbed --- /dev/null +++ b/specs/013-zero-trust-ip-egress/plan.md @@ -0,0 +1,96 @@ +# Implementation Plan: Zero Trust IP Egress + +**Branch**: `013-zero-trust-ip-egress` | **Date**: 2026-05-08 | **Spec**: `/specs/013-zero-trust-ip-egress/spec.md` +**Input**: Feature specification from `/specs/013-zero-trust-ip-egress/spec.md` + +## Summary + +Add `allowTo[].ips` to the zero-trust-mesh chart so workloads that call external destination IPs directly can be allowed through both Istio and Kubernetes network policy. Render IP rules as Istio ServiceEntries with `addresses` and `resolution: NONE`, plus source-workload egress NetworkPolicies using `ipBlock`. + +## Technical Context + +**Language/Version**: Helm template DSL, YAML manifests +**Primary Dependencies**: Helm 3 CLI, Kubernetes NetworkPolicy `networking.k8s.io/v1`, Istio ServiceEntry `networking.istio.io/v1` +**Storage**: N/A +**Testing**: `helm lint`, `helm template`, focused shell render assertion +**Target Platform**: Kubernetes clusters with a NetworkPolicy provider and Istio sidecar traffic management +**Project Type**: Helm chart repository +**Performance Goals**: Render behavior remains linear in `allowTo` entry count and configured IP count +**Constraints**: Preserve existing service and host rule behavior; expose behavior only through values; add a runnable example for the new public values surface +**Scale/Scope**: One chart (`zero-trust-mesh`), one example under `examples/zero-trust-mesh/`, and Speckit artifacts under `specs/013-zero-trust-ip-egress/` + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [x] **Chart-First**: Work stays inside `charts/zero-trust-mesh`, repo examples, and repo specs. +- [x] **Values Contract**: New consumer-facing behavior is exposed via `values.yaml` as `allowTo[].ips`. +- [x] **Lint & Template**: Plan includes `helm lint` and `helm template` with default, focused test, and example values. +- [x] **Versioning & Compatibility**: Change is backward-compatible and includes a patch version bump. +- [x] **Simplicity & Defaults**: IP support is opt-in and defaults to `443/TCP` only for IP rules. +- [x] **Examples for new abilities**: Plan includes `examples/zero-trust-mesh/ip-egress.yaml`. +- [x] **Example testing and regression**: Plan includes rendering the new example and existing zero-trust-mesh examples. +- [x] **Official documentation before implementation**: Kubernetes NetworkPolicy and Istio ServiceEntry field shapes are checked in official docs. + +## Project Structure + +### Documentation (this feature) + +```text +specs/013-zero-trust-ip-egress/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ └── render-contract.md +├── checklists/ +│ └── requirements.md +└── tasks.md +``` + +### Source Code (repository root) + +```text +charts/ +└── zero-trust-mesh/ + ├── Chart.yaml + ├── README.md + ├── values.yaml + ├── templates/ + │ ├── _helpers.tpl + │ ├── istio-serviceentries.yaml + │ └── networkpolicy-ip-egress.yaml + └── tests/ + ├── ip-egress-values.yaml + └── render-ip-egress.sh + +examples/ +└── zero-trust-mesh/ + └── ip-egress.yaml +``` + +**Structure Decision**: Keep IP egress in a dedicated NetworkPolicy template because it is an egress rule for source workloads, while existing `networkpolicy-flows.yaml` handles service ingress allows. Extend the existing ServiceEntry template so DNS hosts and direct IP entries remain in the same Istio registry-rendering location. + +## Phase 0: Research Plan + +- Confirm Istio ServiceEntry supports `addresses` for VIP/IP matching and `resolution: NONE` for direct IP connections. +- Confirm Kubernetes NetworkPolicy `ipBlock.cidr` supports CIDR egress destinations. +- Confirm existing chart values documentation and test conventions. + +## Phase 1: Design & Contracts Plan + +- Document `IpEgressRule`, `IpPort`, and `NormalizedIpBlock` in `data-model.md`. +- Define render contract for IP ServiceEntry, IP egress NetworkPolicy, default port, and regression cases in `contracts/render-contract.md`. +- Provide quickstart commands for focused render assertions, chart linting, default rendering, and example rendering. +- Re-check constitution compliance after artifact generation. + +## Post-Design Constitution Check + +- [x] No constitution violations remain in the planned implementation. +- [x] Chart version bump is included in tasks. +- [x] New public value is paired with README/values documentation and a runnable example. + +## Complexity Tracking + +No constitution violations requiring justification. diff --git a/specs/013-zero-trust-ip-egress/quickstart.md b/specs/013-zero-trust-ip-egress/quickstart.md new file mode 100644 index 0000000..e2c9701 --- /dev/null +++ b/specs/013-zero-trust-ip-egress/quickstart.md @@ -0,0 +1,45 @@ +# Quickstart: Zero Trust IP Egress + +Run all commands from the repository root. + +## Focused IP Egress Test + +```bash +./charts/zero-trust-mesh/tests/render-ip-egress.sh ./charts/zero-trust-mesh +``` + +Expected: command exits with status `0`. + +## Chart Lint + +```bash +helm lint ./charts/zero-trust-mesh +``` + +Expected: `1 chart(s) linted, 0 chart(s) failed`. + +## Default Render + +```bash +helm template ztm-default ./charts/zero-trust-mesh -n default +``` + +Expected: command exits with status `0` and includes existing service and host outputs plus the default sample IP rule from `values.yaml`. + +## Runnable IP Egress Example + +```bash +helm template ztm-ip-egress ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/ip-egress.yaml +``` + +Expected: command exits with status `0`, renders a ServiceEntry with `resolution: NONE`, and renders a NetworkPolicy with `ipBlock`. + +## Existing Example Regression + +```bash +helm template ztm-target-pod-labels ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/target-pod-labels.yaml +helm template ztm-namespace ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/values.namespace.yaml +helm template ztm-full ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/values.full.yaml +``` + +Expected: each command exits with status `0`. diff --git a/specs/013-zero-trust-ip-egress/research.md b/specs/013-zero-trust-ip-egress/research.md new file mode 100644 index 0000000..f93cb34 --- /dev/null +++ b/specs/013-zero-trust-ip-egress/research.md @@ -0,0 +1,61 @@ +# Research: Zero Trust IP Egress + +## Decisions + +### Add `allowTo[].ips` as the third allow rule type + +**Decision**: Extend the existing single `allowTo` list with an `ips` rule type. + +**Rationale**: The chart already models destination allows as a single list with separate service and host rule shapes. Adding `ips` preserves that values style and avoids creating a parallel top-level egress configuration. + +**Alternatives considered**: + +- Reuse `hosts` for IPs: rejected because direct IP calls are not DNS host/SNI-based and require different Istio and NetworkPolicy fields. +- Add top-level `allowIpEgress`: rejected because it splits allow rules across multiple lists and makes the chart harder to scan. + +### Render ServiceEntry with `addresses` and `resolution: NONE` + +**Decision**: For IP rules, render one Istio `ServiceEntry` containing normalized IP/CIDR values under `spec.addresses`, with `location: MESH_EXTERNAL` and `resolution: NONE`. + +**Rationale**: Direct IP traffic needs an Istio registry entry when outbound traffic policy is `REGISTRY_ONLY`. `resolution: NONE` represents already-resolved direct destination addresses. + +**Alternatives considered**: + +- DNS ServiceEntry: rejected because direct IP traffic has no DNS host to resolve. +- Egress-gateway VirtualService routing: rejected for this change because the existing gateway flow is host/SNI-oriented, while direct IP traffic may not have useful SNI. + +### Render a source-workload egress NetworkPolicy + +**Decision**: Add `templates/networkpolicy-ip-egress.yaml` that selects source workload pods and allows egress to configured IP/CIDR blocks. + +**Rationale**: Namespace default-deny egress blocks direct IP traffic unless a NetworkPolicy egress rule allows the destination. The existing service flow template is ingress-oriented and selects destination pods, so a separate source egress template is clearer. + +### Normalize single IP values to host CIDRs + +**Decision**: Render single IPv4 addresses as `/32` and single IPv6 addresses as `/128`; preserve values that already contain `/`. + +**Rationale**: Kubernetes `NetworkPolicyPeer.ipBlock.cidr` expects CIDR notation. Host CIDRs make single-address input convenient while rendering valid policy blocks. + +## Official Documentation Notes + +- Istio ServiceEntry supports `addresses` as VIPs or CIDR prefixes associated with hosts and supports `resolution: NONE` for cases where the proxy does not resolve the endpoint. +- Kubernetes NetworkPolicy supports `ipBlock.cidr` on ingress or egress peers for IP CIDR selection. +- Kubernetes NetworkPolicy port protocols are L4 protocols (`TCP`, `UDP`, `SCTP`), so Istio protocol labels such as `HTTP` or `HTTPS` cannot be copied directly into NetworkPolicy protocol fields. + +Sources checked: + +- Istio ServiceEntry reference: https://istio.io/latest/docs/reference/config/networking/service-entry/ +- Kubernetes NetworkPolicy concept docs: https://kubernetes.io/docs/concepts/services-networking/network-policies/ +- Kubernetes NetworkPolicy API reference: https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/network-policy-v1/ + +## Validation Notes + +The focused render assertion should verify: + +- an IP rule renders a ServiceEntry +- the ServiceEntry uses `resolution: NONE` +- a single IP renders as `/32` +- a CIDR input is preserved +- an IP rule renders a NetworkPolicy +- the NetworkPolicy uses `ipBlock` +- configured ports appear in both outputs diff --git a/specs/013-zero-trust-ip-egress/spec.md b/specs/013-zero-trust-ip-egress/spec.md new file mode 100644 index 0000000..4f8f677 --- /dev/null +++ b/specs/013-zero-trust-ip-egress/spec.md @@ -0,0 +1,102 @@ +# Feature Specification: Zero Trust IP Egress + +**Feature Branch**: `013-zero-trust-ip-egress` +**Created**: 2026-05-08 +**Status**: Draft +**Input**: User description: "support IP addresses in zero-trust-mesh chart because applications call destination IPs directly and existing allowTo host/service rules cannot authorize IP egress" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Allow direct destination IP egress (Priority: P1) + +As a zero-trust-mesh chart consumer, I can configure an external destination IP address or CIDR in `allowTo`, so workloads that call IPs directly can pass both Istio registry checks and Kubernetes egress policy. + +**Why this priority**: This is the requested production blocker. Existing `service` and `hosts` rule types do not represent direct IP calls. + +**Independent Test**: Render `charts/zero-trust-mesh` with an `allowTo` entry containing `ips`, then verify the output includes an Istio `ServiceEntry` with `addresses` and `resolution: NONE` plus a Kubernetes egress `NetworkPolicy` using `ipBlock`. + +**Acceptance Scenarios**: + +1. **Given** an `allowTo` rule with `ips: ["192.0.2.10"]`, **When** the chart is rendered, **Then** the generated ServiceEntry contains address `192.0.2.10/32` and `resolution: NONE`. +2. **Given** the same rule, **When** the chart is rendered, **Then** the generated NetworkPolicy allows egress to `192.0.2.10/32` through `ipBlock`. +3. **Given** an `allowTo` rule with `ips: ["198.51.100.0/24"]`, **When** the chart is rendered, **Then** the CIDR is preserved in both ServiceEntry addresses and NetworkPolicy ipBlock. + +--- + +### User Story 2 - Preserve existing service and host behavior (Priority: P2) + +As an existing chart consumer, I can keep using `allowTo[].service` and `allowTo[].hosts` without behavior or rendered output changes caused by the IP egress addition. + +**Why this priority**: The new rule type must be opt-in and must not regress existing service-to-service or DNS host egress flows. + +**Independent Test**: Render default values and existing target-pod-label test values, then confirm existing service and host resources still render successfully. + +**Acceptance Scenarios**: + +1. **Given** an existing service allow rule, **When** the chart is rendered, **Then** NetworkPolicy ingress and AuthorizationPolicy output remains valid. +2. **Given** an existing hosts allow rule, **When** the chart is rendered, **Then** DNS ServiceEntry output remains valid and uses `resolution: DNS`. + +--- + +### User Story 3 - Discover and validate the IP egress values shape (Priority: P3) + +As a chart consumer, I can find a documented example for IP egress and run a Helm render command to validate the generated manifests before deployment. + +**Why this priority**: Direct IP egress changes the public values contract and must be documented with a runnable example. + +**Independent Test**: Follow the example under `examples/zero-trust-mesh/ip-egress.yaml` and render it successfully with Helm. + +**Acceptance Scenarios**: + +1. **Given** the documented IP egress example, **When** a user runs its top-line Helm command, **Then** the chart renders successfully. +2. **Given** a user reads the chart README, **When** they scan the values table, **Then** they can find `allowTo[].ips` and its default/example behavior. + +### Edge Cases + +- Single IPv4 address without a CIDR suffix is normalized to `/32`. +- Single IPv6 address without a CIDR suffix is normalized to `/128`. +- CIDR input is preserved as provided. +- If `ports` is omitted on an IP rule, the chart uses `443/TCP`. +- Istio protocol names such as `HTTP` or `HTTPS` are allowed on ServiceEntry ports, but Kubernetes NetworkPolicy renders only L4 protocols and maps unsupported protocol names to `TCP`. +- Host-only `allowTo` entries continue to render DNS ServiceEntry resources and do not render IP egress NetworkPolicies. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The chart MUST support optional `allowTo[].ips` as a list of external destination IP addresses or CIDR blocks. +- **FR-002**: For every IP rule, the chart MUST render an Istio `ServiceEntry` in the workload namespace with `spec.addresses` set to the configured IP/CIDR list. +- **FR-003**: IP-rule ServiceEntries MUST use `location: MESH_EXTERNAL` and `resolution: NONE`. +- **FR-004**: For every IP rule, the chart MUST render a Kubernetes `NetworkPolicy` in the workload namespace that selects the source workload and allows egress to each configured IP/CIDR through `ipBlock`. +- **FR-005**: Single IPv4 addresses MUST be rendered as `/32` CIDRs for Kubernetes NetworkPolicy compatibility. +- **FR-006**: Single IPv6 addresses MUST be rendered as `/128` CIDRs for Kubernetes NetworkPolicy compatibility. +- **FR-007**: IP rule `ports` MUST default to `443/TCP` when omitted. +- **FR-008**: IP egress support MUST NOT change existing service allow rule behavior. +- **FR-009**: IP egress support MUST NOT change existing host ServiceEntry behavior. +- **FR-010**: The chart MUST document `allowTo[].ips` in `charts/zero-trust-mesh/values.yaml` and `charts/zero-trust-mesh/README.md`. +- **FR-011**: The repository MUST include a runnable example under `examples/zero-trust-mesh/` that demonstrates IP egress. +- **FR-012**: The change MUST include a render check that fails against the previous chart and passes after implementation. +- **FR-013**: The affected chart version MUST be bumped according to repository constitution requirements. + +### Key Entities *(include if feature involves data)* + +- **IP egress rule**: An `allowTo` entry containing `ips` and optional `ports`. +- **Normalized IP block**: The CIDR form rendered into both Istio `ServiceEntry.spec.addresses` and Kubernetes `NetworkPolicy.ipBlock.cidr`. +- **IP ServiceEntry**: The Istio registry object that permits direct IP traffic in `REGISTRY_ONLY` meshes. +- **IP egress NetworkPolicy**: The Kubernetes policy that permits selected source workload pods to connect to configured destination IP blocks. + +### Assumptions + +- Direct IP egress targets are external to the Kubernetes service-to-service rules managed by `allowTo[].service`. +- Consumers are responsible for providing valid IP addresses or CIDR blocks. +- IP egress does not use the existing DNS/SNI egress gateway routing path. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Rendering the IP egress test fixture produces a ServiceEntry with every configured IP/CIDR in `spec.addresses`. +- **SC-002**: Rendering the IP egress test fixture produces a NetworkPolicy with every configured IP/CIDR in `egress[].to[].ipBlock.cidr`. +- **SC-003**: Rendering default chart values completes successfully and keeps existing service and host outputs valid. +- **SC-004**: `helm lint ./charts/zero-trust-mesh` completes with 0 failed charts. +- **SC-005**: A reviewer can locate the new `allowTo[].ips` values shape in README, `values.yaml`, and a runnable example in under 5 minutes. diff --git a/specs/013-zero-trust-ip-egress/tasks.md b/specs/013-zero-trust-ip-egress/tasks.md new file mode 100644 index 0000000..9b87ffe --- /dev/null +++ b/specs/013-zero-trust-ip-egress/tasks.md @@ -0,0 +1,109 @@ +# Tasks: Zero Trust IP Egress + +**Input**: Design documents from `/specs/013-zero-trust-ip-egress/` +**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/render-contract.md`, `quickstart.md` + +**Tests**: This feature requires Helm lint/template checks plus a focused render assertion. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing. + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Confirm API contracts and capture the missing IP egress behavior. + +- [x] T001 Review Istio ServiceEntry and Kubernetes NetworkPolicy IP block field shapes; document findings in `specs/013-zero-trust-ip-egress/research.md` +- [x] T002 Add `charts/zero-trust-mesh/tests/ip-egress-values.yaml` with an `allowTo` IP rule containing one single IP and one CIDR +- [x] T003 Add `charts/zero-trust-mesh/tests/render-ip-egress.sh` to assert ServiceEntry, `resolution: NONE`, normalized IPs, NetworkPolicy, `ipBlock`, and configured ports render +- [x] T004 Run `./charts/zero-trust-mesh/tests/render-ip-egress.sh ./charts/zero-trust-mesh` before implementation and confirm it fails because IP egress resources are absent + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Add shared helpers required by the IP ServiceEntry and NetworkPolicy templates. + +- [x] T005 Add `ztm.ipCidr` helper in `charts/zero-trust-mesh/templates/_helpers.tpl` +- [x] T006 Add `ztm.networkPolicyProtocol` helper in `charts/zero-trust-mesh/templates/_helpers.tpl` +- [x] T007 Ensure `ztm.ipCidr` preserves CIDRs, converts IPv4 host addresses to `/32`, and converts IPv6 host addresses to `/128` + +**Checkpoint**: Shared IP normalization and NetworkPolicy protocol helpers are available. + +--- + +## Phase 3: User Story 1 - Allow direct destination IP egress (Priority: P1) 🎯 MVP + +**Goal**: Render IP rules into both Istio and Kubernetes policy resources. + +**Independent Test**: `./charts/zero-trust-mesh/tests/render-ip-egress.sh ./charts/zero-trust-mesh` + +- [x] T008 [US1] Update `charts/zero-trust-mesh/templates/istio-serviceentries.yaml` to render `allowTo[].ips` as a ServiceEntry with `addresses` and `resolution: NONE` +- [x] T009 [US1] Add `charts/zero-trust-mesh/templates/networkpolicy-ip-egress.yaml` to render source-workload egress NetworkPolicy rules with `ipBlock` +- [x] T010 [US1] Re-run the focused render assertion and confirm it exits `0` + +**Checkpoint**: Direct IP egress is rendered and independently verifiable. + +--- + +## Phase 4: User Story 2 - Preserve existing service and host behavior (Priority: P2) + +**Goal**: Keep old service and host rule outputs valid. + +**Independent Test**: Default rendering and existing target-pod-label render assertion complete successfully. + +- [x] T011 [US2] Render default chart values and confirm service and host outputs remain valid +- [x] T012 [US2] Run `./charts/zero-trust-mesh/tests/render-target-pod-labels.sh ./charts/zero-trust-mesh` +- [x] T013 [US2] Confirm host-only `allowTo` entries still render DNS ServiceEntry resources with `resolution: DNS` + +**Checkpoint**: Existing service and host consumers are not regressed. + +--- + +## Phase 5: User Story 3 - Discover and validate the IP egress values shape (Priority: P3) + +**Goal**: Document and demonstrate `allowTo[].ips`. + +**Independent Test**: A user can render the repo-level example command successfully. + +- [x] T014 [US3] Document `allowTo[].ips` in `charts/zero-trust-mesh/values.yaml` +- [x] T015 [US3] Document `allowTo[].ips` in `charts/zero-trust-mesh/README.md` +- [x] T016 [US3] Add `examples/zero-trust-mesh/ip-egress.yaml` with a top-line runnable Helm command +- [x] T017 [US3] Render the new example with `helm template ztm-ip-egress ./charts/zero-trust-mesh -n default -f ./examples/zero-trust-mesh/ip-egress.yaml` + +**Checkpoint**: Documentation and example values show the new IP egress option. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final compliance, versioning, and release readiness. + +- [x] T018 Add Speckit artifacts under `specs/013-zero-trust-ip-egress/` +- [x] T019 Bump `charts/zero-trust-mesh/Chart.yaml` patch version +- [x] T020 Run `helm lint ./charts/zero-trust-mesh` +- [x] T021 Run `helm template ztm-default ./charts/zero-trust-mesh -n default` +- [x] T022 Run focused render assertion `./charts/zero-trust-mesh/tests/render-ip-egress.sh ./charts/zero-trust-mesh` +- [x] T023 Run existing example regressions from `specs/013-zero-trust-ip-egress/quickstart.md` +- [x] T024 Run `git diff --check` + +## Dependencies & Execution Order + +- Phase 1 precedes implementation because the render assertion must fail first. +- Phase 2 precedes US1 because both IP rendering paths use shared helpers. +- US2 depends on final template output to validate regression behavior. +- US3 depends on finalized values shape and render output. +- Phase 6 depends on all stories. + +## Parallel Opportunities + +- Documentation updates (`T014`, `T015`) can run after values shape is final. +- Example rendering and default rendering can run in parallel during verification. +- Speckit documentation can be reviewed independently of template code after behavior is finalized. + +## Implementation Strategy + +1. Prove current behavior fails the new IP egress assertion. +2. Add IP normalization and protocol helpers. +3. Render IP ServiceEntry and IP egress NetworkPolicy. +4. Confirm focused IP egress rendering. +5. Confirm service and host behavior still renders. +6. Add docs/example/version bump and run Helm validation.