@@ -207,6 +207,44 @@ def _delete_if_exists(fn, *args, logger=None):
207207 logger .warning (f"Failed to delete { args } : { exc } " )
208208
209209
210+ def _ingress_class_name (crd_name ):
211+ return f"tailscale-{ crd_name } "
212+
213+
214+ def _ensure_ingress_class (networking_api , crd_name , labels , logger ):
215+ """Create per-org IngressClass so the operator only watches its own ingresses."""
216+ ic_name = _ingress_class_name (crd_name )
217+ body = kubernetes .client .V1IngressClass (
218+ metadata = kubernetes .client .V1ObjectMeta (
219+ name = ic_name ,
220+ labels = labels ,
221+ ),
222+ spec = kubernetes .client .V1IngressClassSpec (
223+ controller = "tailscale.com/ts-ingress" ,
224+ ),
225+ )
226+ try :
227+ networking_api .read_ingress_class (ic_name )
228+ logger .info (f"IngressClass { ic_name } already exists" )
229+ except ApiException as exc :
230+ if exc .status == 404 :
231+ networking_api .create_ingress_class (body )
232+ logger .info (f"Created IngressClass { ic_name } " )
233+ else :
234+ raise
235+
236+
237+ def _delete_ingress_class (networking_api , crd_name , logger ):
238+ """Delete per-org IngressClass."""
239+ ic_name = _ingress_class_name (crd_name )
240+ try :
241+ networking_api .delete_ingress_class (ic_name )
242+ logger .info (f"Deleted IngressClass { ic_name } " )
243+ except ApiException as exc :
244+ if exc .status != 404 :
245+ logger .warning (f"Failed to delete IngressClass { ic_name } : { exc } " )
246+
247+
210248def _ensure_proxy_group (custom_api , crd_name , labels , default_tags , logger ):
211249 """Create the ProxyGroup so ingresses appear as Tailscale Services."""
212250 pg_name = _proxy_group_name (crd_name )
@@ -260,91 +298,10 @@ def _delete_proxy_group(custom_api, crd_name, logger):
260298
261299
262300# ---------------------------------------------------------------------------
263- # Operator role rules (what the Tailscale operator needs within its namespace)
301+ # RBAC rules for proxy pods ( namespace-scoped )
264302# ---------------------------------------------------------------------------
265303
266304
267- def _operator_role_rules ():
268- return [
269- kubernetes .client .V1PolicyRule (
270- api_groups = ["" ],
271- resources = ["secrets" , "serviceaccounts" , "configmaps" , "events" ],
272- verbs = [
273- "get" ,
274- "list" ,
275- "watch" ,
276- "create" ,
277- "update" ,
278- "patch" ,
279- "delete" ,
280- "deletecollection" ,
281- ],
282- ),
283- kubernetes .client .V1PolicyRule (
284- api_groups = ["" ],
285- resources = ["services" , "services/status" ],
286- verbs = [
287- "get" ,
288- "list" ,
289- "watch" ,
290- "create" ,
291- "update" ,
292- "patch" ,
293- "delete" ,
294- "deletecollection" ,
295- ],
296- ),
297- kubernetes .client .V1PolicyRule (
298- api_groups = ["" ],
299- resources = ["pods" ],
300- verbs = ["get" , "list" , "watch" ],
301- ),
302- kubernetes .client .V1PolicyRule (
303- api_groups = ["apps" ],
304- resources = ["statefulsets" , "deployments" ],
305- verbs = [
306- "get" ,
307- "list" ,
308- "watch" ,
309- "create" ,
310- "update" ,
311- "patch" ,
312- "delete" ,
313- "deletecollection" ,
314- ],
315- ),
316- kubernetes .client .V1PolicyRule (
317- api_groups = ["networking.k8s.io" ],
318- resources = ["ingresses" , "ingresses/status" ],
319- verbs = ["get" , "list" , "watch" , "update" , "patch" ],
320- ),
321- kubernetes .client .V1PolicyRule (
322- api_groups = ["tailscale.com" ],
323- resources = ["*" ],
324- verbs = ["get" , "list" , "watch" , "create" , "update" , "patch" , "delete" ],
325- ),
326- kubernetes .client .V1PolicyRule (
327- api_groups = ["coordination.k8s.io" ],
328- resources = ["leases" ],
329- verbs = ["get" , "list" , "watch" , "create" , "update" , "patch" ],
330- ),
331- kubernetes .client .V1PolicyRule (
332- api_groups = ["rbac.authorization.k8s.io" ],
333- resources = ["roles" , "rolebindings" ],
334- verbs = [
335- "get" ,
336- "list" ,
337- "watch" ,
338- "create" ,
339- "update" ,
340- "patch" ,
341- "delete" ,
342- "deletecollection" ,
343- ],
344- ),
345- ]
346-
347-
348305def _proxies_role_rules ():
349306 return [
350307 kubernetes .client .V1PolicyRule (
@@ -401,26 +358,26 @@ def reconcile_operator(spec, name, namespace, memo, logger, retry, **kwargs):
401358 else :
402359 raise
403360
404- # 2. ServiceAccounts
361+ # 2. Per-org IngressClass
362+ networking_api = kubernetes .client .NetworkingV1Api ()
363+ _ensure_ingress_class (networking_api , name , labels , logger )
364+
365+ # 3. ServiceAccounts
405366 _ensure_service_account (core_api , namespace , operator_sa , labels )
406367 _ensure_service_account (core_api , namespace , "proxies" , labels )
407368 logger .info ("Ensured ServiceAccounts" )
408369
409- # 3. Operator Role + RoleBinding
410- _ensure_role (rbac_api , namespace , operator_sa , _operator_role_rules (), labels )
411- _ensure_role_binding (
412- rbac_api , namespace , operator_sa , operator_sa , operator_sa , labels
413- )
414- logger .info ("Ensured operator RBAC" )
370+ # 3. ClusterRoleBinding subject (grants operator cluster-wide access
371+ # via the pre-provisioned tailscale-operator ClusterRole — no
372+ # redundant namespace Role needed)
373+ _ensure_cluster_role_binding_subject (rbac_api , namespace , operator_sa , logger )
415374
416- # 4. Proxies Role + RoleBinding
375+ # 4. Proxies Role + RoleBinding (proxies don't have a ClusterRole,
376+ # they only need namespace-scoped secret access for their state)
417377 _ensure_role (rbac_api , namespace , "proxies" , _proxies_role_rules (), labels )
418378 _ensure_role_binding (rbac_api , namespace , "proxies" , "proxies" , "proxies" , labels )
419379 logger .info ("Ensured proxies RBAC" )
420380
421- # 5. ClusterRoleBinding subject
422- _ensure_cluster_role_binding_subject (rbac_api , namespace , operator_sa , logger )
423-
424381 # 6. Operator Deployment (WIF mode — JWT from cabotage OIDC issuer)
425382 deploy_name = operator_sa
426383 env_vars = [
@@ -433,6 +390,10 @@ def reconcile_operator(spec, name, namespace, memo, logger, retry, **kwargs):
433390 ),
434391 # CLIENT_ID (not CLIENT_ID_FILE) triggers WIF mode in the operator
435392 kubernetes .client .V1EnvVar (name = "CLIENT_ID" , value = client_id ),
393+ # Per-org IngressClass so this operator only watches its own ingresses
394+ kubernetes .client .V1EnvVar (
395+ name = "OPERATOR_INGRESS_CLASS_NAME" , value = _ingress_class_name (name )
396+ ),
436397 ]
437398 if default_tags :
438399 env_vars .append (
@@ -516,16 +477,12 @@ def delete_operator(spec, name, namespace, logger, **kwargs):
516477 custom_api = kubernetes .client .CustomObjectsApi ()
517478
518479 # Delete in reverse order
480+ networking_api = kubernetes .client .NetworkingV1Api ()
481+ _delete_ingress_class (networking_api , name , logger )
519482 _delete_proxy_group (custom_api , name , logger )
520483 _delete_if_exists (
521484 apps_api .delete_namespaced_deployment , operator_sa , namespace , logger = logger
522485 )
523- _delete_if_exists (
524- rbac_api .delete_namespaced_role_binding , operator_sa , namespace , logger = logger
525- )
526- _delete_if_exists (
527- rbac_api .delete_namespaced_role , operator_sa , namespace , logger = logger
528- )
529486 _delete_if_exists (
530487 rbac_api .delete_namespaced_role_binding , "proxies" , namespace , logger = logger
531488 )
0 commit comments