Access Control: AppPlatformRegistration RBAC

Overview

The AppPlatformRegistration controller automatically enforces access control based on the spec.access[] field. When an AppPlatformRegistration is reconciled, the controller:

  1. Creates the platform repo (<app>-platform) with deploy key and Flux config
  2. Discovers namespaces with label labrats.work/app=<app> (managed by the application team)
  3. Creates RBAC (Roles + RoleBindings) in every labeled namespace based on access entries

Namespaces are created and deleted through the management interface (dashboard API) — the controller only discovers them via the labrats.work/app label and creates RBAC for resources within those namespaces. Users never get direct K8s RBAC for namespace operations.

How It Works

AppPlatformRegistration: my-diet
  spec.access:
    - group: mydiet-ops        role: admin
    - group: mydiet-developer  role: developer
    - group: mydiet-viewer     role: reader

  Manages:
    GitHub repo: my-diet-platform
    Deploy key:  my-diet-platform-deploy-key (in app-platform ns)

   ┌───────────────────┐   ┌───────────────────┐
   │  my-diet-dev      │   │  my-diet-prod     │
   │  (labrats.work/   │   │  (labrats.work/   │
   │   app=my-diet)    │   │   app=my-diet)    │
   │                   │   │                   │
   │  Roles:           │   │  Roles:           │
   │  app-platform:    │   │  app-platform:    │
   │    admin          │   │    admin          │
   │    developer      │   │    developer      │
   │    reader         │   │    reader         │
   │                   │   │                   │
   │  RoleBindings:    │   │  RoleBindings:    │
   │    admin →        │   │    admin →        │
   │      mydiet-ops   │   │      mydiet-ops   │
   │    developer →    │   │    developer →    │
   │      mydiet-      │   │      mydiet-      │
   │      developer    │   │      developer    │
   │    reader →       │   │    reader →       │
   │      mydiet-      │   │      mydiet-      │
   │      viewer       │   │      viewer       │
   └───────────────────┘   └───────────────────┘

AppPlatformRegistration Spec

apiVersion: labrats.work/v1alpha1
kind: AppPlatformRegistration
metadata:
  name: my-diet
spec:
  displayName: "My Diet"
  access:
    - group: mydiet-ops
      role: admin
    - group: mydiet-developer
      role: developer
    - group: mydiet-viewer
      role: reader

The group field maps to OIDC group names. The role field must be one of: admin, developer, reader.

Role Definitions

Each environment namespace gets up to three Roles, named app-platform:<level>.

admin

Full CRUD on resources within namespaces. Does not include namespace creation/deletion — that is handled exclusively by the management interface (dashboard API).

API GroupsResourcesVerbs
""pods, pods/log, pods/exec, pods/portforward, services, configmaps, secrets, events, persistentvolumeclaims, serviceaccounts, endpointsget, list, watch, create, update, patch, delete
appsdeployments, statefulsets, replicasets, daemonsetsget, list, watch, create, update, patch, delete
batchjobs, cronjobsget, list, watch, create, update, patch, delete
networking.k8s.ioingresses, networkpoliciesget, list, watch, create, update, patch, delete

developer

Read workloads, view logs, exec into pods, port-forward, and read secrets.

API GroupsResourcesVerbs
""podsget, list, watch
""pods/logget
""pods/exec, pods/portforwardcreate
""services, configmaps, events, persistentvolumeclaimsget, list, watch
""secretsget, list, watch
appsdeployments, statefulsets, replicasets, daemonsetsget, list, watch
batchjobs, cronjobsget, list, watch
networking.k8s.ioingressesget, list, watch

reader

Read-only access to workloads and logs. No secrets, no exec.

API GroupsResourcesVerbs
""podsget, list, watch
""pods/logget
""services, configmaps, events, persistentvolumeclaimsget, list, watch
appsdeployments, statefulsets, replicasets, daemonsetsget, list, watch
batchjobs, cronjobsget, list, watch
networking.k8s.ioingressesget, list, watch

Key Differences

Capabilityadmindeveloperreader
Read workloadsYesYesYes
View logsYesYesYes
Exec into podsYesYesNo
Port-forwardYesYesNo
Read secretsYesYesNo
Create/modify resourcesYesNoNo
Delete resourcesYesNoNo
Create/delete namespacesNo (via dashboard only)NoNo

Note: No role grants namespace-level verbs via K8s RBAC. Namespace creation and deletion is managed exclusively by the dashboard API, which uses the operator's ServiceAccount (ClusterRole) to create namespaces with the correct labels and prefix enforcement. Users only have access to resources *within* their namespaces.

Lifecycle

  • Namespace discovery: The controller finds namespaces via the labrats.work/app label. Namespaces are created/deleted through the management interface (dashboard API). New namespaces are picked up on the next reconciliation (every 5 minutes).
  • Cleanup: If a role level has no groups assigned in spec.access[], its Role and RoleBinding are deleted from all labeled namespaces.
  • Labels: All managed Roles and RoleBindings are labeled with labrats.work/app: {appName} and app.kubernetes.io/managed-by: app-platform.
  • No owner references: AppPlatformRegistration is cluster-scoped while Roles are namespace-scoped — cross-scope ownership is not supported in Kubernetes. Labels are used for discovery instead.
  • No finalizer needed: RBAC resources are garbage collected when namespaces are deleted.

Multiple Groups Per Role

Multiple groups can share the same role level:

spec:
  access:
    - group: ops-team
      role: admin
    - group: platform-team
      role: admin
    - group: dev-team
      role: developer

This creates a single app-platform:admin RoleBinding with two Group subjects (ops-team and platform-team).

Platform Repository

The AppPlatformRegistration controller automatically creates a <app>-platform GitHub repository for each registered app. This repo stores Flux configuration that enables Flux to sync from the app's source repos.

The controller also provisions per-platform AGE encryption:

  1. AGE keypair — A unique X25519 keypair is generated and stored as K8s Secret <app>-platform-age-key in the operator namespace
  2. Flux repo config — SOPS-encrypted copies of both the deploy key and AGE key are committed to apps/<app>-platform/ in the Flux repo (encrypted with the cluster AGE key)
  3. .sops.yaml — Auto-committed to the root of the <app>-platform repo, configuring SOPS to use the platform's AGE public key

This tiered encryption design limits blast radius — each platform's AGE key can only decrypt that platform's secrets. If the cluster AGE key were shared into app-accessible namespaces, any app team could decrypt all SOPS-encrypted secrets across the entire cluster.

The AGE public key is stored in status.agePublicKey for reference.

Kyverno Resource Protection

Platform-managed resources (secrets, RBAC) are labeled app.kubernetes.io/managed-by: app-platform. Kyverno policies in the platform repo enforce that only the platform operator's ServiceAccount can modify these resources.

This prevents application users — even those with admin role — from tampering with platform-managed secrets or RBAC bindings in their namespaces. See Case: my-diet for a concrete example of the Kyverno policy definition.