Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ helm install krm-foyer oci://ghcr.io/configbutler/charts/krm-foyer --version <re
--set oidc.clientSecret.secretName=krm-foyer-oidc,tls.secretName=krm-foyer-tls
```

<div class="artifacthub-widget" data-url="https://artifacthub.io/packages/helm/krm-foyer/krm-foyer" data-theme="light" data-header="true" data-stars="true" data-responsive="false"><blockquote><p lang="en" dir="ltr"><b>krm-foyer</b>: Browser login, Kubernetes API access and live resources on one origin, with every access decision left to the API server</p>&mdash; Open in <a href="https://artifacthub.io/packages/helm/krm-foyer/krm-foyer">Artifact Hub</a></blockquote></div><script async src="https://artifacthub.io/artifacthub-widget.js"></script>

The repository comes with a devcontainer that has Go, Task, the linters CI runs, and
k3d/kubectl for testing against a real API server. See [CONTRIBUTING.md](CONTRIBUTING.md).

Expand Down
5 changes: 3 additions & 2 deletions charts/artifacthub-repo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
#
# Format: https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
#
# Once the repository is added in the Artifact Hub control panel, its repositoryID goes
# here to earn the Verified Publisher badge.
# repositoryID is the ID Artifact Hub gave the repository when it was added in its
# control panel; publishing it here earns the Verified Publisher badge.
repositoryID: 9a991dbf-e9ce-4b3e-ada7-2b8d399e7c10

# Claims repository ownership. The email must match an Artifact Hub account exactly.
owners:
Expand Down
250 changes: 250 additions & 0 deletions docs/implementer-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# Feedback for krm-foyer

Notes for krm-foyer's maintainers. They cover what Voter would still need before it
can delete its own login, session, proxy and stream code and run on krm-foyer instead.
It uses the same format as [krm-stream-feedback.md](krm-stream-feedback.md) and
[gitops-reverser-feedback.md](gitops-reverser-feedback.md): each entry says what Voter
needs, what krm-foyer does today, and whether we think krm-foyer should change. That
includes entries where we concluded it should not.

Against **krm-foyer 0.2.0** (`cfd290d`) and Voter at `51dbb1f`, 2026-10-05.
[k8s-front-adoption.md](k8s-front-adoption.md) is the older recommendation. It still
holds for the domain questions it raises; this page is about the transport.

---

## Summary

0.2.0 closed most of what Voter asked for. Sessions are kept in encrypted cookies.
Login parameters are configurable. `/auth/whoami` exists, the attribution extras are
in place, shared watches are guarded by SubjectAccessReviews, and the chart can be
installed. About 2,000 of Voter's 4,950 lines of Go are plumbing that krm-foyer
replaces as it stands.

What is still missing is not in the proxy itself. It is how krm-foyer meets the two
things Voter keeps:

1. **A domain backend on the same origin that needs to know who is calling.**
Voting and the coffee storefront have rules that RBAC cannot express. Today they
read the user from Voter's own session. Under krm-foyer they get nothing.
([entry 1](#1-identity-for-a-domain-backend-on-the-same-origin))
2. **The Room Pass QR login.** Nobody has run it end to end through krm-foyer. Running
it is the release evidence the audience-release plan itself asks for.
([entry 2](#2-prove-the-room-pass-qr-login-through-krm-foyer))

Below those come two smaller asks: routing recipes for one shared host
([entry 3](#3-routing-recipes-for-one-host-traefik-and-vite)), and an application
scope for the operator, who signs in as cluster-admin
([entry 4](#4-the-operator-signs-in-as-cluster-admin)).
[What Voter does not need](#what-voter-does-not-need) lists roadmap items that can wait
as far as Voter is concerned.

---

## What 0.2.0 already covers

| Voter today | krm-foyer 0.2.0 | Voter code that goes away |
| --- | --- | --- |
| OIDC with PKCE, state, nonce and a browser-binding cookie; logins in progress kept in memory | `/auth/login`, `/auth/callback`, logins in progress sealed in cookies | `voter/oidc.go:1-400` |
| `connector=` restricted by `OIDC_CONNECTOR_CHOICES` | `login.authorizationParameters.connector_id` with `allowedValues`, sent as `oidc.connector_id=` | `connectorFor` in `oidc.go` |
| Session sealed in `__Host-voter-session` with a 3,800-byte budget, ended by the earlier of the max age and the ID token's expiry | Same design, the same budget, rotatable keys | `participant_session.go`, `session_cookie.go` |
| `/auth/session` with `displayName`, `groups`, `connector`, CSRF | `sessionClaims.connector: /federated_claims/connector_id` | Most of `oidc_handlers.go` |
| `/auth/whoami` (SelfSubjectReview, rendered as YAML) | `/auth/whoami` (JSON) | `oidc_handlers.go:93-123` |
| Kubernetes clients built per request with the user's ID token, no impersonation | `/k8s` with the user's token, nothing to fall back to | `participant_kube.go`, `http_handlers.go:18-48` |
| `/public/stream`: shared watches as the ServiceAccount, a SubjectAccessReview per subscriber, rechecked every 30 s | `/stream/v1` with `sharedWatches`, an identity of its own and bounded rechecks | `stream_runtime.go`, `participant_stream.go` |
| `refusalRecorder`: one log line per 4xx | One log line per refusal, with the subject and who refused | `oidc_handlers.go:191-238` |
| Stream metrics on a separate listener | `-metrics-listen` | `streamMetrics` |
| Thin handlers: get and patch a CoffeeConfig, list, create and patch Databases, set a round's state | Native `/k8s` paths | Most of `participant_coffee.go`, `participant_databases.go` and `participant_save.go` |
| `/auth/rules` (a SelfSubjectRulesReview, flattened) | A POST of a SelfSubjectRulesReview through `/k8s`, which the design recommends to code | `participant_authz.go` |
| The operator page creates and deletes the `voter-audience-coffee-admin` RoleBinding with the operator's token | The same request through `/k8s` | `operator_audience_grant.go` |

`k8s.koudijs.dev/2-gitops/krm-foyer` already runs 0.2.0 with the hello example on
`foyer.k8s.koudijs.dev`. Two things that deployment worked out apply to Voter too:

- The `audience:server:client_id:kubernetes` cross-client scope, which makes Dex issue
a token the API server accepts.
- The NetworkPolicy values.

The hello example runs on its own host, though. Voter has to share one host with
Room Pass, and none of the open entries below has been tried there.

---

## 1. Identity for a domain backend on the same origin

**open · ask · `/auth/check` with identity headers**

Some of Voter is domain logic, and it stays in a backend:

- **Votes** (`participant_quiz.go:193-295`). The round must be `live`. Only the
`room-pass` connector may vote. The round's `generation` must match. Answers are
validated against the questions. The submission name is chosen by the server, so a
second vote is a 409.
- **The coffee storefront and orders** (`participant_storefront.go`,
`coffee_orders.go`). Prices and vouchers are computed on the server. Orders are not
Kubernetes objects at all.

Each of these handlers needs to know who is calling. Today they read it from Voter's own
session cookie. With krm-foyer the cookie is krm-foyer's, sealed with keys only
krm-foyer has, so a backend behind the same host learns nothing. The roadmap has this
under *"Later, when a hybrid application asks: identity headers from the check for a
domain backend, never the token"*. **Voter is that hybrid application, and it is asking.**

What we would need:

- `/auth/check` usable as a Traefik `ForwardAuth` target. On success it answers `204`
with identity headers: the **Kubernetes** username, UID, groups and extras from the
SelfSubjectReview, plus `connector` and `displayName` from the session claims. For a
`fetch` the answer is a `401` in the shape `/k8s` already uses. Never the token.
- Inbound copies of those headers stripped, with a test that a spoofed header changes
nothing. That is the same test `/k8s` already has for `Impersonate-*`.
- A bounded cost per check. A SelfSubjectReview on every request to a vote endpoint is
fine at Voter's scale, and the shared-watch lookup already reuses decisions.

The alternative is Voter changing its domain instead: votes go straight to `/k8s`, and
admission enforces the rules ([bff-choice.md](https://github.com/ConfigButler/krm-foyer/blob/main/docs/bff-choice.md#example-a-quiz-with-one-submission-per-participant),
options A to C). We have not chosen yet. A ValidatingAdmissionPolicy cannot read the
round to check that it is `live`, so the quiz would need a webhook. The storefront has
no such way out, because orders are not resources. The audience-release plan says
*"Coffee's backend authentication and authorization will be solved in its own
application"*. In practice that application is Voter's coffee storefront, so it needs
this entry too.

## 2. Prove the Room Pass QR login through krm-foyer

**open · ask · release evidence, not new krm-foyer code**

The audience-release plan says: *"Before switching voter to foyer, prove one real
QR-to-login journey there."* That has not happened. krm-foyer's fixture signs in with
Dex static users, and the deployed instance allows only `connector_id: github`.

To be clear about what we are **not** asking for: the join code should not go through
krm-foyer. Room Pass builds the join URL after Dex, so `oidc.*` has nothing to carry
it in. Room Pass's own contract
([qr-join.md](https://github.com/sunib/room-pass/blob/main/docs/qr-join.md)) is a
`Secure`, `HttpOnly`, `SameSite=Lax` cookie `__Host-room-pass-joincode`, set by the
application on the join host. Voter keeps that endpoint, about 85 lines
(`voter/oidc.go:401-484`). It sets the cookie and then redirects to
`/auth/login?return_to=…&oidc.connector_id=room-pass`. The audience-release plan
assigns it to the integration in the same way.

What we would like:

- Have the e2e fixture, or a documented recipe, run this flow against Room Pass's
[reference deployment](https://github.com/sunib/room-pass/tree/main/deploy) (Room
Pass, Dex `authproxy`, an edge), which Room Pass's CI now runs: QR → app endpoint
sets the cookie → krm-foyer login → Room Pass `/join` → Dex → `/auth/callback` →
`/auth/session` showing `connector: room-pass`.
- Then check the four other release items from the plan against that login: display
metadata, a write attributed through both extras, a pod replacement that keeps the
session, and logout.

The breakage we expect is the host split. The cookie arrives only when the app, Room
Pass's `/join` and krm-foyer share one host. If they don't, the flow quietly falls back
to typing the code. A browser test is the only thing that catches it. That leads to
entry 3.

## 3. Routing recipes for one host: Traefik and Vite

**open · ask · roadmap: "Routing recipes for one shared domain"**

Voter would serve four things from one host:

- krm-foyer: `/auth/*`, `/k8s/*`, `/stream/v1`, `/_foyer/*`
- Voter's static SPA: `/`
- Voter's domain backend: `/public/*`
- Room Pass: `/join`, `/bind`, …

The roadmap names Gateway API, nginx and Vite. This cluster routes with **Traefik
`IngressRoute`** (`voter-demo/ingress.yaml`). A Traefik recipe would match it directly.
It would also be where entry 1's `ForwardAuth` middleware and krm-foyer's own `/`
start page get sorted out, because `/` has to belong to the application.

The **Vite dev-server proxy** matters more than it looks. Voter's local loop is Vite
plus a mock backend (`frontend/dev/kube-mock-plugin.ts`). Under krm-foyer that mock
either goes or becomes a mock of krm-foyer. A recipe for pointing Vite at `task demo`'s
krm-foyer would settle which one. Cookies and CSRF through a dev proxy are where we
expect to lose an afternoon.

## 4. The operator signs in as cluster-admin

**open question · [application scope](https://github.com/ConfigButler/krm-foyer/blob/main/docs/application-scope.md)**

The operator logs in through GitHub as `github:simonkoudijs@gmail.com`, and that user
is bound to `cluster-admin` (`auth/rbac/humans-rbac.yaml`). Today Voter's backend uses
that token in a few specific handlers. Under krm-foyer, any script on Voter's origin
holds cluster-admin for as long as the operator is signed in. That is the case the
application-scope page lists: *"An administrator signing in to a small application"*.

Voter can work around this without krm-foyer: give the operator a second, narrow
identity just for the demo. The roadmap's first step under application scope is
*"document a browser identity in the cluster's authentication config, proved by one
e2e spec"*. A Dex recipe for that would answer it: the same person, a different
connector or claim, a different username, a narrow Role. We are not asking for
krm-foyer's scope list.

---

## What Voter does not need

So these can be ranked lower against Voter's use:

- **Refresh.** Room Pass's `authproxy` connector issues no refresh tokens, and a
session that ends with its ID token is what Voter does today.
- **More than one replica.** Voter runs one replica with `Recreate`.
- **The `/_foyer/access` page.** Voter's identity screen can use the native reviews
through `/k8s`, as the design suggests.
- **Proxy subresources, upgrade protocols, and the native-watch connector for
krm-stream.**
- **Certificate reload.** It is nice to have. A rollout that briefly refuses
connections is something krm-stream's reconnect already handles on stage.

## What stays Voter's work

None of these is an ask. They are listed so nobody expects them from krm-foyer:

- Move the browser client from krm-stream 0.4.0 to 0.7.0, and `/public/stream` to
`/stream/v1`.
- Change login links (`return=` → `return_to=`, `connector=` → `oidc.connector_id=`).
The hard-coded ones are in `AdminScreen.vue`, `DatabaseEditScreen.vue`,
`RoomScreen.vue` and `OrderScreen.vue`. The QR code (`RoomScreen.vue:118`) points
at `/auth/login?code=` today. Once krm-foyer owns `/auth/login`, it has to point at
Voter's own QR endpoint instead.
- Move application settings out of `/auth/session` into a static `config.json`:
`namespace`, `coffeeConfigName`, `roomName`, `commitURLTemplate`. Compute `canVote`
from `connector` in the browser for display only; the domain backend enforces it.
- **Review grants before exposing `/k8s`.** Participants hold `create` and `list` on
`quizsubmissions`. Today only the vote handler uses `create`, and only the aggregate
results come back. Through `/k8s` a participant could create a submission without
the handler's rules, and list everyone's answers with the submitter's name in a
label. Those grants need narrowing, or admission, before the switch. Results already
live in `QuizSession.status` ([live-results-design.md](live-results-design.md)), so
`list` is no longer needed for them.
- **Keep a CoffeeConfig save to spec only.** The handler refuses anything outside
`spec`. Through `/k8s`, the editor's `patch` grant would also allow labels and
annotations, which gitops-reverser would commit. That needs a
ValidatingAdmissionPolicy.
- **Keep a CoffeeConfig save together with its CommitRequest.** Today that is two
writes on the server with partial success reported. It becomes two writes from the
browser, with the same partial-success handling.
- **Give krm-foyer a client and audience in Dex on Voter's host.** Follow the
cross-client-scope pattern `foyer.k8s.koudijs.dev` already uses.

## Something we could give back

`frontend/src/api/liveEditableResource.ts` (329 lines) is Voter's generic edit loop over
krm-stream. It covers keeping a draft, showing a conflict, a guarded save and retry. The
roadmap's integration guide (*"helper outcomes, conditional saves, unknown write
results, … draft preservation at sign-out"*) describes the same ground. If it would help,
we can extract it as the example rather than have the guide written from scratch.

## Suggested order

1. **Operator-only pilot, now.** It needs entry 3, and entry 4 decided. With the GitHub
login, Voter's admin and CoffeeConfig editor run through `/k8s` and `/stream/v1`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
This is the roadmap's own *"Voter's CoffeeConfig editor running on krm-foyer"*.
2. **The audience login.** It needs entry 2. Participant login moves as one piece,
since there can only be one session cookie on the origin.
3. **Votes and the storefront.** They need entry 1, or a decision in Voter to move
votes to admission. After this step Voter keeps its domain handlers, the quiz
reconciler, the QR endpoint and static hosting, and nothing else.
Loading