Architecture Overview
Understand how KubePanel's three-layer architecture separates business logic, infrastructure specification, and infrastructure management — and why that matters.
Overview
KubePanel is a Kubernetes-native web hosting control panel built on a strict separation of concerns. Three components handle three distinct responsibilities:
Design principle: Django handles business logic, the Domain CR is the contract between Django and the operator, and the Kopf operator owns all infrastructure. Secrets never touch the Django database — they are generated and stored only in Kubernetes.
Handles authentication, multi-tenancy, package quotas, domain ownership, email accounts, DNS, and audit logging. Talks to Kubernetes only to create/update/delete Domain CRs and read their status.
kubepanel.io/v1alpha1 Kind: DomainThe contract between Django and the operator. Specifies domain name, workload configuration, resource limits, email/DNS/backup settings, and receives status updates (SFTP port, DB credentials, phase, conditions).
Watches Domain CRs for changes and reconciles the full infrastructure: namespace, PVC, secrets (SFTP, DB, DKIM), ConfigMaps (nginx, app config), Deployment, Services, Ingress, and the central DKIM configuration. Self-heals drift every 5 minutes.
dom-example-comThe Three Layers
Django Dashboard — Business Logic
The Django application handles everything that requires business rules: who owns which domain, what resource limits apply, billing tiers, multi-tenancy, and audit trails. It communicates with Kubernetes exclusively through the Domain CR API.
| Responsibility | Why Here |
|---|---|
| User accounts & authentication | Sessions, 2FA, login rate limiting |
| Domain ownership | Multi-tenancy, authorization checks |
| Packages & quotas | Business rules, limit enforcement |
| WorkloadType & WorkloadVersion | Admin-configurable runtime options |
| Mail users & aliases | Business entities with FK relationships |
| Cloudflare API tokens | User-specific encrypted credentials |
| DNS zones & records | User-managed via UI, synced to Cloudflare |
| Audit logging | Business audit trail (every operation logged) |
Domain CR — The Contract
The Domain Custom Resource is the single source of truth for a domain's desired infrastructure state. Django writes to it; the operator reads from it. Status flows back the other way: the operator writes SFTP port, database credentials reference, phase, and conditions; Django reads them to display in the UI.
apiVersion: kubepanel.io/v1alpha1 kind: Domain metadata: name: example-com spec: domainName: example.com workload: type: php version: "8.2" image: docker.io/kubepanel/php82:v1.0 port: 9001 proxyMode: fastcgi resources: storage: 10Gi limits: cpu: "500m" memory: 512Mi email: enabled: true dkimSecretRef: dkim-example-com status: phase: Ready sftp: port: 30042 username: example-com database: host: mariadb.kubepanel.svc.cluster.local name: example_com
Kopf Operator — Infrastructure Manager
The operator is a Python application using the Kopf framework. It watches all Domain CRs cluster-wide and reconciles the full infrastructure stack for each domain. Key behaviors:
- On create: provisions the full stack — namespace, PVC, secrets, ConfigMaps, Deployment, Services, Ingress, MariaDB database, DKIM keys
- On update: patches only what changed (e.g. workload image, resource limits, nginx config)
- On delete: removes all Kubernetes resources, drops the MariaDB database, removes DKIM keys from central OpenDKIM
- On resume: operator restart — skips full reconciliation if the domain is already
Ready - Timer (every 5 min): reconciliation loop that corrects any drift from desired state
Request Flow
A typical HTTP request from a visitor to a KubePanel-hosted site:
Handles TLS, sets X-Forwarded-For, applies GlobalWAF rules, routes to the domain's ClusterIP Service.
Applies per-domain WAF rules, serves static files, proxies dynamic requests to the app container.
Kubernetes Resources Per Domain
Every domain gets its own namespace (dom-example-com) containing the following resources:
| Resource | Type | Purpose |
|---|---|---|
data | PersistentVolumeClaim | Website files storage (Linstor) |
sftp-credentials | Secret | SSH keypair, SFTP password, shadow hash |
db-credentials | Secret | MariaDB database name, user, password |
nginx-config | ConfigMap | Auto-generated nginx.conf with proxy mode |
app-config | ConfigMap | php.ini / gunicorn.conf / app settings |
{domain} | Deployment | App + nginx + SFTP containers |
{domain}-svc | Service (ClusterIP) | Web traffic from ingress |
{domain}-sftp | Service (NodePort) | External SFTP access |
{domain} | Ingress | TLS termination, routing from nginx-ingress |
backup | PVC | Backup storage (VolumeSnapshots + DB dumps) |
Storage — Linstor / DRBD
KubePanel uses Linstor (Piraeus Operator) with DRBD for replicated block storage. Each domain's data PVC is replicated across nodes in real time. If a node fails, the PVC is immediately available on a healthy node.
| Resource | StorageClass | Details |
|---|---|---|
| Domain data PVC | linstor-sc | Replicated, RWO, size configurable per domain |
| MariaDB PVC | linstor-sc | 5Gi, shared database instance |
| SMTP PVC | linstor-sc | 5Gi, Dovecot Maildir storage |
| VolumeSnapshots | piraeus-snapshots | Used for backup/restore |
Networking & Ingress
External HTTP/HTTPS traffic flows through nginx-ingress with ModSecurity WAF enabled. SFTP is exposed via NodePort services on the cluster's public node IPs.
nginx-ingress is configured with real_ip_header X-Forwarded-For and set_real_ip_from 10.0.0.0/8 to ensure real client IPs reach application logs and WAF rules.
| Traffic Type | Entry Point | Protocol |
|---|---|---|
| HTTP | nginx-ingress NodePort :80 | HTTP → redirected to HTTPS |
| HTTPS | nginx-ingress NodePort :443 | TLS terminated, proxied to ClusterIP |
| SFTP | Per-domain NodePort (30000–32767) | SSH/SFTP direct to pod |
| SMTP Submission | nginx-ingress TCP :587 | STARTTLS to Postfix |
| IMAPS | nginx-ingress TCP :993 | TLS to Dovecot |
| POP3S | nginx-ingress TCP :995 | TLS to Dovecot |
Email Stack
All email services run in the kubepanel namespace and serve all hosted domains from a single shared stack.
| Component | Role | Port |
|---|---|---|
| Postfix | MTA — SMTP submission + outbound delivery | 587, 465 |
| Dovecot | IMAP/POP3 — mailbox access | 993, 995 |
| OpenDKIM | DKIM signing milter (all domains) | 8891 (milter) |
| Rspamd | Spam / malware filtering milter | 11332 (milter) |
DKIM private keys for all domains are stored in the dkim-keys Secret. The operator updates this secret and restarts OpenDKIM whenever a domain is added, updated, or deleted. See Email Hosting for the full DKIM flow.
Database
A single shared MariaDB instance runs in the kubepanel namespace. Each hosted domain gets its own database and user with auto-generated credentials stored only in a Kubernetes Secret — never in Django's database.
Database credentials are generated by the operator on domain creation and stored exclusively in the db-credentials Secret in the domain's namespace. Django never sees the raw password.
Isolation Model
Each domain is fully isolated from every other domain at multiple layers:
| Layer | Mechanism | What It Prevents |
|---|---|---|
| Filesystem | Separate PVC per domain | Cross-domain file access |
| Process | Separate container per domain | Process-level interference |
| Network | Kubernetes namespace + network policies | Lateral network movement |
| Resources | CPU/memory limits per container | Noisy neighbor resource starvation |
| Database | Separate DB + user per domain | Cross-domain data access |
| Secrets | Secrets scoped to domain namespace | Credential leakage between domains |
Secrets Management
KubePanel never stores sensitive credentials in Django's database. All secrets are generated by the operator and stored in Kubernetes Secrets within the domain's namespace.
| Secret Name | Namespace | Contents |
|---|---|---|
sftp-credentials | dom-{domain} | SSH public/private key, SFTP password, shadow hash |
db-credentials | dom-{domain} | Database name, username, password |
dkim-{domain} | kubepanel | DKIM private key, public key, DNS TXT record |
dkim-keys | kubepanel | All domain private keys (used by OpenDKIM) |
mariadb-auth | kubepanel | MariaDB root credentials (operator reads only) |
Namespaces
| Namespace | Contains |
|---|---|
kubepanel | Dashboard, Operator, MariaDB, Postfix/Dovecot, OpenDKIM, Rspamd, shared secrets |
dom-{domain} | Per-domain: PVC, Secrets, ConfigMaps, Deployment, Services, Ingress |
ingress-nginx | nginx-ingress controller, ModSecurity WAF |
cert-manager | cert-manager, ClusterIssuer (letsencrypt-prod) |
piraeus-system | Linstor / Piraeus storage operator |
Custom Resource Definitions
| Kind | Group | Scope | Purpose |
|---|---|---|---|
Domain | kubepanel.io/v1alpha1 | Cluster | Per-domain infrastructure spec |
Backup | kubepanel.io/v1alpha1 | Namespaced | Backup job trigger and status |
Restore | kubepanel.io/v1alpha1 | Namespaced | Restore job trigger and status |
DNSZone | kubepanel.io/v1alpha1 | Cluster | Cloudflare DNS zone and records |
GlobalWAF | kubepanel.io/v1alpha1 | Cluster | Global ModSecurity rules |
DomainWAF | kubepanel.io/v1alpha1 | Namespaced | Per-domain WAF rules |
GlobalL3Firewall | kubepanel.io/v1alpha1 | Cluster | Calico network policies |
SMTPFirewall | kubepanel.io/v1alpha1 | Cluster | SMTP-level blocking rules |
License | kubepanel.io/v1alpha1 | Cluster | License key and tier enforcement |
Port Reference
| Port | Protocol | Service | Notes |
|---|---|---|---|
| 80 | TCP | nginx-ingress | HTTP — redirects to HTTPS |
| 443 | TCP | nginx-ingress | HTTPS — all hosted domains |
| 587 | TCP | Postfix | SMTP Submission (STARTTLS) |
| 465 | TCP | Postfix | SMTPS |
| 993 | TCP | Dovecot | IMAPS |
| 995 | TCP | Dovecot | POP3S |
| 30000–32767 | TCP | SFTP | NodePort per domain |
| 3306 | TCP | MariaDB | Internal cluster only |
| 8891 | TCP | OpenDKIM | Milter — internal only |
| 11332 | TCP | Rspamd | Milter — internal only |