Phase 8a: Platform namespace NetworkPolicies in Terraform #77

Merged
forgejo_admin merged 2 commits from 76-phase-8a-platform-namespace-networkpolic into main 2026-03-15 04:05:35 +00:00

Summary

  • Add default-deny ingress NetworkPolicies for all 9 platform namespaces managed by kubernetes_namespace_v1 resources
  • ArgoCD skipped because its namespace is created by Helm, not a standalone namespace resource
  • Each policy uses podSelector = {} (all pods) with policyTypes = ["Ingress"] only -- no egress restrictions

Changes

  • terraform/network-policies.tf (new): 9 kubernetes_manifest resources for NetworkPolicies
    • monitoring -- allow from tailscale + same namespace (internal mesh)
    • forgejo -- allow from tailscale + woodpecker (CI webhooks)
    • woodpecker -- allow from tailscale + same namespace (agent-server)
    • harbor -- allow from tailscale + same namespace + monitoring (metrics scrape)
    • minio -- allow from tailscale + postgres (CNPG backup) + woodpecker (CNPG backup)
    • keycloak -- allow from tailscale only
    • postgres -- allow from pal-e-docs + cnpg-system (operator)
    • ollama -- allow from pal-e-docs only
    • cnpg-system -- allow from kube-system (webhook)

tofu plan Output

Plan: 9 to add, 2 to change, 0 to destroy.

The 9 additions are the 9 NetworkPolicy resources. The 2 changes are pre-existing drift (dora_exporter secret write-only attribute, unrelated to this PR).

tofu fmt -- no changes needed (already formatted).
tofu validate -- Success.

Test Plan

  • tofu fmt network-policies.tf passes clean
  • tofu validate returns Success
  • tofu plan shows 9 to add, 0 to destroy
  • After apply: kubectl get netpol -A shows default-deny-ingress in all 9 namespaces
  • Smoke test: Grafana dashboards load, Forgejo UI accessible, Woodpecker pipelines run

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • Closes #76
  • plan-pal-e-platform
## Summary - Add default-deny ingress NetworkPolicies for all 9 platform namespaces managed by `kubernetes_namespace_v1` resources - ArgoCD skipped because its namespace is created by Helm, not a standalone namespace resource - Each policy uses `podSelector = {}` (all pods) with `policyTypes = ["Ingress"]` only -- no egress restrictions ## Changes - `terraform/network-policies.tf` (new): 9 `kubernetes_manifest` resources for NetworkPolicies - **monitoring** -- allow from tailscale + same namespace (internal mesh) - **forgejo** -- allow from tailscale + woodpecker (CI webhooks) - **woodpecker** -- allow from tailscale + same namespace (agent-server) - **harbor** -- allow from tailscale + same namespace + monitoring (metrics scrape) - **minio** -- allow from tailscale + postgres (CNPG backup) + woodpecker (CNPG backup) - **keycloak** -- allow from tailscale only - **postgres** -- allow from pal-e-docs + cnpg-system (operator) - **ollama** -- allow from pal-e-docs only - **cnpg-system** -- allow from kube-system (webhook) ## tofu plan Output ``` Plan: 9 to add, 2 to change, 0 to destroy. ``` The 9 additions are the 9 NetworkPolicy resources. The 2 changes are pre-existing drift (dora_exporter secret write-only attribute, unrelated to this PR). `tofu fmt` -- no changes needed (already formatted). `tofu validate` -- Success. ## Test Plan - [x] `tofu fmt network-policies.tf` passes clean - [x] `tofu validate` returns Success - [x] `tofu plan` shows 9 to add, 0 to destroy - [ ] After apply: `kubectl get netpol -A` shows `default-deny-ingress` in all 9 namespaces - [ ] Smoke test: Grafana dashboards load, Forgejo UI accessible, Woodpecker pipelines run ## Review Checklist - [x] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related - Closes #76 - `plan-pal-e-platform`
feat: add NetworkPolicies for all platform namespaces
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
3af5bb9e20
Default-deny ingress policies for 9 platform namespaces managed by
kubernetes_namespace_v1 resources. ArgoCD skipped (namespace created
by Helm, not managed here). Each policy allows ingress only from
explicitly listed namespaces. Egress is unrestricted.

Closes #76

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Tofu Plan Output

tailscale_acl.this: Refreshing state... [id=acl]
helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin]
kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak]
kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system]
data.kubernetes_namespace_v1.tofu_state: Reading...
kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring]
kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor]
kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres]
kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale]
kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama]
data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state]
kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo]
data.kubernetes_namespace_v1.pal_e_docs: Reading...
kubernetes_namespace_v1.minio: Refreshing state... [id=minio]
kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs]
kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data]
kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin]
helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator]
kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
helm_release.loki_stack: Refreshing state... [id=loki-stack]
kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials]
helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack]
kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard]
helm_release.cnpg: Refreshing state... [id=cnpg]
kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url]
helm_release.forgejo: Refreshing state... [id=forgejo]
kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
helm_release.ollama: Refreshing state... [id=ollama]
kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel]
kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource]
kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel]
helm_release.harbor: Refreshing state... [id=harbor]
kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel]
kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard]
kubernetes_manifest.blackbox_alerts: Refreshing state...
helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter]
kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
helm_release.minio: Refreshing state... [id=minio]
kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard]
kubernetes_manifest.dora_exporter_service_monitor: Refreshing state...
kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel]
kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel]
minio_iam_user.cnpg: Refreshing state... [id=cnpg]
minio_s3_bucket.assets: Refreshing state... [id=assets]
minio_iam_user.tf_backup: Refreshing state... [id=tf-backup]
minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup]
minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal]
kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel]
minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups]
minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal]
kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel]
minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001]
minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001]
kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds]
kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds]
kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds]
kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify]
kubernetes_manifest.woodpecker_postgres: Refreshing state...
helm_release.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel]

OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

OpenTofu will perform the following actions:

  # kubernetes_manifest.netpol_cnpg_system will be created
  + resource "kubernetes_manifest" "netpol_cnpg_system" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "cnpg-system"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "kube-system"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "cnpg-system"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "kube-system"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_forgejo will be created
  + resource "kubernetes_manifest" "netpol_forgejo" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "forgejo"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "forgejo"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_harbor will be created
  + resource "kubernetes_manifest" "netpol_harbor" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "harbor"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "harbor"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "harbor"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "harbor"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_keycloak will be created
  + resource "kubernetes_manifest" "netpol_keycloak" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "keycloak"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "keycloak"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_minio will be created
  + resource "kubernetes_manifest" "netpol_minio" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "minio"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "postgres"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "minio"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "postgres"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_monitoring will be created
  + resource "kubernetes_manifest" "netpol_monitoring" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "monitoring"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "monitoring"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_ollama will be created
  + resource "kubernetes_manifest" "netpol_ollama" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "ollama"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "ollama"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_postgres will be created
  + resource "kubernetes_manifest" "netpol_postgres" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "postgres"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "postgres"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_woodpecker will be created
  + resource "kubernetes_manifest" "netpol_woodpecker" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "woodpecker"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "woodpecker"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

Plan: 9 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't
guarantee to take exactly these actions if you run "tofu apply" now.
## Tofu Plan Output ``` tailscale_acl.this: Refreshing state... [id=acl] helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] data.kubernetes_namespace_v1.tofu_state: Reading... kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] data.kubernetes_namespace_v1.pal_e_docs: Reading... kubernetes_namespace_v1.minio: Refreshing state... [id=minio] kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs] kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.loki_stack: Refreshing state... [id=loki-stack] kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials] helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard] helm_release.cnpg: Refreshing state... [id=cnpg] kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url] helm_release.forgejo: Refreshing state... [id=forgejo] kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] helm_release.ollama: Refreshing state... [id=ollama] kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] helm_release.harbor: Refreshing state... [id=harbor] kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] kubernetes_manifest.blackbox_alerts: Refreshing state... helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter] kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.minio: Refreshing state... [id=minio] kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard] kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel] minio_iam_user.cnpg: Refreshing state... [id=cnpg] minio_s3_bucket.assets: Refreshing state... [id=assets] minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001] minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001] kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds] kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds] kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify] kubernetes_manifest.woodpecker_postgres: Refreshing state... helm_release.woodpecker: Refreshing state... [id=woodpecker] kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create OpenTofu will perform the following actions: # kubernetes_manifest.netpol_cnpg_system will be created + resource "kubernetes_manifest" "netpol_cnpg_system" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "cnpg-system" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "kube-system" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "cnpg-system" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "kube-system" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_forgejo will be created + resource "kubernetes_manifest" "netpol_forgejo" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "forgejo" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "forgejo" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_harbor will be created + resource "kubernetes_manifest" "netpol_harbor" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "harbor" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "harbor" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "harbor" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "harbor" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_keycloak will be created + resource "kubernetes_manifest" "netpol_keycloak" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "keycloak" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "keycloak" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_minio will be created + resource "kubernetes_manifest" "netpol_minio" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "minio" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "postgres" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "minio" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "postgres" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_monitoring will be created + resource "kubernetes_manifest" "netpol_monitoring" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "monitoring" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "monitoring" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_ollama will be created + resource "kubernetes_manifest" "netpol_ollama" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "ollama" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "ollama" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_postgres will be created + resource "kubernetes_manifest" "netpol_postgres" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "postgres" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "postgres" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_woodpecker will be created + resource "kubernetes_manifest" "netpol_woodpecker" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "woodpecker" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "woodpecker" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } Plan: 9 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run "tofu apply" now. ```
Author
Owner

PR #77 Review

DOMAIN REVIEW

Tech stack: Terraform (OpenTofu) / Kubernetes NetworkPolicy / k8s security hardening

This PR adds 9 kubernetes_manifest resources in a new terraform/network-policies.tf file, one default-deny-ingress NetworkPolicy per platform namespace. The approach is sound: podSelector = {} selects all pods, policyTypes = ["Ingress"] only restricts ingress (egress unrestricted), and each policy explicitly whitelists ingress sources via namespaceSelector using the well-known kubernetes.io/metadata.name label.

Terraform style: Clean. All resources use depends_on to reference the parent namespace. Namespace references use the kubernetes_namespace_v1.*.metadata[0].name pattern consistently (no hardcoded namespace strings). Resource naming follows netpol_{namespace} convention. Comment header explains the design rationale (ArgoCD skipped, egress unrestricted).

I verified every policy's ingress rules against the actual cross-namespace traffic flows declared in main.tf. The following issues were found by tracing service URLs, ServiceMonitor/PodMonitor declarations, and Blackbox Exporter probe targets.


BLOCKERS

B1. forgejo policy is missing monitoring ingress (will break Blackbox Exporter probes + DORA exporter)

