Every service, every group, one matrix.
A small app that turns single sign-on, a gateway and Kubernetes into a managed identity platform. It finds every service, knows which ones the internet can reach, writes the access rules, ships them to git and audits every click — whether the click came from an admin, an app or an agent.
Counted live from the running database, the cluster and the source tree.
"Just one more service" is six chores
Single sign-on is the brain, the gateway is the door, Kubernetes is the house. The glue between them is configuration — hand-edited, unless something does it for you.
A cluster-aware access loop
One app reads the cluster, keeps a model of services × groups, writes the rules and reloads sign-on. Every step is real code.
- 1An admin opens the managerReachable only from admin networks, behind its own hardened login.
- 2It reads the clusterEvery few minutes it lists every gateway route, how each one is protected and whether the internet or only the LAN can reach it.
- 3It keeps one modelA small database of services, domains, groups and who may reach what.
- 4It writes the rules three placesThe live config, the git repository and an audit row — in one ordered action.
- 5It reloads sign-onA graceful reload in about three seconds, with a restart as fallback.
- 6Every service is protectedOne place to grant. One place to revoke. One audit row per click.
Six motions, step by step
The first three are most of an admin's day. The last three arrived in October 2026: an app enrols its own people, an agent tightens a door, a site goes public on purpose. Each is one click or one call; underneath is a precise chain of reads, writes and audit entries.
Scan → sync → review
- RList every route; capture every hostname it serves.
- RGroup routes into logical apps with friendly names.
- WShow a diff: new apps, new domains, stale ones.
- WApply it — new apps wait until an admin assigns groups.
- ALog the change; stay silent when nothing changed.
Apply access, in order
- RRead the whole matrix: 95 service × group entries.
- RBuild rules in first-match order and merge duplicates — 56 rules, 428 lines.
- WBack up the live file, then write the new one.
- WCommit and push the same file to git.
- AReload sign-on and record every result in one audit row.
Toggle sign-on per route
- RFetch the live route for a local service.
- WAdd the sign-on check if the namespace lacks it.
- WPatch the live route.
- WFind the matching manifest in git and change it too.
- AAudit it — "who protected that dashboard at 2 AM?" has a one-second answer.
An app enrols its own people
- RThe classroom calls the service API with its own token; the API knows which group prefix it owns.
- WCreate the student with an unguessable password nobody sees; the person sets their own through sign-on's reset mail.
- WPut a course group behind the library or the meeting server at password or second-factor level — only from an allowlist.
- WApply — the same ordered write as Flow 2.
- AAudited as the service and as the teacher it acted for. An admin, another service, a password: out of reach by construction.
An agent tightens a door
- RA local agent lists the services that are open and may be protected — public sites and machine endpoints are on a never-touch list.
- RDry run: what would change, for which groups.
- WProtect: add sign-on, grant existing groups, raise a policy — never open, never lower, never bypass.
- WApply, like an admin would.
- AAudited under the agent's own name. Loosening anything stays a human's click in the UI.
Public on purpose
- RInternet traffic enters on its own gateway entry point; a route is public exactly when it lists it.
- RTwo invariants every sync: an internal name on the internet is critical, a public site that is LAN-only is a warning.
- WPublish or make LAN-only: patch the live route and the manifest in git, comments kept.
- WRefuse, always, to publish an internal hostname.
- AAudit it; a message goes out only when the set of violations changes — today it is empty.
A running system, counted live
Pulled from the database, the cluster and the source tree every 15 minutes. No marketing numbers.
The cluster it manages
The identity model
How routes are protected
The audit trail
The codebase
Three decisions worth talking about
The details that don't make a README — each one a response to a real failure.
Same data, same bytes
A normal YAML dump reorders indentation and inserts aliases, so identical rules produce different files — and noisy diffs. A tiny custom dumper fixes it: diffs show only real change.
class StableDumper(yaml.SafeDumper): def increase_indent(self, flow=False, indentless=False): return super().increase_indent(flow, False) def ignore_aliases(self, _): return True
Atomic enough
True atomicity across a file, git and a database is impossible. Ordering the writes makes any single failure recoverable.
1. back up the live file 2. write the new file # live 3. copy to the repository 4. commit & push # may fail — OK 5. reload sign-on # ~3 s 6. write one audit row # records 1–5
Bypass has no subject
An early version gave "bypass" rules a group subject. Reasonable — and a config error the sign-on service refuses to start with. Now the generator simply never emits one.
if policy == "bypass": rule["policy"] = "bypass" # no subject else: rule["subject"] = [groups]
An access manager is a target
If it falls, everything behind it falls. It was hardened against an internal audit; every finding has a control.
The closest commercial equivalent is a hosted identity service plus a custom cluster operator plus a GitOps tool. This one knows every route in the cluster, writes byte-stable rules, and proves what changed in the same row that proves who changed it.— the design brief, written backwards from what was needed
Boring tools that compose well
Running single sign-on on your own cluster?
Happy to talk through the design, the trade-offs and the production lessons.
Built with Claude Code as a collaborator — the architecture, the bugs and the production lessons are mine. Proprietary software; not licensed for reproduction or derivative works.