|
| 1 | +# Cozystack Security Groups — Design |
| 2 | + |
| 3 | +**Status:** Implemented (v1alpha1, aggregated-API projection) |
| 4 | + |
| 5 | +## 1. Summary |
| 6 | + |
| 7 | +A **SecurityGroup** is a namespace-scoped, tenant-facing firewall object in the `sdn.cozystack.io/v1alpha1` API group. It is served by the Cozystack aggregated API server (`cozystack-api`) as a **1:1 projection of a single CiliumNetworkPolicy** in the same namespace. Tenants manage SecurityGroups; the API server translates each one, synchronously, into a CiliumNetworkPolicy on write and back on read. There is no separate controller and no extra reconcile loop. |
| 8 | + |
| 9 | +The product value is the **RBAC boundary plus a Cozystack-native surface**: tenants get full self-service control over their own network policy without being granted any access to the `cilium.io` API group, and without the platform exposing Cilium's full, version-coupled CRD surface to them. |
| 10 | + |
| 11 | +## 2. Goals / Non-Goals |
| 12 | + |
| 13 | +**Goals** |
| 14 | + |
| 15 | +- Let tenants declare allow-list ingress/egress for their own pods, scoped to their namespace. |
| 16 | +- Keep policy count equal to the number of SecurityGroups (1 SecurityGroup ↔ 1 CiliumNetworkPolicy). |
| 17 | +- Require no direct tenant access to `cilium.io` resources. |
| 18 | +- Avoid disrupting clusters or namespaces that do not create SecurityGroups. |
| 19 | + |
| 20 | +**Non-Goals** |
| 21 | + |
| 22 | +- We do not redesign tenant isolation; existing platform isolation policies remain and are untouched (they carry no SecurityGroup marker label, so they are invisible to this API). |
| 23 | +- We do not introduce an AWS-style attach/detach abstraction, reusable CIDR/FQDN groups, or per-attachment policy logic. Those are future work (§8). |
| 24 | + |
| 25 | +## 3. Architecture |
| 26 | + |
| 27 | +1. A tenant creates a **SecurityGroup** (`sdn.cozystack.io/v1alpha1`) with an `endpointSelector` and `ingress`/`egress` rules. |
| 28 | +2. The `cozystack-api` REST storage translates it into a **CiliumNetworkPolicy** of the same name in the same namespace, carrying the marker label `sdn.cozystack.io/securitygroup: "true"`. |
| 29 | +3. Reads (`get`/`list`/`watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view. |
| 30 | +4. The translation is synchronous and stateless — the aggregated API server owns it, so there is no controller, no status reconciliation, and no eventual-consistency window. |
| 31 | + |
| 32 | +### 3.1 Marker-label scoping |
| 33 | + |
| 34 | +Only CiliumNetworkPolicies labelled `sdn.cozystack.io/securitygroup: "true"` are visible through the SecurityGroup API. Policies created by other means (platform tenant-isolation policies, hand-written CiliumNetworkPolicies) are never surfaced and never mutated. `get`/`update`/`patch`/`delete` of an unmarked policy returns `NotFound`. The storage owns the marker label and always re-asserts it on every write (`create`, `update`, `patch`) — the marker is set after any tenant-supplied labels are merged, so a tenant cannot orphan an enforced policy by overwriting the marker through spec labels. |
| 35 | + |
| 36 | +### 3.2 Why an in-tree CiliumNetworkPolicy mirror |
| 37 | + |
| 38 | +`cozystack-api` does not import the `github.com/cilium/cilium` Go module: the current Cilium release pins a Kubernetes minor version newer than this project's `k8s.io/apimachinery` fork supports. Instead the storage uses a minimal in-tree mirror type (`CiliumNetworkPolicy` with a spec equal to `SecurityGroupSpec`) registered with the controller-runtime client at GroupVersion `cilium.io/v2`. Because the SecurityGroup field names and JSON tags match the CiliumNetworkPolicy CRD exactly, marshalling the mirror produces wire-compatible objects and the projection is a near-identity translation in both directions. |
| 39 | + |
| 40 | +## 4. API |
| 41 | + |
| 42 | +```yaml |
| 43 | +apiVersion: sdn.cozystack.io/v1alpha1 |
| 44 | +kind: SecurityGroup |
| 45 | +metadata: |
| 46 | + name: sg-db |
| 47 | + namespace: tenant-a |
| 48 | +spec: |
| 49 | + endpointSelector: |
| 50 | + matchLabels: |
| 51 | + app: db |
| 52 | + ingress: |
| 53 | + - fromEndpoints: |
| 54 | + - matchLabels: |
| 55 | + app: web |
| 56 | + toPorts: |
| 57 | + - ports: |
| 58 | + - port: "5432" |
| 59 | + protocol: TCP |
| 60 | + egress: |
| 61 | + - toFQDNs: |
| 62 | + - matchPattern: "*.apt.example.org" |
| 63 | + - toCIDR: |
| 64 | + - "10.0.0.0/8" |
| 65 | +``` |
| 66 | +
|
| 67 | +`SecurityGroupSpec` exposes the subset of the CiliumNetworkPolicy rule that the abstraction supports: `endpointSelector`, `ingress[]` (`fromEndpoints`, `fromCIDR`, `toPorts`) and `egress[]` (`toEndpoints`, `toCIDR`, `toFQDNs`, `toPorts`). An empty `ingress`/`egress` list with a set `endpointSelector` denies all traffic in that direction for the selected pods, matching Cilium semantics. |
| 68 | + |
| 69 | +## 5. Backing CiliumNetworkPolicy |
| 70 | + |
| 71 | +The SecurityGroup above projects to: |
| 72 | + |
| 73 | +```yaml |
| 74 | +apiVersion: cilium.io/v2 |
| 75 | +kind: CiliumNetworkPolicy |
| 76 | +metadata: |
| 77 | + name: sg-db |
| 78 | + namespace: tenant-a |
| 79 | + labels: |
| 80 | + sdn.cozystack.io/securitygroup: "true" |
| 81 | +spec: |
| 82 | + endpointSelector: |
| 83 | + matchLabels: |
| 84 | + app: db |
| 85 | + ingress: |
| 86 | + - fromEndpoints: |
| 87 | + - matchLabels: |
| 88 | + app: web |
| 89 | + toPorts: |
| 90 | + - ports: |
| 91 | + - port: "5432" |
| 92 | + protocol: TCP |
| 93 | + egress: |
| 94 | + - toFQDNs: |
| 95 | + - matchPattern: "*.apt.example.org" |
| 96 | + - toCIDR: |
| 97 | + - "10.0.0.0/8" |
| 98 | +``` |
| 99 | + |
| 100 | +## 6. RBAC |
| 101 | + |
| 102 | +- **`cozystack-api` ServiceAccount** — full CRUD on `ciliumnetworkpolicies.cilium.io`, since the storage CRUDs these objects on behalf of tenants. |
| 103 | +- **Tenants** — full access to `securitygroups.sdn.cozystack.io` in the base tenant role and read access in the view role, aggregated the same way as `apps.cozystack.io` resources. Tenants never receive any `cilium.io` permission. |
| 104 | + |
| 105 | +## 7. Safety & Interactions |
| 106 | + |
| 107 | +- Policies only apply to pods matched by the SecurityGroup's `endpointSelector`; other pods are unaffected. |
| 108 | +- CiliumNetworkPolicy is namespaced, so a SecurityGroup can only affect its own namespace and cannot grant cross-tenant access (network policies are allow-lists for the selected pods' own traffic). |
| 109 | +- The marker label keeps SecurityGroups and platform-managed policies strictly separate. |
| 110 | + |
| 111 | +## 8. Future Work |
| 112 | + |
| 113 | +The aggregated-API model is intentionally minimal. The following can be layered on without changing the core projection: |
| 114 | + |
| 115 | +- **Cluster-scope** SecurityGroups projecting to CiliumClusterwideNetworkPolicy. |
| 116 | +- An **attachment abstraction** (attach/detach a SecurityGroup to a managed application by reference, with controller-driven pod labelling) for AWS-like ergonomics. |
| 117 | +- Reusable **CIDR/FQDN groups** and references between SecurityGroups. |
| 118 | +- Exposing more of the CiliumNetworkPolicy rule surface (ICMP, `toServices`, `fromRequires`, `fromCIDRSet`/`toCIDRSet` exceptions) as demand appears. |
| 119 | +- **Server-side spec validation** of CIDR ranges, port values and FQDN patterns. Today the storage performs no validation. Only structural CRD-schema violations are caught synchronously when the backing CiliumNetworkPolicy is written (and surface to the caller as a Cilium-level error); a rule that is schema-valid but semantically wrong (a malformed CIDR, an out-of-range port) is accepted, the create/update returns success, and Cilium rejects it asynchronously in the agent — so the policy silently never enforces. For a low-skill-operator platform this is a sharp edge worth closing with SecurityGroup-level validation. |
0 commit comments