The Blackbox Exporter (deployed in monitoring) probes Forgejo via internal URL http://forgejo-http.forgejo.svc.cluster.local:80 (main.tf line 415). The DORA exporter (also in monitoring) calls Forgejo at http://forgejo-http.forgejo.svc.cluster.local:80 (main.tf line 1141). Both are ingress TO forgejo FROM monitoring.

Current policy only allows: tailscale, woodpecker.
Must also allow: monitoring.

After apply, Blackbox will report Forgejo as DOWN and DORA metrics collection will fail.

B2. woodpecker policy is missing monitoring and cnpg-system ingress

  • Blackbox Exporter probes Woodpecker at http://woodpecker-server.woodpecker.svc.cluster.local:80 (main.tf line 420). DORA exporter hits http://woodpecker-server.woodpecker.svc.cluster.local:80 (main.tf line 1139). Both are ingress FROM monitoring.
  • CNPG operator (in cnpg-system) manages the woodpecker-db Cluster CR in the woodpecker namespace (main.tf line 1462). The operator needs to connect to the Postgres pods it manages. This is ingress TO woodpecker FROM cnpg-system.
  • Prometheus scrapes the woodpecker-db PodMonitor (enablePodMonitor = true, main.tf line 1527) -- ingress FROM monitoring.

Current policy only allows: tailscale, woodpecker (self).
Must also allow: monitoring, cnpg-system.

After apply, Blackbox will report Woodpecker as DOWN, DORA metrics will fail, CNPG operator will lose management of woodpecker-db (backup scheduling, failover, health checks), and Prometheus will fail to scrape woodpecker-db metrics.

B3. minio policy is missing monitoring and tofu-state ingress

  • Blackbox Exporter probes MinIO at http://minio.minio.svc.cluster.local:9000/minio/health/live (main.tf line 450). Ingress FROM monitoring.
  • Prometheus scrapes MinIO metrics via ServiceMonitor (serviceMonitor.enabled = true, main.tf line 1041). Ingress FROM monitoring.
  • The TF state backup CronJob runs in the tofu-state namespace and connects to http://minio.minio.svc.cluster.local:9000 (main.tf line 2028). Ingress FROM tofu-state.

Current policy only allows: tailscale, postgres, woodpecker.
Must also allow: monitoring, tofu-state.

After apply, Blackbox will report MinIO as DOWN, Prometheus MinIO metrics will stop, and daily TF state backups will fail silently.

B4. harbor policy is missing woodpecker ingress (will break CI image pulls)

Woodpecker CI pipeline pods (in woodpecker namespace) pull container images from Harbor during builds (kaniko pushes to Harbor, pipeline pods pull base images). This is ingress TO harbor FROM woodpecker.

Current policy only allows: tailscale, harbor (self), monitoring.
Must also allow: woodpecker.

After apply, CI pipelines that pull from Harbor will fail.

B5. cnpg-system policy is missing monitoring ingress (will break operator metrics scraping)

The CNPG operator has podMonitorEnabled = true (main.tf line 1358), meaning Prometheus scrapes the operator's metrics endpoint. This is ingress FROM monitoring TO cnpg-system.

Current policy only allows: kube-system.
Must also allow: monitoring.

After apply, CNPG operator metrics will stop being collected by Prometheus.


NITS

N1. Consider a locals block for repeated namespace selector patterns

The { namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "tailscale" } } } pattern appears in every policy. A locals block with common selectors (e.g., local.from_tailscale, local.from_monitoring) would reduce repetition and make policies easier to read:

locals {
  from_tailscale  = { namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "tailscale" } } }
  from_monitoring = { namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "monitoring" } } }
}

N2. postgres policy may want monitoring if Prometheus scrapes CNPG cluster metrics directly

CNPG clusters expose metrics via PodMonitor. If the CNPG Cluster in the postgres namespace has monitoring enabled (it does not appear to in this repo -- it is managed by pal-e-services), this would need monitoring ingress. Not a blocker for this PR since the postgres namespace CNPG cluster is defined elsewhere, but worth noting for the follow-up app-namespace phase.

N3. Comment at top could document the traffic map

A brief traffic map comment (like the one in the issue body) at the top of the file would help future maintainers understand WHY each namespace has specific ingress sources without needing to cross-reference main.tf.


