A generic, Jinja2-template-driven Kubernetes operator engine (kopf-based), plus Secure Namespace Operator, a full reference implementation of that engine for automated, security-first namespace provisioning.
This repository ships one Helm chart with two clearly separated parts:
| Part | Path | What it is |
|---|---|---|
| Generic engine | CHARTS/templates/core/ |
CRD-agnostic: reads any CRD, renders Jinja2 templates, applies/deletes the resulting K8s resources, tracks revisions. Never edited to add a new implementation. |
| Example implementation | CHARTS/templates/implementations/secure-namespace/ |
The SecureNamespace CRD, its Jinja2 templates (namespace, quotas, NetworkPolicies, ingress, VIP egress gateway), Kyverno policies and RBAC. This is what the rest of this README documents in detail. |
Which CRD the engine watches is controlled entirely by values.yaml::crd (group/version/kind/plural)
— see Building Your Own Implementation. Nothing in templates/core/
hardcodes SecureNamespace.
- Overview
- Architecture
- Building Your Own Implementation
- Prerequisites
- Deployment (Helm)
- Namespace Instantiation
- CRD Reference
- Template Values Reference
- Customization Guide
- Kyverno Policies
- Monitoring & Troubleshooting
- Complete Examples
The Secure Namespace Operator example is a Python-based Kubernetes operator that provides a declarative way to create and manage secure, isolated namespaces with pre-configured networking policies, resource quotas, ingress controllers, and egress gateways — built entirely on top of the generic engine below.
Key Features:
- Security-first: Automatic network isolation via Cilium NetworkPolicies
- Multi-Ingress Support: NGINX, HAProxy, or Traefik ingress controllers per namespace
- Egress Control: Automated VIP-based egress gateway with L2 announcement
- Resource Management: Configurable quotas for compute, storage, and Kubernetes objects
- Revision History: Full audit trail of all configuration changes
- Generic Framework: Template-based architecture — all business logic lives in Jinja2 templates, the engine itself doesn't know it's managing namespaces
- GitOps Ready: Fully declarative with drift correction via periodic reconciliation
┌──────────────────────────────────────────────────────────┐
│ namespace-operator ns │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Main Operator │ │ VIP Controller │ │
│ │ (kopf-based) │ │ (per SecureNamespace) │ │
│ └────────┬────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ ┌────────▼─────────────────────────▼─────────────┐ │
│ │ ConfigMap Templates │ │
│ │ templates-core / templates-network / │ │
│ │ templates-ingress-* / templates-rbac / │ │
│ │ vip-controller-templates / ... │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│
┌────────────▼────────────┐
│ <crd.group>/<version> │
│ <crd.kind> (Cluster) │
│ = SecureNamespace by │
│ default │
└────────────────────────┘
CHARTS/
├── Chart.yaml, values.yaml # values.crd.{group,version,kind,plural} selects the CRD
├── templates/
│ ├── _helpers.tpl # shared Helm label/name helpers
│ ├── core/ # ── GENERIC ENGINE — reusable for any CRD ──
│ │ ├── 10_deployment.yml # operator Deployment, injects CRD_GROUP/VERSION/PLURAL env
│ │ └── CODE/
│ │ ├── 3_operator.yml # 8 embedded Python modules (kopf handlers, template
│ │ │ # rendering, resource apply/delete, revision tracking)
│ │ └── 2_VIP-controller.yml # per-CR sub-controller: same render/apply pipeline as the
│ │ # operator, spawned by it as its own Deployment per CR
│ └── implementations/
│ └── secure-namespace/ # ── EXAMPLE IMPLEMENTATION (declarative only) ──
│ ├── CRD/0_CRD.yml # the SecureNamespace CustomResourceDefinition
│ ├── RBAC/10_roles.yaml # RBAC for the resource kinds this example manages
│ ├── KYVERNO_rules/ # admission policies (mutate/validate)
│ ├── TEMPLATES/ # Jinja2 templates: namespace, quotas, network, RBAC, VIP
│ └── CONTROLER/TEMPLATES/ # Jinja2 templates for the VIP controller (ingress
│ # controller manifests, VIP status patches)
└── examples/ # sample SecureNamespace custom resources to `kubectl apply`
The operator is split into focused modules, all embedded as ConfigMap data and mounted into the operator pod.
None of them reference SecureNamespace — the CRD identity comes entirely from the CRD_GROUP/CRD_VERSION/
CRD_PLURAL environment variables (see values.yaml::crd):
| Module | Role |
|---|---|
operator.py |
Entry point, logging initialization |
config.py |
Environment variables and constants, including CRD_GROUP/CRD_VERSION/CRD_PLURAL |
handlers.py |
Kopf event handlers (@on.create, @on.update, @on.timer, @on.delete) — registered on (CRD_GROUP, CRD_VERSION, CRD_PLURAL), not a hardcoded kind |
crd_manager.py |
CRD introspection, default extraction, label/annotation helpers |
template_manager.py |
Jinja2 template loading and rendering (custom delimiters [{ }]/[% %]/[# #] to avoid colliding with Helm's {{ }}) |
resource_manager.py |
Generic Kubernetes resource apply/delete via dynamic client |
revision_manager.py |
Revision counter and history ConfigMap management |
utils.py |
Diff formatting, deep merge, spec serialization |
The engine offers a second generic pattern, at a different scope than the operator. The operator
watches the CRD as a whole — every instance, cluster-wide. A controller runs the exact same generic
engine (load_templates_order() → load_templates() → Jinja2 render → apply/delete resources — its
own class-based ResourceManager/TemplateManager, parallel implementations of the operator's
function-based ones) but dedicated to a single CRD instance, spawned by the operator as its own
Deployment (one controller instance per SecureNamespace, via
implementations/secure-namespace/TEMPLATES/1.7_templates_vip_controller.yml). That's why it lives in
core/ alongside the operator rather than under implementations/ — the mechanism itself (watch,
render, apply, all template-driven) is just as CRD-agnostic as the operator's.
What's implementation-specific is what a given controller additionally watches beyond its one CRD
instance: this reference implementation's controller.py (watch loop) and node_finder.py (node
selection) track a Service's LoadBalancer VIP and drive Cilium egress/L2 policies — that choice of
target and the business logic around it belong to secure-namespace, not to the engine. A different
implementation reusing this engine either adapts that watch/business logic for its own use case, or
skips spawning a controller entirely — it's optional, unlike the operator.
To point this engine at a different CRD instead of (or alongside) SecureNamespace:
- Set
values.yaml::crd.group/.version/.kind/.pluralto your CRD's identity. - Add
CHARTS/templates/implementations/<your-name>/with your ownCRD/, RBAC, and Jinja2TEMPLATES/(using[{ variable }]/[% if ... %]/[# comment #]— never{{ }}, which Helm consumes first). - Two functions in
templates/core/CODE/3_operator.ymlstill encode SecureNamespace's specific spec shape (crd_manager.py::extract_namespace_info(),template_manager.py::get_template_values()) — you'll adapt those for your own fields. Everything else incore/(kopf registration, resource apply/delete, revision tracking, the Jinja2 render engine itself) is genuinely CRD-agnostic. - Any label/annotation prefix your templates want to share with the engine's own revision-tracking
annotations should reuse
{{ .Values.crd.group }}in Helm-rendered files, or theCRD_GROUPenv var in Python, rather than hardcoding a domain.
Full walkthrough, with secure-namespace referenced file-by-file at each step:
documentation/HOWTO-NEW-IMPLEMENTATION.md.
All business logic resides in Jinja2 templates. The Python code is a lightweight orchestrator that:
- Detects events (create/update/delete/timer)
- Builds a
valuesdictionary from the CRD spec - Renders each template in order
- Applies/deletes the resulting Kubernetes resources
This means configuration changes often require only template edits — no Python code modifications.
Templates are stored in Kubernetes ConfigMaps and loaded at reconciliation time. The execution order is defined in a dedicated ConfigMap (secure-namespace-operator-templates-order). Each entry uses the format configmap-name/template-key.
templates-order.yaml:
templates:
- templates-core/namespace.yaml → ConfigMap "templates-core", key "namespace.yaml"
- templates-network/network-policy.yaml → ConfigMap "templates-network", key "network-policy.yaml"
- ...
To avoid conflicts with Helm's {{ }} syntax, templates use custom delimiters:
| Purpose | Delimiter |
|---|---|
| Variables | [{ variable }] |
| Blocks | [% if condition %] ... [% endif %] |
| Comments | [# comment #] |
Templates use a _action field to signal the operator:
# _action: apply → create or patch the resource
# _action: delete → delete the resource if it exists
# (no _action) → treated as apply
[% if spec.ingress.enabled %]
_action: apply
spec:
...
[% else %]
_action: delete
[% endif %]- Kubernetes 1.24+
- Helm 3.x
- Cilium CNI with L2 announcement support (for egress VIP)
- cert-manager (for webhook TLS, if
certManager.enabled: true) - Kyverno (optional, for ingress admission policies)
- Harbor or equivalent container registry (for operator image)
# Namespace where the operator is deployed
namespace:
name: namespace-operator
create: true
# Base DNS domain (used for webhook certificates)
dnsDomain: infra.example.com
# Operator configuration
operator:
debug_level: INFO # DEBUG, INFO, WARNING, ERROR
reconcile:
intervalSeconds: 300 # Drift correction interval (seconds)
initialDelaySeconds: 60 # Delay before first reconciliation after startup
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
# Corporate proxy (optional)
proxy:
enabled: true
http: "http://proxy.example.com:8080"
https: "http://proxy.example.com:8080"
noProxy: "127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,localhost,.example.com"
# Webhook
webhook:
replicas: 1
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
# cert-manager
certManager:
enabled: true
issuer:
create: true
# ServiceAccount for the operator
serviceAccount:
create: true
name: secure-namespace-operator
annotations: {}# Add / update the chart repository if applicable
helm repo add secure-ns https://your-registry/helm-charts
helm repo update
# Install
helm install secure-namespace-operator ./chart \
--namespace namespace-operator \
--create-namespace \
-f values.yaml
# Or upgrade
helm upgrade secure-namespace-operator ./chart \
--namespace namespace-operator \
-f values.yaml# Check operator pod
kubectl get pods -n namespace-operator
# Check CRD
kubectl get crd securenamespaces.secure-ns.example.com
# Check template ConfigMaps
kubectl get configmaps -n namespace-operator
# Operator logs
kubectl logs -n namespace-operator deployment/secure-namespace-operator -fThe operator deployment accepts the following environment variables (set via Helm values):
| Variable | Default | Description |
|---|---|---|
OPERATOR_NAMESPACE |
namespace-operator |
Namespace where the operator runs |
LOG_LEVEL |
INFO |
Python logging level |
RECONCILE_INTERVAL_SECONDS |
300 |
Periodic drift correction interval |
RECONCILE_INITIAL_DELAY_SECONDS |
60 |
Startup delay before first reconciliation |
apiVersion: secure-ns.example.com/v1alpha1
kind: SecureNamespace
metadata:
name: myapp-dev
spec:
project:
solution: myapp
environment: DEVThis creates a namespace myapp-dev with default resource quotas and network isolation enabled.
kubectl apply -f myapp-dev.yamlThe operator will:
- Create the namespace
myapp-dev - Apply default resource quotas
- Apply Cilium NetworkPolicies (isolation)
- Record revision
1in the history ConfigMap
# List all SecureNamespaces (short column view)
kubectl get sns
# Detailed view
kubectl get securenamespace myapp-dev -o yaml
# Check the created namespace
kubectl get ns myapp-dev --show-labels
# View revision history
kubectl get configmap myapp-dev-history -n namespace-operator \
-o jsonpath='{.data.history\.txt}'Edit the SecureNamespace resource — the operator detects spec changes and reconciles automatically:
kubectl edit securenamespace myapp-dev
# or
kubectl patch securenamespace myapp-dev --type=merge -p '{"spec":{"ingress":{"enabled":true}}}'kubectl delete securenamespace myapp-devThe operator's finalizer ensures controlled cleanup of all managed resources before the SecureNamespace object is removed. The history ConfigMap is preserved in the operator namespace.
| Field | Type | Required | Values | Description |
|---|---|---|---|---|
solution |
string | ✅ | any | Project/solution name |
environment |
string | ✅ | TEST, DEV, INTEG, PROD |
Environment identifier |
contact.name |
string | — | any | Responsible person name |
contact.email |
string | — | valid email | Responsible person email |
Generated namespace name: {solution}-{environment} (lowercased), e.g. myapp-dev.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable resource quotas |
compute.requests.cpu |
string | "1024m" |
Total CPU requests for the namespace |
compute.requests.memory |
string | "1Gi" |
Total memory requests |
compute.limits.cpu |
string | "2048m" |
Total CPU limits |
compute.limits.memory |
string | "2Gi" |
Total memory limits |
storage.requests.storage |
string | "10Gi" |
Total storage requests |
storage.persistentvolumeclaims |
integer | 2 |
Max PVCs |
objects.pods |
integer | 5 |
Max pods |
objects.services |
integer | 5 |
Max services |
objects.configmaps |
integer | 5 |
Max ConfigMaps |
objects.secrets |
integer | 5 |
Max Secrets |
When enabled: false, the ResourceQuota object is deleted from the namespace.
| Field | Type | Default | Description |
|---|---|---|---|
interfaceName |
string | "ens192" |
Network interface used for egress traffic |
isolationEnabled |
boolean | true |
Enable Cilium network isolation (deny all by default) |
externalAccess.enabled |
boolean | false |
Enable external access rules |
externalAccess.rules |
array | [] |
IP/FQDN egress rules |
externalAccess.services |
array | [] |
Access to specific Kubernetes services |
externalAccess.namespaces |
array | [] |
Access to entire namespaces |
externalAccess.standard |
object | — | Standard infrastructure service flags |
| Field | Type | Description |
|---|---|---|
name |
string | Rule name (required) |
description |
string | Human-readable description |
cidrs |
array[string] | CIDR blocks to allow (e.g. 10.0.5.0/24) |
fqdns |
array[string] | FQDNs to allow (e.g. api.example.com) |
ports |
array | Port/protocol restrictions (port, `protocol: TCP |
| Field | Type | Description |
|---|---|---|
name |
string | Kubernetes service name (required) |
namespace |
string | Service namespace (required) |
ports |
array | Optional port restrictions |
| Field | Type | Description |
|---|---|---|
name |
string | Namespace name (required) |
ports |
array | Optional port restrictions |
Pre-defined boolean flags to allow access to common infrastructure services without manually writing CIDR/FQDN rules. The actual network targets are defined in the templates-network/network-infrastructure-access.yaml template.
| Flag | Default | Description |
|---|---|---|
logging |
true |
Access to centralized logging stack |
proxy |
false |
Access to corporate HTTP proxy |
vault |
false |
Access to HashiCorp Vault |
git |
false |
Access to Git repositories |
s3 |
false |
Access to S3-compatible storage |
smtp |
false |
Access to SMTP relay |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Deploy an ingress controller + LoadBalancer service |
serviceName |
string | "lb-gateway" |
Name of the LoadBalancer service |
domain |
string | "k8s-staging.secure-ns.example.com" |
Base domain for DNS/TLS |
controller.type |
string | "traefik" |
Ingress controller type: nginx, haproxy, traefik |
controller.replicas |
integer | 1 |
Number of controller replicas |
controller.nodeSelector.label |
string | "secure-ns/app" |
Node label key for placement |
controller.nodeSelector.value |
string | "GENERIC" |
Node label value for placement |
controller.resources.requests.cpu |
string | "100m" |
CPU request per replica |
controller.resources.requests.memory |
string | "128Mi" |
Memory request per replica |
controller.resources.limits.cpu |
string | "500m" |
CPU limit per replica |
controller.resources.limits.memory |
string | "512Mi" |
Memory limit per replica |
Generated full domain: {environment}.{solution}.{domain}
Example: dev.myapp.k8s-staging.secure-ns.example.com
Each namespace gets its own dedicated IngressClass named after the namespace, ensuring ingress resources are routed to the correct controller.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Deploy a dedicated VIP controller for this namespace |
reconcileIntervalSeconds |
integer | 30 |
VIP controller reconciliation interval (10–300s) |
The VIP controller is a separate Python pod deployed per namespace. It:
- Watches the LoadBalancer service for VIP assignment
- Selects an eligible node (via
nodeSelector) to host the VIP - Adds the VIP to the node's network interface (
spec.network.interfaceName) - Creates
CiliumEgressGatewayPolicyandCiliumL2AnnouncementPolicy - Automatically migrates the VIP if the hosting node becomes unavailable
VIP and node assignments are reflected in the SecureNamespace annotations:
| Annotation | Description |
|---|---|
current-vip |
Active egress VIP address |
current-node |
Node currently hosting the VIP |
last-vip |
Previous VIP (after migration) |
last-node |
Previous node (after migration) |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Deploy a demo application (nginx) |
image |
string | "nginx:latest" |
Container image |
hostPrefix |
string | "app" |
Hostname prefix for ingress |
Generated URL: https://{hostPrefix}.{environment}.{solution}.{domain}
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Create a ServiceAccount with RBAC |
name |
string | "app-sa" |
ServiceAccount name |
roles |
array | [] |
Namespace-scoped rules (creates a Role + RoleBinding) |
clusterroles |
array | [] |
Cluster-scoped rules (creates a ClusterRole + ClusterRoleBinding) |
RBAC rule format (for both roles and clusterroles):
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
resourceNames: [] # Optional: restrict to specific named resourcesWhen a template is rendered, the following variables are available:
| Variable | Type | Description |
|---|---|---|
solution |
string | spec.project.solution (lowercased) |
environment |
string | spec.project.environment (lowercased) |
namespace_name |
string | Generated namespace name: {solution}-{environment} |
securenamespace_name |
string | Name of the SecureNamespace CR object |
operator_namespace |
string | Namespace where the operator runs |
revision |
integer | Current revision number |
domain |
string | Full domain: {environment}.{solution}.{ingress.domain} |
labels |
dict | Common labels to apply to all resources |
annotations |
dict | Common annotations to apply to all resources |
spec |
dict | Full CRD spec with all defaults applied |
The spec variable mirrors the CRD structure with defaults applied from the CRD schema. All fields are accessible with dot notation:
spec.project.solution
spec.project.environment
spec.project.contact.name
spec.project.contact.email
spec.quotas.enabled
spec.quotas.compute['requests.cpu']
spec.quotas.compute['requests.memory']
spec.quotas.compute['limits.cpu']
spec.quotas.compute['limits.memory']
spec.quotas.storage['requests.storage']
spec.quotas.storage.persistentvolumeclaims
spec.quotas.objects.pods
spec.quotas.objects.services
spec.quotas.objects.configmaps
spec.quotas.objects.secrets
spec.network.interfaceName
spec.network.isolationEnabled
spec.network.externalAccess.enabled
spec.network.externalAccess.rules # list of rule objects
spec.network.externalAccess.services # list of service objects
spec.network.externalAccess.namespaces # list of namespace objects
spec.network.externalAccess.standard.logging
spec.network.externalAccess.standard.proxy
spec.network.externalAccess.standard.vault
spec.network.externalAccess.standard.git
spec.network.externalAccess.standard.s3
spec.network.externalAccess.standard.smtp
spec.ingress.enabled
spec.ingress.serviceName
spec.ingress.domain
spec.ingress.controller.type
spec.ingress.controller.replicas
spec.ingress.controller.nodeSelector.label
spec.ingress.controller.nodeSelector.value
spec.ingress.controller.resources.requests.cpu
spec.ingress.controller.resources.requests.memory
spec.ingress.controller.resources.limits.cpu
spec.ingress.controller.resources.limits.memory
spec.egressController.enabled
spec.egressController.reconcileIntervalSeconds
spec.example.enabled
spec.example.image
spec.example.hostPrefix
spec.serviceaccount.enabled
spec.serviceaccount.name
spec.serviceaccount.roles # list
spec.serviceaccount.clusterroles # list
Common labels are pre-built and should be applied to all managed resources:
labels:
app.kubernetes.io/managed-by: secure-namespace-operator
secure-ns.example.com/securenamespace: [{ securenamespace_name }]
secure-ns.example.com/solution: [{ solution }]
secure-ns.example.com/environment: [{ environment }]
secure-ns.example.com/revision: "[{ revision }]"- Create or edit the appropriate ConfigMap (e.g.
templates-core,templates-network, or a new one). - Add the template key in the ConfigMap
datasection. - Register it in
secure-namespace-operator-templates-orderConfigMap undertemplates:.
Example — adding a LimitRange template:
Step 1 — Add to templates-core ConfigMap:
limit-range.yaml: |
apiVersion: v1
kind: LimitRange
metadata:
name: [{ namespace_name }]-limits
namespace: [{ namespace_name }]
[% if spec.quotas.enabled %]
_action: apply
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
[% else %]
_action: delete
[% endif %]Step 2 — Add to templates-order.yaml:
templates:
- templates-core/namespace.yaml
- templates-core/resource-quota.yaml
- templates-core/limit-range.yaml # ← new entry
- templates-network/network-policy.yaml
...- Add the field to
CRD.ymlunder the appropriatespecsection. - Set a
default:value if applicable (auto-extracted and applied by the operator). - Reference it in templates via
[{ spec.your_new_field }].
No Python code changes needed — the operator reads defaults directly from the CRD schema.
To add a new standard infrastructure service (e.g. LDAP):
- Add a boolean flag in
CRD.ymlunderspec.network.externalAccess.standard:
ldap:
type: boolean
default: false- In
templates-network/network-infrastructure-access.yaml, add the corresponding Cilium policy rules:
[% if spec.network.externalAccess.standard.ldap %]
- toEntities: []
toCIDRSet:
- cidr: "10.x.x.x/32" # LDAP server IP
toPorts:
- ports:
- port: "389"
protocol: TCP
[% endif %]For large sets of templates, create a new ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: templates-myfeature
namespace: {{ .Release.Namespace }}
data:
myresource.yaml: |
apiVersion: v1
kind: ConfigMap
metadata:
name: [{ namespace_name }]-myconfig
namespace: [{ namespace_name }]
[% if spec.myfeature.enabled %]
_action: apply
data:
key: value
[% else %]
_action: delete
[% endif %]Then register it in templates-order.yaml:
- templates-myfeature/myresource.yaml- Create a new ConfigMap
templates-ingress-mycontrollerwithrbac.yamlanddeployment.yaml. - Add the controller type to the CRD enum:
controller:
type:
enum:
- nginx
- haproxy
- traefik
- mycontroller # ← add here- Add entries to
vip-controller-templates-order.yaml:
- templates-ingress-mycontroller/rbac.yaml
- templates-ingress-mycontroller/deployment.yaml- Handle the new type in
templates-ingress/ingress-class.yaml:
[% elif spec.ingress.controller.type == 'mycontroller' %]
controller: mycontroller.io/ingress-controllerThe operator optionally integrates with Kyverno for admission control on ingress resources. These policies replace the previous Flask-based mutating webhook for ingress management.
auto-assign-ingressclass-securenamespace (MutatingPolicy)
Automatically sets spec.ingressClassName to the namespace name for any Ingress created in a managed namespace (labelled secure-ns.example.com/managed: "true"). This ensures ingress rules target the correct per-namespace controller.
validate-ingress-hostname-securenamespace (ValidatingPolicy)
Rejects Ingress resources whose hostname does not match the expected pattern *.{namespace}.{domain}. Enforces hostname consistency and prevents misconfiguration.
auto-set-ingress-tls-securenamespace (MutatingPolicy)
Automatically injects TLS configuration referencing the namespace's TLS secret when an ingress hostname matches the managed domain pattern.
securenamespace-validate (ValidatingPolicy)
Blocks creation of namespaces whose name matches the {solution}-{environment} pattern unless they are created by the operator (i.e. the request comes from the secure-namespace-operator ServiceAccount). Prevents bypassing the operator.
securenamespace-mutate (MutatingPolicy)
Adds standard labels to managed namespaces:
secure-ns.example.com/managed: "true"secure-ns.example.com/securenamespace: {name}
Apply the dedicated RBAC to allow Kyverno to manage ingress resources in managed namespaces:
kubectl apply -f kyverno-rbac.yamlkubectl logs -n namespace-operator deployment/secure-namespace-operator -f# Replace 'myapp-dev' with your SecureNamespace name
kubectl logs -n namespace-operator -l app=myapp-dev-vip-controller -f# All resources
kubectl get all -n myapp-dev
# Network policies
kubectl get ciliumnetworkpolicies -n myapp-dev
# Ingress class
kubectl get ingressclass myapp-dev
# Resource quotas
kubectl get resourcequota -n myapp-dev# Human-readable format (last 20 revisions)
kubectl get configmap myapp-dev-history -n namespace-operator \
-o jsonpath='{.data.history\.txt}'
# JSON format (full history, up to 100 entries)
kubectl get configmap myapp-dev-history -n namespace-operator \
-o jsonpath='{.data.history\.json}' | jqThe operator reconciles automatically on spec changes. To force immediate drift correction, trigger an annotation update:
kubectl annotate securenamespace myapp-dev force-reconcile=$(date +%s) --overwriteTemplate not applied: Check the operator logs for Template not found warnings. Verify the template key in templates-order.yaml matches the ConfigMap name and data key exactly.
CRD defaults not applied: The operator extracts defaults from the live CRD schema at startup. After modifying the CRD, restart the operator pod.
VIP not assigned: Verify that:
spec.egressController.enabled: true- At least one node has the label matching
spec.ingress.controller.nodeSelector - The LoadBalancer service has received an external IP
Namespace not created: Check that the solution and environment fields are set and environment is one of the allowed enum values.
apiVersion: secure-ns.example.com/v1alpha1
kind: SecureNamespace
metadata:
name: myapp-dev
spec:
project:
solution: myapp
environment: DEV
contact:
name: "Jane Doe"
email: "jane@example.com"apiVersion: secure-ns.example.com/v1alpha1
kind: SecureNamespace
metadata:
name: myapp-dev
spec:
project:
solution: myapp
environment: DEV
ingress:
enabled: true
domain: "k8s-dev.example.com"
controller:
type: traefik
replicas: 1
example:
enabled: true
hostPrefix: demo
# Access: https://demo.dev.myapp.k8s-dev.example.com
network:
isolationEnabled: true
externalAccess:
standard:
logging: true
proxy: trueapiVersion: secure-ns.example.com/v1alpha1
kind: SecureNamespace
metadata:
name: webapp-prod
spec:
project:
solution: webapp
environment: PROD
contact:
name: "Platform Team"
email: "platform@example.com"
quotas:
enabled: true
compute:
requests.cpu: "4000m"
requests.memory: "8Gi"
limits.cpu: "8000m"
limits.memory: "16Gi"
storage:
requests.storage: "100Gi"
persistentvolumeclaims: 10
objects:
pods: 30
services: 15
configmaps: 20
secrets: 20
ingress:
enabled: true
domain: "k8s.production.example.com"
controller:
type: nginx
replicas: 3
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1Gi"
network:
interfaceName: "eth0"
isolationEnabled: true
externalAccess:
enabled: true
rules:
- name: "Partner API"
fqdns:
- "api.partner.com"
ports:
- port: 443
protocol: TCP
- name: "On-prem Database"
cidrs:
- "10.50.0.0/24"
ports:
- port: 5432
protocol: TCP
services:
- name: redis-prod
namespace: cache-infra
ports:
- port: 6379
protocol: TCP
namespaces:
- name: monitoring-system
standard:
logging: true
vault: true
egressController:
enabled: true
reconcileIntervalSeconds: 30
serviceaccount:
enabled: true
name: webapp-sa
roles:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
clusterroles:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]MIT — see LICENSE.
- Understand the split: generic engine (
CHARTS/templates/core/) vs. thesecure-namespaceimplementation (CHARTS/templates/implementations/secure-namespace/) — see Architecture above anddocumentation/HOWTO-NEW-IMPLEMENTATION.mdif you're adding a new implementation rather than changing this one. - Branches:
feature/<name>,bugfix/<name>,hotfix/<name>. - Commits:
type(scope): message— types:feat,fix,docs,refactor,test,chore. - Before opening a PR:
helm lint ./CHARTSandhelm template ./CHARTSmust pass; if you touchedvalues.yaml::crd-related code, re-render with a non-defaultcrd.group/.version/.kind/.plural(--set) to confirm nothing intemplates/core/or the RBAC/Kyverno files still hardcodes thesecure-namespacedefaults. - Update docs alongside behavior changes: this README,
CHARTS/README.md,documentation/README.md(CRD field reference), anddocumentation/MAINTENANCE.md(internals/troubleshooting). - Tags:
vX.Y.Z(SemVer) —.github/workflows/release.ymlpackages the chart and publishes it tooci://ghcr.io/ccoupel/charts/secure-namespace-operatorautomatically on tag push.
Open an issue on github.com/CCoupel/Generic-Operator/issues.
For operational troubleshooting of the secure-namespace implementation (reconciliation stuck, Kyverno
not mutating, egress VIP not working, quota errors...), check
documentation/MAINTENANCE.md#troubleshooting first.