{
  "version": 1,
  "id": "k8s-namespaces-and-rbac",
  "title": "Namespaces, RBAC and Resource Quotas",
  "summary": "Isolation with namespaces, least-privilege Roles and bindings, ResourceQuota and LimitRange.",
  "estimatedMinutes": 20,
  "prerequisites": [],
  "source": "courses-notebook SYS401_kubernetes_devops 04_namespaces_rbac",
  "quiz": [
    {
      "prompt": "In Kubernetes RBAC, what happens if no Role or ClusterRoleBinding grants a verb on a resource for a user?",
      "options": [
        "Access is allowed by default",
        "Access is denied - Kubernetes RBAC is allow-only, with no explicit deny rules",
        "An explicit Deny rule is required to block access",
        "Access depends on the namespace label"
      ],
      "answer": 1
    },
    {
      "prompt": "A pod spec sets `limits.cpu: 500m` but no `requests.cpu`. What does Kubernetes do?",
      "options": [
        "Rejects the pod spec at admission",
        "Sets `requests.cpu` equal to `limits.cpu` (500m), which can cause inefficient scheduling and prevents bursting into unused capacity",
        "Schedules the pod with `requests.cpu = 0`",
        "Treats the limit as advisory and ignores it"
      ],
      "answer": 1
    }
  ],
  "lesson": "Kubernetes **namespaces** provide organizational and resource-quota isolation between teams, environments, or tenants sharing a cluster. **RBAC** (Role-Based Access Control) enforces least-privilege access: who can do what to which resources in which namespaces. Together they form the foundation of multi-tenant Kubernetes security.\n\n**Learning objectives**\n- Model the RBAC engine: PolicyRule, Role, ClusterRole, RoleBinding\n- Implement the RBAC access check algorithm (allow-only, no deny rules)\n- Model ResourceQuota enforcement to prevent noisy-neighbor problems\n- Simulate a multi-tenant cluster with team isolation\n\n## Core Concepts\n\n| Concept | Definition | Scope |\n|---|---|---|\n| Namespace | Virtual cluster partition \u2014 isolates names + quotas | Cluster |\n| ResourceQuota | Caps total resource usage within a namespace | Namespace |\n| LimitRange | Defaults + min/max for individual Pods/Containers | Namespace |\n| Role | Set of allowed API operations | Namespace-scoped |\n| ClusterRole | Set of allowed API operations | Cluster-wide |\n| RoleBinding | Binds a Role to subjects | Namespace-scoped |\n| ClusterRoleBinding | Binds a ClusterRole to subjects | Cluster-wide |\n| ServiceAccount | Identity for Pods to call the Kubernetes API | Namespace |\n| Subjects | Users, Groups, or ServiceAccounts in bindings | \u2014 |\n| Least privilege | Grant only minimum required permissions | Security principle |\n\n```python\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Set, Optional, Tuple\nfrom enum import Enum\n\n# ---------------------------------------------------------------------------\n# RBAC Data Model\n# ---------------------------------------------------------------------------\n\nclass Verb(Enum):\n    GET    = 'get'\n    LIST   = 'list'\n    WATCH  = 'watch'\n    CREATE = 'create'\n    UPDATE = 'update'\n    PATCH  = 'patch'\n    DELETE = 'delete'\n\nREAD_VERBS  = {Verb.GET, Verb.LIST, Verb.WATCH}\nWRITE_VERBS = {Verb.CREATE, Verb.UPDATE, Verb.PATCH, Verb.DELETE}\nALL_VERBS   = set(Verb)\n\n\n@dataclass\nclass PolicyRule:\n    api_groups: List[str]   # '' = core API; 'apps', 'batch', etc.\n    resources:  List[str]   # 'pods', 'deployments', '*' for all\n    verbs:      Set[Verb]\n\n    def allows(self, api_group: str, resource: str, verb: Verb) -> bool:\n        group_match    = '*' in self.api_groups or api_group in self.api_groups\n        resource_match = '*' in self.resources  or resource  in self.resources\n        return group_match and resource_match and verb in self.verbs\n\n\n@dataclass\nclass Role:\n    name:      str\n    namespace: str\n    rules:     List[PolicyRule]\n\n\n@dataclass\nclass ClusterRole:\n    name:  str\n    rules: List[PolicyRule]\n\n\n@dataclass\nclass RoleBinding:\n    name:      str\n    namespace: str\n    role_ref:  str           # name of Role or ClusterRole\n    is_cluster_role: bool    # True \u2192 role_ref is a ClusterRole\n    subjects:  List[Dict]    # [{'kind': 'User', 'name': 'alice'}]\n\n\n@dataclass\nclass ClusterRoleBinding:\n    name:        str\n    role_ref:    str\n    subjects:    List[Dict]\n\n\n@dataclass\nclass RBACEngine:\n    \"\"\"\n    Implements Kubernetes RBAC: allow-only, no explicit deny.\n    Access = ALLOWED iff at least one rule in a bound role grants it.\n    \"\"\"\n    roles:                Dict[str, Role]              = field(default_factory=dict)\n    cluster_roles:        Dict[str, ClusterRole]       = field(default_factory=dict)\n    role_bindings:        List[RoleBinding]            = field(default_factory=list)\n    cluster_role_bindings: List[ClusterRoleBinding]    = field(default_factory=list)\n\n    def _subject_match(self, subjects: List[Dict], username: str) -> bool:\n        return any(s.get('name') == username for s in subjects)\n\n    def can(self, username: str, namespace: str, api_group: str, resource: str, verb: Verb) -> bool:\n        # 1. Check namespace-scoped RoleBindings\n        for rb in self.role_bindings:\n            if rb.namespace != namespace:\n                continue\n            if not self._subject_match(rb.subjects, username):\n                continue\n            if rb.is_cluster_role:\n                role_rules = self.cluster_roles.get(rb.role_ref, ClusterRole('', [])).rules\n            else:\n                key = f'{namespace}/{rb.role_ref}'\n                role_rules = self.roles.get(key, Role('', '', [])).rules\n            if any(r.allows(api_group, resource, verb) for r in role_rules):\n                return True\n\n        # 2. Check cluster-wide ClusterRoleBindings\n        for crb in self.cluster_role_bindings:\n            if not self._subject_match(crb.subjects, username):\n                continue\n            cr = self.cluster_roles.get(crb.role_ref, ClusterRole('', []))\n            if any(r.allows(api_group, resource, verb) for r in cr.rules):\n                return True\n        return False\n\n    def audit_user(self, username: str, namespace: str) -> None:\n        checks: List[Tuple[str, str, Verb]] = [\n            ('',     'pods',          Verb.GET),\n            ('',     'pods',          Verb.CREATE),\n            ('',     'pods',          Verb.DELETE),\n            ('apps', 'deployments',   Verb.GET),\n            ('apps', 'deployments',   Verb.UPDATE),\n            ('',     'secrets',       Verb.GET),\n            ('',     'configmaps',    Verb.GET),\n            ('',     'namespaces',    Verb.CREATE),\n            ('rbac.authorization.k8s.io', 'roles', Verb.CREATE),\n        ]\n        print(f'RBAC audit: {username} in namespace={namespace}')\n        for api_group, resource, verb in checks:\n            allowed = self.can(username, namespace, api_group, resource, verb)\n            marker  = 'ALLOW' if allowed else 'DENY '\n            print(f'  [{marker}] {verb.value:8} {resource:20} (apiGroup={api_group or \"core\"})')\n\n\n# ---------------------------------------------------------------------------\n# Build a realistic multi-team RBAC configuration\n# ---------------------------------------------------------------------------\nrbac = RBACEngine()\n\n# Developer role: read most things, manage pods, no secrets access\nrbac.roles['production/developer'] = Role('developer', 'production', [\n    PolicyRule(['', 'apps'], ['pods', 'deployments', 'replicasets', 'services', 'configmaps'],\n               READ_VERBS),\n    PolicyRule([''], ['pods'], {Verb.CREATE, Verb.DELETE}),\n])\n\n# Ops cluster role: full cluster access\nrbac.cluster_roles['ops-admin'] = ClusterRole('ops-admin', [\n    PolicyRule(['*'], ['*'], ALL_VERBS),\n])\n\n# Read-only viewer: cluster-wide\nrbac.cluster_roles['viewer'] = ClusterRole('viewer', [\n    PolicyRule(['*'], ['*'], READ_VERBS),\n])\n\n# Bindings\nrbac.role_bindings += [\n    RoleBinding('alice-dev',  'production', 'developer', False, [{'kind': 'User', 'name': 'alice'}]),\n    RoleBinding('carol-dev',  'staging',    'developer', False, [{'kind': 'User', 'name': 'carol'}]),\n    # Bob uses ClusterRole bound to a namespace via RoleBinding\n    RoleBinding('bob-viewer', 'production', 'viewer',    True,  [{'kind': 'User', 'name': 'bob'}]),\n]\nrbac.cluster_role_bindings += [\n    ClusterRoleBinding('sre-admin', 'ops-admin', [{'kind': 'User', 'name': 'sre'}]),\n]\n\n# Audit\nrbac.audit_user('alice', 'production')   # developer in production\nprint()\nrbac.audit_user('bob', 'production')     # viewer in production\nprint()\nrbac.audit_user('sre', 'production')     # cluster admin\n```\n\n## ResourceQuota and LimitRange Enforcement\n\n```python\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Optional, Tuple\n\n\n@dataclass\nclass ResourceQuota:\n    namespace: str\n    max_pods: int = 50\n    max_cpu_requests: float = 10.0   # CPU cores\n    max_memory_requests_gi: float = 20.0  # GiB\n    max_services: int = 20\n\n\n@dataclass\nclass NamespaceUsage:\n    namespace: str\n    current_pods: int = 0\n    current_cpu_requests: float = 0.0   # CPU cores\n    current_memory_gi: float = 0.0\n    current_services: int = 0\n\n    def can_schedule_pod(\n        self,\n        quota: ResourceQuota,\n        cpu_request_cores: float,\n        memory_request_gi: float,\n    ) -> Tuple[bool, str]:\n        if self.current_pods + 1 > quota.max_pods:\n            return False, f'Pod quota exceeded ({self.current_pods}/{quota.max_pods})'\n        if self.current_cpu_requests + cpu_request_cores > quota.max_cpu_requests:\n            return False, (f'CPU quota exceeded: would be '\n                           f'{self.current_cpu_requests + cpu_request_cores:.1f}'\n                           f'/{quota.max_cpu_requests} cores')\n        if self.current_memory_gi + memory_request_gi > quota.max_memory_requests_gi:\n            return False, (f'Memory quota exceeded: would be '\n                           f'{self.current_memory_gi + memory_request_gi:.1f}'\n                           f'/{quota.max_memory_requests_gi} GiB')\n        return True, 'Admitted'\n\n    def admit_pod(self, cpu_cores: float, memory_gi: float) -> None:\n        self.current_pods += 1\n        self.current_cpu_requests += cpu_cores\n        self.current_memory_gi += memory_gi\n\n    def utilization(self, quota: ResourceQuota) -> Dict[str, str]:\n        return {\n            'pods':   f'{self.current_pods}/{quota.max_pods}',\n            'cpu':    f'{self.current_cpu_requests:.1f}/{quota.max_cpu_requests} cores '\n                      f'({self.current_cpu_requests/quota.max_cpu_requests*100:.0f}%)',\n            'memory': f'{self.current_memory_gi:.1f}/{quota.max_memory_requests_gi} GiB '\n                      f'({self.current_memory_gi/quota.max_memory_requests_gi*100:.0f}%)',\n        }\n\n\n# Simulate two teams sharing a cluster with per-namespace quotas\nquotas = {\n    'team-ml':      ResourceQuota('team-ml',      max_pods=30, max_cpu_requests=16.0, max_memory_requests_gi=32.0),\n    'team-backend': ResourceQuota('team-backend',  max_pods=20, max_cpu_requests=8.0,  max_memory_requests_gi=16.0),\n}\nusage = {\n    'team-ml':      NamespaceUsage('team-ml'),\n    'team-backend': NamespaceUsage('team-backend'),\n}\n\n# Simulate pod scheduling requests\npod_requests = [\n    ('team-ml',      0.5,  1.0,  'ml-worker-01'),\n    ('team-ml',      0.5,  1.0,  'ml-worker-02'),\n    ('team-ml',      4.0,  8.0,  'ml-gpu-job-01'),      # large GPU job\n    ('team-backend', 0.2,  0.5,  'api-pod-01'),\n    ('team-backend', 0.2,  0.5,  'api-pod-02'),\n    ('team-ml',      8.0, 16.0,  'ml-gpu-job-02'),      # will exceed quota\n    ('team-backend', 8.0, 16.0,  'backend-large'),      # will exceed quota\n]\n\nprint('ResourceQuota enforcement simulation:')\nprint(f'{\"Namespace\":>15}  {\"Pod\":>18}  {\"CPU\":>6}  {\"Mem\":>6}  Result')\nprint('-' * 75)\nfor ns, cpu, mem, name in pod_requests:\n    allowed, reason = usage[ns].can_schedule_pod(quotas[ns], cpu, mem)\n    if allowed:\n        usage[ns].admit_pod(cpu, mem)\n    status = 'ADMITTED' if allowed else f'REJECTED: {reason}'\n    print(f'  {ns:>13}  {name:>18}  {cpu:>5.1f}c  {mem:>5.1f}G  {status}')\n\nprint()\nprint('Final namespace utilization:')\nfor ns, u in usage.items():\n    util = u.utilization(quotas[ns])\n    print(f'  {ns}:')\n    for k, v in util.items():\n        print(f'    {k:8}: {v}')\n```\n\n## Quick Quiz\n\n**Q1**: What is the difference between a Role and a ClusterRole, and when would you use a ClusterRoleBinding vs a RoleBinding?\n> **A**: Role is namespace-scoped (only grants permissions within one namespace). ClusterRole is cluster-wide. A RoleBinding can bind either a Role or ClusterRole to subjects, but the access is scoped to the RoleBinding's namespace. A ClusterRoleBinding binds a ClusterRole cluster-wide \u2014 use this for cluster-admin or metrics-server that needs to read resources in all namespaces.\n\n**Q2**: Why are namespaces not sufficient as a security boundary?\n> **A**: Namespace isolation is purely organizational \u2014 Pods can communicate across namespaces by default. Without NetworkPolicies, any pod can reach any other pod's IP in the cluster. RBAC only restricts Kubernetes API calls, not network traffic.\n\n**Q3**: What happens if you grant a ServiceAccount `verbs: [*]` on `resources: [roles, rolebindings]`?\n> **A**: The pod can create new Roles with any permissions and bind them to itself, effectively escalating to cluster-admin. This is a privilege escalation vulnerability. Kubernetes has safeguards (`bind` and `escalate` verbs) but only if `--enable-bootstrap-token-auth` RBAC settings are correct.\n\n## Key Takeaways\n\n- RBAC is allow-only: no matching rule = denied; no explicit deny needed\n- Role = namespace scope; ClusterRole = cluster scope; RoleBinding scopes either to a namespace\n- ServiceAccounts provide pod identity \u2014 grant them only the API calls their workload needs\n- ResourceQuotas prevent noisy-neighbor resource starvation between teams\n- Namespaces are organizational, not security boundaries \u2014 add NetworkPolicy for network isolation\n\n## Going Deeper\n\n1. **Exercise**: Add `impersonation` support to `RBACEngine` \u2014 `kubectl --as=alice` allows an admin to act as alice. Model the `impersonate` verb on `users` resource and enforce it.\n2. **Exercise**: Implement `NetworkPolicy` simulation \u2014 model allow/deny rules for ingress/egress and show which pods can communicate with which.\n3. **Exercise**: Audit for privilege escalation: scan all RoleBindings for any that grant `create/update` on `roles` or `rolebindings` to non-admin subjects.\n4. **Project**: Build a `kubectl auth can-i` simulator that takes `(user, namespace, verb, resource)` and returns allowed/denied with the matching rule path.\n5. **Research**: What is OPA (Open Policy Agent) Gatekeeper and how does it extend beyond RBAC? Compare to Kyverno \u2014 what policy use cases does admission control cover that RBAC cannot?\n\n## Hands-on Lab\n\nA runnable companion lab lives in `lab_04_namespaces_rbac/`. It provisions a local k3d cluster, three namespaces (`dev`, `staging`, `prod`), three `ServiceAccount`s, a `ClusterRole` (`pod-reader`), namespace-scoped `Role`s (`editor`, `admin`), and the matching `RoleBinding`s.\n\nWalk-through:\n\n```bash\ncd lab_04_namespaces_rbac\nmake up              # k3d cluster + apply manifests\nmake test-rbac       # auth can-i matrix: SA x verb x resource x namespace\nmake kubeconfig-dev  # mint a token kubeconfig for dev-readonly\nmake kubeconfig-prod # mint a token kubeconfig for prod-admin\nmake down\n```\n\nWhat you will demonstrate:\n\n- `dev-readonly` cannot delete pods (`kubectl auth can-i delete pods` -> no).\n- `prod-admin` can delete pods in `prod` but not in `dev` or `staging`.\n- A `ClusterRole` referenced from a `RoleBinding` is scoped to that namespace only (vs. a `ClusterRoleBinding`, which would grant it cluster-wide).\n- `scripts/exec-as.sh` uses the `TokenRequest` API to mint a short-lived bearer token and writes a self-contained kubeconfig so you can run commands as that ServiceAccount.\n\nThis makes the RBAC engine modeled above observable on a real API server.\n"
}