SOP COMPLIANCE

  • Branch named after issue: 76-phase-8a-platform-namespace-networkpolic (matches issue #76)
  • PR body has: Summary, Changes, Test Plan, Related sections
  • Related references plan slug: plan-pal-e-platform
  • Closes #76 in Related section
  • No secrets committed
  • No unnecessary file changes (single new file, scoped correctly)
  • Commit messages are descriptive
  • tofu fmt and tofu validate passed per PR body
  • tofu plan output included (9 to add, 2 to change unrelated drift)

PROCESS OBSERVATIONS

Change failure risk: HIGH. This PR as-is will break monitoring (Blackbox probes, DORA metrics, Prometheus scraping) and CI (Harbor image pulls) across multiple namespaces the moment it is applied. The blast radius covers most of the platform's observability stack and CI pipeline. The fixes are straightforward -- adding missing monitoring, cnpg-system, tofu-state, and woodpecker namespace selectors to the affected policies -- but the current state would cause a multi-service outage.

MTTR consideration: NetworkPolicy issues are notoriously hard to debug because failures manifest as silent timeouts, not explicit errors. Blackbox probes returning DOWN, CI pipelines hanging on image pull, and CNPG operator losing contact with managed databases would all appear as unrelated incidents. Including the traffic map as a comment in the file (N3) would significantly reduce future MTTR.

Positive notes: The design is architecturally sound. Default-deny with explicit allow-lists is the correct approach. Using kubernetes.io/metadata.name labels is the right selector strategy. Skipping ArgoCD (Helm-managed namespace) is a reasonable scoping decision. The PR body is thorough and the test plan is sensible.


VERDICT: NOT APPROVED

Five blockers must be fixed: missing monitoring ingress on forgejo, woodpecker, minio, and cnpg-system; missing cnpg-system on woodpecker; missing tofu-state on minio; missing woodpecker on harbor. All are missing ingress rules that will break active cross-namespace traffic flows verified against main.tf.

## PR #77 Review ### DOMAIN REVIEW **Tech stack:** Terraform (OpenTofu) / Kubernetes NetworkPolicy / k8s security hardening This PR adds 9 `kubernetes_manifest` resources in a new `terraform/network-policies.tf` file, one default-deny-ingress NetworkPolicy per platform namespace. The approach is sound: `podSelector = {}` selects all pods, `policyTypes = ["Ingress"]` only restricts ingress (egress unrestricted), and each policy explicitly whitelists ingress sources via `namespaceSelector` using the well-known `kubernetes.io/metadata.name` label. **Terraform style:** Clean. All resources use `depends_on` to reference the parent namespace. Namespace references use the `kubernetes_namespace_v1.*.metadata[0].name` pattern consistently (no hardcoded namespace strings). Resource naming follows `netpol_{namespace}` convention. Comment header explains the design rationale (ArgoCD skipped, egress unrestricted). **I verified every policy's ingress rules against the actual cross-namespace traffic flows declared in `main.tf`.** The following issues were found by tracing service URLs, ServiceMonitor/PodMonitor declarations, and Blackbox Exporter probe targets. --- ### BLOCKERS **B1. forgejo policy is missing `monitoring` ingress (will break Blackbox Exporter probes + DORA exporter)** The Blackbox Exporter (deployed in `monitoring`) probes Forgejo via internal URL `http://forgejo-http.forgejo.svc.cluster.local:80` (main.tf line 415). The DORA exporter (also in `monitoring`) calls Forgejo at `http://forgejo-http.forgejo.svc.cluster.local:80` (main.tf line 1141). Both are ingress TO forgejo FROM monitoring. Current policy only allows: `tailscale`, `woodpecker`. Must also allow: `monitoring`. After apply, Blackbox will report Forgejo as DOWN and DORA metrics collection will fail. **B2. woodpecker policy is missing `monitoring` and `cnpg-system` ingress** - Blackbox Exporter probes Woodpecker at `http://woodpecker-server.woodpecker.svc.cluster.local:80` (main.tf line 420). DORA exporter hits `http://woodpecker-server.woodpecker.svc.cluster.local:80` (main.tf line 1139). Both are ingress FROM monitoring. - CNPG operator (in `cnpg-system`) manages the `woodpecker-db` Cluster CR in the `woodpecker` namespace (main.tf line 1462). The operator needs to connect to the Postgres pods it manages. This is ingress TO woodpecker FROM cnpg-system. - Prometheus scrapes the woodpecker-db PodMonitor (`enablePodMonitor = true`, main.tf line 1527) -- ingress FROM monitoring. Current policy only allows: `tailscale`, `woodpecker` (self). Must also allow: `monitoring`, `cnpg-system`. After apply, Blackbox will report Woodpecker as DOWN, DORA metrics will fail, CNPG operator will lose management of woodpecker-db (backup scheduling, failover, health checks), and Prometheus will fail to scrape woodpecker-db metrics. **B3. minio policy is missing `monitoring` and `tofu-state` ingress** - Blackbox Exporter probes MinIO at `http://minio.minio.svc.cluster.local:9000/minio/health/live` (main.tf line 450). Ingress FROM monitoring. - Prometheus scrapes MinIO metrics via ServiceMonitor (`serviceMonitor.enabled = true`, main.tf line 1041). Ingress FROM monitoring. - The TF state backup CronJob runs in the `tofu-state` namespace and connects to `http://minio.minio.svc.cluster.local:9000` (main.tf line 2028). Ingress FROM tofu-state. Current policy only allows: `tailscale`, `postgres`, `woodpecker`. Must also allow: `monitoring`, `tofu-state`. After apply, Blackbox will report MinIO as DOWN, Prometheus MinIO metrics will stop, and daily TF state backups will fail silently. **B4. harbor policy is missing `woodpecker` ingress (will break CI image pulls)** Woodpecker CI pipeline pods (in `woodpecker` namespace) pull container images from Harbor during builds (`kaniko` pushes to Harbor, pipeline pods pull base images). This is ingress TO harbor FROM woodpecker. Current policy only allows: `tailscale`, `harbor` (self), `monitoring`. Must also allow: `woodpecker`. After apply, CI pipelines that pull from Harbor will fail. **B5. cnpg-system policy is missing `monitoring` ingress (will break operator metrics scraping)** The CNPG operator has `podMonitorEnabled = true` (main.tf line 1358), meaning Prometheus scrapes the operator's metrics endpoint. This is ingress FROM monitoring TO cnpg-system. Current policy only allows: `kube-system`. Must also allow: `monitoring`. After apply, CNPG operator metrics will stop being collected by Prometheus. --- ### NITS **N1. Consider a `locals` block for repeated namespace selector patterns** The `{ namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "tailscale" } } }` pattern appears in every policy. A `locals` block with common selectors (e.g., `local.from_tailscale`, `local.from_monitoring`) would reduce repetition and make policies easier to read: ```hcl locals { from_tailscale = { namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "tailscale" } } } from_monitoring = { namespaceSelector = { matchLabels = { "kubernetes.io/metadata.name" = "monitoring" } } } } ``` **N2. postgres policy may want `monitoring` if Prometheus scrapes CNPG cluster metrics directly** CNPG clusters expose metrics via PodMonitor. If the CNPG Cluster in the `postgres` namespace has monitoring enabled (it does not appear to in this repo -- it is managed by pal-e-services), this would need monitoring ingress. Not a blocker for this PR since the `postgres` namespace CNPG cluster is defined elsewhere, but worth noting for the follow-up app-namespace phase. **N3. Comment at top could document the traffic map** A brief traffic map comment (like the one in the issue body) at the top of the file would help future maintainers understand WHY each namespace has specific ingress sources without needing to cross-reference main.tf. --- ### SOP COMPLIANCE - [x] Branch named after issue: `76-phase-8a-platform-namespace-networkpolic` (matches issue #76) - [x] PR body has: Summary, Changes, Test Plan, Related sections - [x] Related references plan slug: `plan-pal-e-platform` - [x] `Closes #76` in Related section - [x] No secrets committed - [x] No unnecessary file changes (single new file, scoped correctly) - [x] Commit messages are descriptive - [x] `tofu fmt` and `tofu validate` passed per PR body - [x] `tofu plan` output included (9 to add, 2 to change unrelated drift) --- ### PROCESS OBSERVATIONS **Change failure risk: HIGH.** This PR as-is will break monitoring (Blackbox probes, DORA metrics, Prometheus scraping) and CI (Harbor image pulls) across multiple namespaces the moment it is applied. The blast radius covers most of the platform's observability stack and CI pipeline. The fixes are straightforward -- adding missing `monitoring`, `cnpg-system`, `tofu-state`, and `woodpecker` namespace selectors to the affected policies -- but the current state would cause a multi-service outage. **MTTR consideration:** NetworkPolicy issues are notoriously hard to debug because failures manifest as silent timeouts, not explicit errors. Blackbox probes returning DOWN, CI pipelines hanging on image pull, and CNPG operator losing contact with managed databases would all appear as unrelated incidents. Including the traffic map as a comment in the file (N3) would significantly reduce future MTTR. **Positive notes:** The design is architecturally sound. Default-deny with explicit allow-lists is the correct approach. Using `kubernetes.io/metadata.name` labels is the right selector strategy. Skipping ArgoCD (Helm-managed namespace) is a reasonable scoping decision. The PR body is thorough and the test plan is sensible. --- ### VERDICT: NOT APPROVED Five blockers must be fixed: missing `monitoring` ingress on forgejo, woodpecker, minio, and cnpg-system; missing `cnpg-system` on woodpecker; missing `tofu-state` on minio; missing `woodpecker` on harbor. All are missing ingress rules that will break active cross-namespace traffic flows verified against `main.tf`.
fix: add missing monitoring, cnpg-system, woodpecker, tofu-state ingress rules
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
766d8914f6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Tofu Plan Output

tailscale_acl.this: Refreshing state... [id=acl]
helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin]
kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring]
kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres]
kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama]
kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale]
data.kubernetes_namespace_v1.pal_e_docs: Reading...
data.kubernetes_namespace_v1.tofu_state: Reading...
kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system]
data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs]
data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state]
kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak]
kubernetes_namespace_v1.minio: Refreshing state... [id=minio]
kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo]
kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor]
kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url]
kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials]
kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data]
helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator]
kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin]
kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
helm_release.loki_stack: Refreshing state... [id=loki-stack]
kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack]
helm_release.cnpg: Refreshing state... [id=cnpg]
kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard]
helm_release.forgejo: Refreshing state... [id=forgejo]
kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
helm_release.ollama: Refreshing state... [id=ollama]
kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel]
kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource]
kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel]
kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel]
helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter]
kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard]
kubernetes_manifest.blackbox_alerts: Refreshing state...
helm_release.harbor: Refreshing state... [id=harbor]
kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard]
helm_release.minio: Refreshing state... [id=minio]
kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_manifest.dora_exporter_service_monitor: Refreshing state...
kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel]
kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel]
minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal]
minio_iam_user.tf_backup: Refreshing state... [id=tf-backup]
minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup]
minio_s3_bucket.assets: Refreshing state... [id=assets]
kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel]
kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel]
minio_iam_user.cnpg: Refreshing state... [id=cnpg]
minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal]
minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups]
minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001]
minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001]
kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds]
kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds]
kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds]
kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify]
kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_manifest.woodpecker_postgres: Refreshing state...
helm_release.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel]

OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

OpenTofu will perform the following actions:

  # kubernetes_manifest.netpol_cnpg_system will be created
  + resource "kubernetes_manifest" "netpol_cnpg_system" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "cnpg-system"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "kube-system"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "cnpg-system"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "kube-system"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_forgejo will be created
  + resource "kubernetes_manifest" "netpol_forgejo" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "forgejo"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "forgejo"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_harbor will be created
  + resource "kubernetes_manifest" "netpol_harbor" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "harbor"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "harbor"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "harbor"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "harbor"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_keycloak will be created
  + resource "kubernetes_manifest" "netpol_keycloak" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "keycloak"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "keycloak"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_minio will be created
  + resource "kubernetes_manifest" "netpol_minio" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "minio"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "postgres"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tofu-state"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "minio"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "postgres"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tofu-state"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_monitoring will be created
  + resource "kubernetes_manifest" "netpol_monitoring" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "monitoring"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "monitoring"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_ollama will be created
  + resource "kubernetes_manifest" "netpol_ollama" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "ollama"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "ollama"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_postgres will be created
  + resource "kubernetes_manifest" "netpol_postgres" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "postgres"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "postgres"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "pal-e-docs"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

  # kubernetes_manifest.netpol_woodpecker will be created
  + resource "kubernetes_manifest" "netpol_woodpecker" {
      + manifest = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + name      = "default-deny-ingress"
              + namespace = "woodpecker"
            }
          + spec       = {
              + ingress     = [
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                            },
                        ]
                    },
                  + {
                      + from = [
                          + {
                              + namespaceSelector = {
                                  + matchLabels = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                            },
                        ]
                    },
                ]
              + podSelector = {}
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
      + object   = {
          + apiVersion = "networking.k8s.io/v1"
          + kind       = "NetworkPolicy"
          + metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + finalizers                 = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
              + name                       = "default-deny-ingress"
              + namespace                  = "woodpecker"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
            }
          + spec       = {
              + egress      = (known after apply)
              + ingress     = [
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "tailscale"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "woodpecker"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "monitoring"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                  + {
                      + from  = [
                          + {
                              + ipBlock           = {
                                  + cidr   = (known after apply)
                                  + except = (known after apply)
                                }
                              + namespaceSelector = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = {
                                      + "kubernetes.io/metadata.name" = "cnpg-system"
                                    }
                                }
                              + podSelector       = {
                                  + matchExpressions = (known after apply)
                                  + matchLabels      = (known after apply)
                                }
                            },
                        ]
                      + ports = (known after apply)
                    },
                ]
              + podSelector = {
                  + matchExpressions = (known after apply)
                  + matchLabels      = (known after apply)
                }
              + policyTypes = [
                  + "Ingress",
                ]
            }
        }
    }

