From 31679f82dbdd5003261d6f00b82a39bd24510576 Mon Sep 17 00:00:00 2001 From: Manuel Lorenzo Date: Tue, 16 Jun 2026 13:03:26 +0200 Subject: [PATCH] mbp-1126: Network segmentation using UDN Signed-off-by: Manuel Lorenzo --- charts/qtodo/templates/app-deployment.yaml | 3 + .../templates/postgresql-statefulset.yaml | 4 + .../templates/udn-admin-network-policy.yaml | 118 +++++++++ .../udn-network-attachment-definition.yaml | 23 ++ .../templates/udn-user-defined-network.yaml | 32 +++ charts/qtodo/values.yaml | 53 ++++ docs/SYNC-WAVE-INVENTORY.md | 7 +- docs/user-defined-networks.md | 236 ++++++++++++++++++ scripts/features/features.yaml | 4 + scripts/features/udn.yaml | 9 + 10 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 charts/qtodo/templates/udn-admin-network-policy.yaml create mode 100644 charts/qtodo/templates/udn-network-attachment-definition.yaml create mode 100644 charts/qtodo/templates/udn-user-defined-network.yaml create mode 100644 docs/user-defined-networks.md create mode 100644 scripts/features/udn.yaml diff --git a/charts/qtodo/templates/app-deployment.yaml b/charts/qtodo/templates/app-deployment.yaml index 2c78c92f..b6eb2f31 100644 --- a/charts/qtodo/templates/app-deployment.yaml +++ b/charts/qtodo/templates/app-deployment.yaml @@ -21,6 +21,9 @@ spec: {{- if .Values.app.spire.enabled }} checksum/app-spiffe-helper-config: {{ include (print $.Template.BasePath "/spiffe-helper-config.yaml") . | sha256sum }} checksum/app-spiffe-vault-client-config: {{ include (print $.Template.BasePath "/spiffe-vault-client-config.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.app.udn.enabled }} + k8s.v1.cni.cncf.io/networks: {{ .Release.Namespace }}/{{ .Values.app.udn.nadName }} {{- end }} labels: app: qtodo diff --git a/charts/qtodo/templates/postgresql-statefulset.yaml b/charts/qtodo/templates/postgresql-statefulset.yaml index f93726f3..df1b49e7 100644 --- a/charts/qtodo/templates/postgresql-statefulset.yaml +++ b/charts/qtodo/templates/postgresql-statefulset.yaml @@ -20,6 +20,10 @@ spec: serviceName: qtodo-db template: metadata: +{{- if .Values.app.udn.enabled }} + annotations: + k8s.v1.cni.cncf.io/networks: {{ .Release.Namespace }}/{{ .Values.app.udn.nadName }} +{{- end }} labels: app: qtodo-db spec: diff --git a/charts/qtodo/templates/udn-admin-network-policy.yaml b/charts/qtodo/templates/udn-admin-network-policy.yaml new file mode 100644 index 00000000..71229b27 --- /dev/null +++ b/charts/qtodo/templates/udn-admin-network-policy.yaml @@ -0,0 +1,118 @@ +{{- if and .Values.app.udn.enabled .Values.app.udn.networkPolicy.enabled }} +# AdminNetworkPolicy for qtodo UDN +# Controls traffic on the secondary (UDN) interface with explicit allow-lists +# Primary network policies (cluster network) are in qtodo-network-policy.yaml +apiVersion: policy.networking.k8s.io/v1alpha1 +kind: AdminNetworkPolicy +metadata: + annotations: + argocd.argoproj.io/sync-wave: '37' + name: qtodo-udn-policy +spec: + priority: 50 + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + ingress: + {{- if .Values.app.udn.networkPolicy.ingress.router.enabled }} + # Allow ingress from OpenShift router on primary network (not UDN) + # Router traffic comes through the cluster network interface + - name: allow-router-ingress + action: Allow + from: + - namespaces: + matchLabels: + policy-group.network.openshift.io/ingress: "" + ports: + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.ingress.router.port }} + {{- end }} + {{- if .Values.app.udn.networkPolicy.egress.postgresql.enabled }} + # Allow ingress to PostgreSQL from qtodo pods within same namespace on UDN + - name: allow-postgresql-ingress + action: Allow + from: + - pods: + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: + app: qtodo + ports: + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.egress.postgresql.port }} + {{- end }} + # Deny all other ingress by default on UDN + - name: deny-all-ingress + action: Deny + from: + - namespaces: {} + egress: + {{- if .Values.app.udn.networkPolicy.egress.dns.enabled }} + # Allow DNS resolution via CoreDNS + - name: allow-dns + action: Allow + to: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.app.udn.networkPolicy.egress.dns.namespace }} + ports: + - portNumber: + protocol: UDP + port: {{ .Values.app.udn.networkPolicy.egress.dns.port }} + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.egress.dns.port }} + {{- end }} + {{- if .Values.app.udn.networkPolicy.egress.postgresql.enabled }} + # Allow PostgreSQL access within same namespace on UDN + - name: allow-postgresql + action: Allow + to: + - pods: + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: + app: qtodo-db + ports: + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.egress.postgresql.port }} + {{- end }} + {{- if .Values.app.udn.networkPolicy.egress.vault.enabled }} + # Allow Vault API access (SPIFFE JWT auth) via cluster network + - name: allow-vault + action: Allow + to: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.app.udn.networkPolicy.egress.vault.namespace }} + ports: + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.egress.vault.port }} + {{- end }} + {{- if .Values.app.udn.networkPolicy.egress.https.enabled }} + # Allow OIDCs (HTTPS connections) via cluster network (external route) + - name: allow-https + action: Allow + to: + - networks: + - 0.0.0.0/0 # External route, cannot match by namespace + ports: + - portNumber: + protocol: TCP + port: {{ .Values.app.udn.networkPolicy.egress.https.port }} + {{- end }} + # Deny all other egress by default on UDN + - name: deny-all-egress + action: Deny + to: + - namespaces: {} +{{- end }} diff --git a/charts/qtodo/templates/udn-network-attachment-definition.yaml b/charts/qtodo/templates/udn-network-attachment-definition.yaml new file mode 100644 index 00000000..321a3621 --- /dev/null +++ b/charts/qtodo/templates/udn-network-attachment-definition.yaml @@ -0,0 +1,23 @@ +{{- if .Values.app.udn.enabled }} +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + annotations: + argocd.argoproj.io/sync-wave: '36' + description: "Network attachment for qtodo UDN isolation" + name: {{ .Values.app.udn.nadName }} + namespace: {{ .Release.Namespace }} +spec: + config: | + { + "cniVersion": "0.4.0", + "type": "ovn-k8s-cni-overlay", + "name": "{{ .Values.app.udn.name }}", + "topology": "{{ lower .Values.app.udn.topology }}", + "netAttachDefName": "{{ .Release.Namespace }}/{{ .Values.app.udn.nadName }}", + {{- if eq .Values.app.udn.topology "Layer2" }} + "subnets": "{{ .Values.app.udn.subnet }}", + {{- end }} + "mtu": {{ .Values.app.udn.mtu }} + } +{{- end }} diff --git a/charts/qtodo/templates/udn-user-defined-network.yaml b/charts/qtodo/templates/udn-user-defined-network.yaml new file mode 100644 index 00000000..40ec33d8 --- /dev/null +++ b/charts/qtodo/templates/udn-user-defined-network.yaml @@ -0,0 +1,32 @@ +{{- if .Values.app.udn.enabled }} +apiVersion: k8s.ovn.org/v1 +kind: UserDefinedNetwork +metadata: + annotations: + argocd.argoproj.io/sync-wave: '35' + name: {{ .Values.app.udn.name }} + namespace: {{ .Release.Namespace }} +spec: + topology: {{ .Values.app.udn.topology }} + {{- if eq .Values.app.udn.topology "Layer2" }} + layer2: + role: Primary + subnets: + - {{ .Values.app.udn.subnet }} + {{- if .Values.app.udn.mtu }} + mtu: {{ .Values.app.udn.mtu }} + {{- end }} + {{- else if eq .Values.app.udn.topology "Layer3" }} + layer3: + role: Primary + subnets: + - {{ .Values.app.udn.subnet }} + {{- if .Values.app.udn.joinSubnet }} + joinSubnets: + - {{ .Values.app.udn.joinSubnet }} + {{- end }} + {{- if .Values.app.udn.mtu }} + mtu: {{ .Values.app.udn.mtu }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index d8ba6ac5..da070365 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -91,6 +91,59 @@ app: # QTodo truststore password path (app-level isolation) vaultPath: "secret/data/apps/qtodo/qtodo-truststore" + # User-Defined Network (UDN) configuration for network isolation + # Provides layer 2/3 network segmentation following Zero Trust principles + udn: + enabled: false + # Network name + name: qtodo-isolated-network + # NetworkAttachmentDefinition name + nadName: qtodo-udn-nad + # Topology: Layer2 or Layer3 + # Layer2: Same subnet, pods can communicate directly + # Layer3: Different subnets per node, requires routing + topology: Layer2 + # CIDR for the UDN subnet (Layer2 only) + subnet: "10.100.0.0/16" + # Join subnet (for Layer3, optional) + # joinSubnet: "100.64.0.0/16" + # MTU for the network (default: 1400 to avoid fragmentation) + mtu: 1400 + # IPAM configuration + ipam: + type: static + # Network Policy for UDN + # UDN requires policies on both primary (cluster network) and secondary (UDN) interfaces + networkPolicy: + # Enable network policies on the UDN + enabled: true + # Allowed egress destinations from qtodo pods on the UDN + egress: + # DNS resolution + dns: + enabled: true + port: 5353 + namespace: openshift-dns + # PostgreSQL database + postgresql: + enabled: true + port: 5432 + # Vault for secrets (via primary network, not UDN) + vault: + enabled: true + port: 8200 + namespace: vault + # Allow HTTPS connections to any destination, used for OIDCs (via primary network for external route) + https: + enabled: true + port: 443 + # Allowed ingress sources to qtodo pods on the UDN + ingress: + # OpenShift router (via primary network, not UDN) + router: + enabled: true + port: 8080 + # PostgreSQL database configuration postgresql: name: qtodo-db diff --git a/docs/SYNC-WAVE-INVENTORY.md b/docs/SYNC-WAVE-INVENTORY.md index e2a47083..94617cc0 100644 --- a/docs/SYNC-WAVE-INVENTORY.md +++ b/docs/SYNC-WAVE-INVENTORY.md @@ -41,16 +41,18 @@ Every sync-wave in the repository, in order. **App** = hub-level Argo CD Applica | 34 | └ rhtpa-operator | chart | oidc-cli-secret | | 34 | └ noobaa-mcg | chart | bucket-class | | 35 | rh-keycloak | **App** | | +| 35 | └ qtodo | chart | udn-user-defined-network (UserDefinedNetwork CR for network isolation) | | 36 | noobaa-mcg | **App** | | | 36 | └ rhtpa-operator | chart | postgresql-serviceaccount, postgresql-external-secret, object-bucket-claim | | 36 | └ keycloak | chart | keycloak.yaml (Keycloak CR) | | 36 | └ quay-registry | chart | object-bucket-claim | | 36 | └ acs-central | chart | admin-password-secret, central-htpasswd-external-secret, keycloak-client-secret-external-secret | -| 36 | └ qtodo | chart | truststore-secret-external-secret, registry-external-secret | +| 36 | └ qtodo | chart | udn-network-attachment-definition (NAD), truststore-secret-external-secret, registry-external-secret | | 38+0 | └ qtodo | chart | registry-seed SA, ClusterRole, ClusterRoleBinding | | 38+5 | └ qtodo | chart (hook) | registry-seed-image (Sync hook Job -- mirrors upstream image to configured registry) | | 37 | └ quay-registry | chart | quay-s3-setup-serviceaccount (5 resources) | | 37 | └ acs-central | chart | create-htpasswd-field (Job) | +| 37 | └ qtodo | chart | udn-admin-network-policy (AdminNetworkPolicy for UDN traffic control) | | 38 | qtodo | **App** | | | 38 | └ quay-registry | chart | quay-config-bundle-secret | | 39 | └ rhtpa-operator | chart | s3-credentials-secret | @@ -243,8 +245,11 @@ Charts marked **(external)** have been externalized to standalone repositories m | --- | ---: | ---: | | registry-seed-job.yaml (SA, ClusterRole, ClusterRoleBinding) | --- | 0 | | registry-seed-job.yaml (Sync hook Job) | --- | 5 | +| udn-user-defined-network.yaml | --- | 35 | +| udn-network-attachment-definition.yaml | --- | 36 | | truststore-secret-external-secret.yaml | 5 | 36 | | registry-external-secret.yaml | --- | 36 | +| udn-admin-network-policy.yaml (AdminNetworkPolicy) | --- | 37 | | postgresql-statefulset.yaml | 10 | 41 | | postgresql-service.yaml | 10 | 41 | | qtodo-truststore-config.yaml | 10 | 41 | diff --git a/docs/user-defined-networks.md b/docs/user-defined-networks.md new file mode 100644 index 00000000..f6681791 --- /dev/null +++ b/docs/user-defined-networks.md @@ -0,0 +1,236 @@ +# User-Defined Networks (UDN) for Zero Trust Network Isolation + +## Overview + +User-Defined Networks (UDN) provide layer 2/3 network isolation for workloads in OpenShift, separate from the default cluster network. This feature implements Zero Trust network segmentation principles by creating an isolated network for the qtodo application, restricting communication to only necessary services. + +## Architecture + +### Network Topology + +The UDN implementation creates a dedicated isolated network for qtodo workloads: + +```text +┌─────────────────────────────────────────────────────┐ +│ Cluster Network │ +│ ┌────────────┐ ┌─────────┐ ┌──────────┐ │ +│ │ Router │───▶│ qtodo │───▶│ Vault │ │ +│ │ (Ingress) │ │ (eth0) │ │ (8200) │ │ +│ └────────────┘ └────┬────┘ └──────────┘ │ +│ │ │ +│ │ UDN Attachment │ +└─────────────────────────┼───────────────────────────┘ + │ + ┌─────▼──────┐ + │ UDN │ + │ (net1/ │ + │ Layer2) │ + └─────┬──────┘ + │ + ┌─────────┴────────┐ + │ │ + ┌─────▼──────┐ ┌────▼─────┐ + │ qtodo pod │ │ qtodo-db │ + │ (isolated) │──────│ (5432) │ + └────────────┘ └──────────┘ + │ + └─────────▶ DNS (5353) via cluster network +``` + +### Dual Network Interfaces + +When UDN is enabled, qtodo pods have two network interfaces: + +1. **eth0 (Primary - Cluster Network)** + - Ingress from OpenShift Router (port 8080) + - Egress to Vault (SPIFFE auth, port 8200) + - Egress to OIDCs (OIDC back-channel, port 443) + - DNS resolution (CoreDNS, port 5353) + +2. **net1 (Secondary - UDN)** + - PostgreSQL communication (qtodo ↔ qtodo-db, port 5432) + - Isolated from other cluster workloads + - Layer 2 topology (same subnet across nodes) + +## Security Benefits + +1. **Network Segmentation**: qtodo workloads are isolated from arbitrary cluster traffic +2. **Explicit Allow-Lists**: AdminNetworkPolicy enforces allow-only-required communication +3. **Defense in Depth**: Combines with existing NetworkPolicy for dual-layer protection +4. **Blast Radius Reduction**: Compromise of qtodo cannot pivot to unrelated services +5. **Compliance**: Supports Zero Trust architecture mandates (NIST 800-207, NIS2, ISO 27001:2022) + +## Components + +UDN is integrated into the qtodo Helm chart (`charts/qtodo`). When enabled, the following resources are created: + +### UserDefinedNetwork CR + +Template: `charts/qtodo/templates/udn-user-defined-network.yaml` + +Creates the isolated network with Layer2 topology: + +- Subnet: `10.100.0.0/16` +- MTU: 1400 (avoids fragmentation) +- IPAM: Persistent IP assignment +- Sync-wave: 35 (before NAD) + +### NetworkAttachmentDefinition + +Template: `charts/qtodo/templates/udn-network-attachment-definition.yaml` + +Defines how pods attach to the UDN: + +- CNI type: `ovn-k8s-cni-overlay` +- References the UserDefinedNetwork +- Used via pod annotation `k8s.v1.cni.cncf.io/networks` +- Sync-wave: 36 (before policies) + +### AdminNetworkPolicy + +Template: `charts/qtodo/templates/udn-admin-network-policy.yaml` + +Explicit allow-list for UDN traffic: + +- **Ingress**: + - OpenShift router (port 8080) + - qtodo pods to qtodo-db (port 5432) +- **Egress**: DNS, PostgreSQL, Vault, Keycloak (HTTPS connections) +- Priority: 50 (higher = processed first) +- Sync-wave: 37 (before qtodo app) + +## Enabling UDN + +### Option 1: Feature Variant Generator (Recommended) + +```bash +python3 scripts/gen-feature-variants.py \ + --features udn \ + --base values-hub.yaml + +# Apply the variant +cp /tmp/values-hub-udn.yaml values-hub.yaml +./pattern.sh make install +``` + +### Option 2: Manual Configuration + +1. **Enable UDN in the qtodo application** in `values-hub.yaml`: + + ```yaml + clusterGroup: + applications: + qtodo: + # ... existing config ... + overrides: + # ... existing overrides ... + - name: app.udn.enabled + value: "true" + ``` + +2. **Deploy**: + + ```bash + ./pattern.sh make install + ``` + +## Verification + +### 1. Check UDN Resources + +```bash +# UserDefinedNetwork +oc get userdefinednetwork -n qtodo +NAME AGE +qtodo-isolated-network 5m + +# NetworkAttachmentDefinition +oc get network-attachment-definitions -n qtodo +NAME AGE +qtodo-udn-nad 5m +``` + +### 2. Verify Network Policies + +```bash +# AdminNetworkPolicy +oc get adminnetworkpolicy +NAME PRIORITY AGE +qtodo-udn-policy 50 5m +``` + +### 3. Test Connectivity + +```bash +# DNS resolution (should work via eth0) +oc exec -n qtodo deploy/qtodo -c qtodo -- getent hosts qtodo-db + +# PostgreSQL connectivity (should work via net1) +oc exec -n qtodo deploy/qtodo -c qtodo -- timeout 5 bash -c '/dev/null' && echo "OK" + +# Vault API (should work via eth0) +oc exec -n qtodo deploy/qtodo -c qtodo -- curl -sk https://vault.vault.svc:8200/v1/sys/health +``` + +### 4. Verify qtodo Application + +```bash +# Get the route +QTODO_URL=$(oc get route -n qtodo qtodo -o jsonpath='{.spec.host}') + +# Access the application +curl https://$QTODO_URL +``` + +## Configuration Options + +UDN is configured via the `app.udn` section in `charts/qtodo/values.yaml`: + +| Parameter | Description | Default | +| -------------------------------- | -------------------------------- | ------------------------ | +| `app.udn.enabled` | Enable UDN | `false` | +| `app.udn.name` | UserDefinedNetwork name | `qtodo-isolated-network` | +| `app.udn.nadName` | NetworkAttachmentDefinition name | `qtodo-udn-nad` | +| `app.udn.topology` | Network topology (Layer2/Layer3) | `Layer2` | +| `app.udn.subnet` | CIDR for UDN | `10.100.0.0/16` | +| `app.udn.mtu` | MTU for the network | `1400` | +| `app.udn.networkPolicy.enabled` | Enable AdminNetworkPolicy | `true` | + +### Layer3 Topology + +For larger deployments, Layer3 provides better scalability. Override in `values-hub.yaml`: + +```yaml +clusterGroup: + applications: + qtodo: + overrides: + - name: app.udn.enabled + value: "true" + - name: app.udn.topology + value: "Layer3" + - name: app.udn.joinSubnet + value: "100.64.0.0/16" +``` + +## Security Considerations + +### Defense in Depth + +UDN complements, but does not replace, other security controls: + +- **NetworkPolicy**: Still applied on the cluster network (eth0) +- **Service Mesh**: mTLS can layer on top of UDN +- **ACS Policies**: Runtime enforcement still active + +### Attack Surface + +- UDN pods are still reachable via cluster network (eth0) for ingress/egress to external services +- AdminNetworkPolicy must be correctly configured to avoid bypasses +- Pods with `CAP_NET_ADMIN` could potentially manipulate interfaces +- For integration with IDPs (_Keycloak_, _EntraID_), HTTPS connections to any destination are enabled. In a more secure environment, this rule should be more restrictive and only allow access to specific destinations. + +## References + +- [OpenShift UDN Documentation](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/multiple_networks/understanding-multiple-networks) +- [OVN-Kubernetes User-Defined Networks](https://github.com/ovn-kubernetes/ovn-kubernetes/blob/master/docs/features/user-defined-networks/user-defined-networks.md) diff --git a/scripts/features/features.yaml b/scripts/features/features.yaml index 434d53a7..d26726ea 100644 --- a/scripts/features/features.yaml +++ b/scripts/features/features.yaml @@ -42,6 +42,10 @@ features: description: "Azure Entra ID integration" depends_on: [supply-chain] + udn: + description: "User-Defined Network isolation for qtodo" + depends_on: [] + # Registry options (only used with supply-chain feature) # Each maps to a file under registry/ subdirectory. registry_options: diff --git a/scripts/features/udn.yaml b/scripts/features/udn.yaml new file mode 100644 index 00000000..71c8d383 --- /dev/null +++ b/scripts/features/udn.yaml @@ -0,0 +1,9 @@ +# User-Defined Network isolation for qtodo application +# Provides layer 2/3 network segmentation following Zero Trust principles +clusterGroup: + # Merge UDN configuration into existing qtodo application + merge_into_applications: + qtodo: + overrides: + - name: app.udn.enabled + value: "true"