Plan: 9 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't
guarantee to take exactly these actions if you run "tofu apply" now.
## Tofu Plan Output ``` tailscale_acl.this: Refreshing state... [id=acl] helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] data.kubernetes_namespace_v1.pal_e_docs: Reading... data.kubernetes_namespace_v1.tofu_state: Reading... kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs] data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] kubernetes_namespace_v1.minio: Refreshing state... [id=minio] kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url] kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials] kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.loki_stack: Refreshing state... [id=loki-stack] kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] helm_release.cnpg: Refreshing state... [id=cnpg] kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard] helm_release.forgejo: Refreshing state... [id=forgejo] kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] helm_release.ollama: Refreshing state... [id=ollama] kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter] kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] kubernetes_manifest.blackbox_alerts: Refreshing state... helm_release.harbor: Refreshing state... [id=harbor] kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard] helm_release.minio: Refreshing state... [id=minio] kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel] minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] minio_s3_bucket.assets: Refreshing state... [id=assets] kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] minio_iam_user.cnpg: Refreshing state... [id=cnpg] minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001] minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001] kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds] kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds] kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify] kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_manifest.woodpecker_postgres: Refreshing state... helm_release.woodpecker: Refreshing state... [id=woodpecker] kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create OpenTofu will perform the following actions: # kubernetes_manifest.netpol_cnpg_system will be created + resource "kubernetes_manifest" "netpol_cnpg_system" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "cnpg-system" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "kube-system" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "cnpg-system" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "kube-system" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_forgejo will be created + resource "kubernetes_manifest" "netpol_forgejo" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "forgejo" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "forgejo" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_harbor will be created + resource "kubernetes_manifest" "netpol_harbor" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "harbor" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "harbor" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "harbor" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "harbor" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_keycloak will be created + resource "kubernetes_manifest" "netpol_keycloak" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "keycloak" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "keycloak" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_minio will be created + resource "kubernetes_manifest" "netpol_minio" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "minio" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "postgres" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tofu-state" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "minio" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "postgres" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tofu-state" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_monitoring will be created + resource "kubernetes_manifest" "netpol_monitoring" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "monitoring" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "monitoring" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_ollama will be created + resource "kubernetes_manifest" "netpol_ollama" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "ollama" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "ollama" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_postgres will be created + resource "kubernetes_manifest" "netpol_postgres" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "postgres" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "postgres" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "pal-e-docs" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } # kubernetes_manifest.netpol_woodpecker will be created + resource "kubernetes_manifest" "netpol_woodpecker" { + manifest = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + name = "default-deny-ingress" + namespace = "woodpecker" } + spec = { + ingress = [ + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } }, ] }, + { + from = [ + { + namespaceSelector = { + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } }, ] }, ] + podSelector = {} + policyTypes = [ + "Ingress", ] } } + object = { + apiVersion = "networking.k8s.io/v1" + kind = "NetworkPolicy" + metadata = { + annotations = (known after apply) + creationTimestamp = (known after apply) + deletionGracePeriodSeconds = (known after apply) + deletionTimestamp = (known after apply) + finalizers = (known after apply) + generateName = (known after apply) + generation = (known after apply) + labels = (known after apply) + managedFields = (known after apply) + name = "default-deny-ingress" + namespace = "woodpecker" + ownerReferences = (known after apply) + resourceVersion = (known after apply) + selfLink = (known after apply) + uid = (known after apply) } + spec = { + egress = (known after apply) + ingress = [ + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "tailscale" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "woodpecker" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "monitoring" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, + { + from = [ + { + ipBlock = { + cidr = (known after apply) + except = (known after apply) } + namespaceSelector = { + matchExpressions = (known after apply) + matchLabels = { + "kubernetes.io/metadata.name" = "cnpg-system" } } + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } }, ] + ports = (known after apply) }, ] + podSelector = { + matchExpressions = (known after apply) + matchLabels = (known after apply) } + policyTypes = [ + "Ingress", ] } } } Plan: 9 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run "tofu apply" now. ```
Author
Owner

PR #77 Re-Review

DOMAIN REVIEW

Tech stack: Terraform (OpenTofu) / Kubernetes NetworkPolicies / k3s

Single new file: terraform/network-policies.tf (195 lines). Creates 9 kubernetes_manifest NetworkPolicy resources, one per platform namespace managed by kubernetes_namespace_v1. Each policy is default-deny ingress with explicit allow-list of source namespaces.

Previous review found 5 blockers -- all 5 concerned missing ingress rules that would break real traffic flows. Verified each against the updated diff:

# Blocker Required Rule Status
1 monitoring -> forgejo (Prometheus scrapes Forgejo metrics) netpol_forgejo allows from monitoring RESOLVED (line 45)
2 monitoring + cnpg-system -> woodpecker (Prometheus scrapes + CNPG operator manages woodpecker-db) netpol_woodpecker allows from monitoring and cnpg-system RESOLVED (lines 66-67)
3 monitoring + tofu-state -> minio (Prometheus scrapes MinIO metrics via ServiceMonitor + tofu-state CronJob backs up to MinIO) netpol_minio allows from monitoring and tofu-state RESOLVED (lines 111-112)
4 woodpecker -> harbor (CI pipelines push images via kaniko) netpol_harbor allows from woodpecker RESOLVED (line 89)
5 monitoring -> cnpg-system (Prometheus scrapes CNPG operator PodMonitor) netpol_cnpg_system allows from monitoring RESOLVED (line 190)

Terraform patterns verified:

  • All 9 resources use kubernetes_manifest (appropriate for raw k8s manifests in Terraform)
  • All use depends_on referencing the parent namespace resource (prevents race conditions)
  • All reference namespace names via kubernetes_namespace_v1.*.metadata[0].name (no hardcoded strings)
  • podSelector = {} correctly selects all pods in each namespace
  • policyTypes = ["Ingress"] only -- egress unrestricted (documented in header comment)
  • tofu fmt and tofu validate confirmed clean per PR body

Cross-reference verification of all 9 policies against main.tf:

  • monitoring: tailscale (funnel) + self (Prometheus/Grafana/Alertmanager mesh). Correct.
  • forgejo: tailscale + woodpecker (CI webhooks/clones) + monitoring (Blackbox probe + scrapes). Correct.
  • woodpecker: tailscale + self (agent-server) + monitoring (scrapes + PodMonitor on CNPG) + cnpg-system (operator manages woodpecker-db Cluster CR). Correct.
  • harbor: tailscale + self (internal component mesh) + monitoring (ServiceMonitor scrapes) + woodpecker (kaniko image pushes). Correct.
  • minio: tailscale + postgres (CNPG WAL backup CronJob) + woodpecker (woodpecker-db WAL archiving to MinIO) + monitoring (ServiceMonitor scrapes) + tofu-state (tf-state-backup CronJob). Correct.
  • keycloak: tailscale only. Correct -- all OIDC/JWKS traffic from apps goes through external Tailscale URLs.
  • postgres: pal-e-docs (DB connection via pal-e-postgres-rw.postgres.svc) + cnpg-system (operator). Correct.
  • ollama: pal-e-docs only. Correct.
  • cnpg-system: kube-system (webhook callbacks) + monitoring (PodMonitor scrapes). Correct.

BLOCKERS

None. All 5 previous blockers are resolved.

NITS

  1. Tailscale namespace excluded without comment: 10 kubernetes_namespace_v1 resources exist but only 9 get policies. The PR body says "9 platform namespaces" without explaining why tailscale is excluded. Adding a one-line comment in the header (e.g., "Tailscale namespace excluded -- it handles ingress proxying for all funnels and must accept arbitrary inbound traffic") would prevent future confusion about whether it was forgotten.

  2. ArgoCD exclusion rationale: The header comment explains ArgoCD is skipped because its namespace is Helm-created, not standalone. This is accurate but worth noting as a future gap -- ArgoCD should eventually get a NetworkPolicy too (perhaps in a follow-up issue).

SOP COMPLIANCE

  • Branch named after issue (76-phase-8a-platform-namespace-networkpolic)
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related references plan-pal-e-platform
  • Closes #76 in Related section
  • No secrets committed
  • No unnecessary file changes (single new file, scope matches issue)
  • Commit messages are descriptive
  • tofu fmt and tofu validate confirmed clean
  • tofu plan output documented (9 to add, 2 pre-existing drift changes)

PROCESS OBSERVATIONS

  • Change Failure Risk: LOW after fix commit. All 5 missing ingress rules have been added. The traffic flow analysis covers Prometheus scraping (ServiceMonitors, PodMonitors, Blackbox probes), CNPG operator management, CI image pushes, and backup CronJobs.
  • Deployment note: The 2 pre-existing drift changes (dora_exporter secret write-only attribute) are unrelated to this PR and should not cause concern during apply.
  • Post-apply smoke test items in the Test Plan are not yet checked -- this is expected (pre-merge). After apply, verify Grafana dashboards, Forgejo UI, Woodpecker pipelines, and CNPG backup jobs all function.

VERDICT: APPROVED

## PR #77 Re-Review ### DOMAIN REVIEW **Tech stack**: Terraform (OpenTofu) / Kubernetes NetworkPolicies / k3s Single new file: `terraform/network-policies.tf` (195 lines). Creates 9 `kubernetes_manifest` NetworkPolicy resources, one per platform namespace managed by `kubernetes_namespace_v1`. Each policy is default-deny ingress with explicit allow-list of source namespaces. **Previous review found 5 blockers** -- all 5 concerned missing ingress rules that would break real traffic flows. Verified each against the updated diff: | # | Blocker | Required Rule | Status | |---|---------|---------------|--------| | 1 | monitoring -> forgejo (Prometheus scrapes Forgejo metrics) | `netpol_forgejo` allows from `monitoring` | RESOLVED (line 45) | | 2 | monitoring + cnpg-system -> woodpecker (Prometheus scrapes + CNPG operator manages woodpecker-db) | `netpol_woodpecker` allows from `monitoring` and `cnpg-system` | RESOLVED (lines 66-67) | | 3 | monitoring + tofu-state -> minio (Prometheus scrapes MinIO metrics via ServiceMonitor + tofu-state CronJob backs up to MinIO) | `netpol_minio` allows from `monitoring` and `tofu-state` | RESOLVED (lines 111-112) | | 4 | woodpecker -> harbor (CI pipelines push images via kaniko) | `netpol_harbor` allows from `woodpecker` | RESOLVED (line 89) | | 5 | monitoring -> cnpg-system (Prometheus scrapes CNPG operator PodMonitor) | `netpol_cnpg_system` allows from `monitoring` | RESOLVED (line 190) | **Terraform patterns verified:** - All 9 resources use `kubernetes_manifest` (appropriate for raw k8s manifests in Terraform) - All use `depends_on` referencing the parent namespace resource (prevents race conditions) - All reference namespace names via `kubernetes_namespace_v1.*.metadata[0].name` (no hardcoded strings) - `podSelector = {}` correctly selects all pods in each namespace - `policyTypes = ["Ingress"]` only -- egress unrestricted (documented in header comment) - `tofu fmt` and `tofu validate` confirmed clean per PR body **Cross-reference verification of all 9 policies against `main.tf`:** - **monitoring**: tailscale (funnel) + self (Prometheus/Grafana/Alertmanager mesh). Correct. - **forgejo**: tailscale + woodpecker (CI webhooks/clones) + monitoring (Blackbox probe + scrapes). Correct. - **woodpecker**: tailscale + self (agent-server) + monitoring (scrapes + PodMonitor on CNPG) + cnpg-system (operator manages woodpecker-db Cluster CR). Correct. - **harbor**: tailscale + self (internal component mesh) + monitoring (ServiceMonitor scrapes) + woodpecker (kaniko image pushes). Correct. - **minio**: tailscale + postgres (CNPG WAL backup CronJob) + woodpecker (woodpecker-db WAL archiving to MinIO) + monitoring (ServiceMonitor scrapes) + tofu-state (tf-state-backup CronJob). Correct. - **keycloak**: tailscale only. Correct -- all OIDC/JWKS traffic from apps goes through external Tailscale URLs. - **postgres**: pal-e-docs (DB connection via `pal-e-postgres-rw.postgres.svc`) + cnpg-system (operator). Correct. - **ollama**: pal-e-docs only. Correct. - **cnpg-system**: kube-system (webhook callbacks) + monitoring (PodMonitor scrapes). Correct. ### BLOCKERS None. All 5 previous blockers are resolved. ### NITS 1. **Tailscale namespace excluded without comment**: 10 `kubernetes_namespace_v1` resources exist but only 9 get policies. The PR body says "9 platform namespaces" without explaining why tailscale is excluded. Adding a one-line comment in the header (e.g., "Tailscale namespace excluded -- it handles ingress proxying for all funnels and must accept arbitrary inbound traffic") would prevent future confusion about whether it was forgotten. 2. **ArgoCD exclusion rationale**: The header comment explains ArgoCD is skipped because its namespace is Helm-created, not standalone. This is accurate but worth noting as a future gap -- ArgoCD should eventually get a NetworkPolicy too (perhaps in a follow-up issue). ### SOP COMPLIANCE - [x] Branch named after issue (`76-phase-8a-platform-namespace-networkpolic`) - [x] PR body has Summary, Changes, Test Plan, Related sections - [x] Related references `plan-pal-e-platform` - [x] `Closes #76` in Related section - [x] No secrets committed - [x] No unnecessary file changes (single new file, scope matches issue) - [x] Commit messages are descriptive - [x] `tofu fmt` and `tofu validate` confirmed clean - [x] `tofu plan` output documented (9 to add, 2 pre-existing drift changes) ### PROCESS OBSERVATIONS - **Change Failure Risk**: LOW after fix commit. All 5 missing ingress rules have been added. The traffic flow analysis covers Prometheus scraping (ServiceMonitors, PodMonitors, Blackbox probes), CNPG operator management, CI image pushes, and backup CronJobs. - **Deployment note**: The 2 pre-existing drift changes (dora_exporter secret write-only attribute) are unrelated to this PR and should not cause concern during apply. - **Post-apply smoke test items** in the Test Plan are not yet checked -- this is expected (pre-merge). After apply, verify Grafana dashboards, Forgejo UI, Woodpecker pipelines, and CNPG backup jobs all function. ### VERDICT: APPROVED
forgejo_admin deleted branch 76-phase-8a-platform-namespace-networkpolic 2026-03-15 04:05:35 +00:00
Sign in to join this conversation.
No description provided.