Skip to content

feat(operator)!: enforce access.groups at the gateway with nebari-operator v0.2.0 - #168

Merged
jbouder merged 3 commits into
mainfrom
feat/gateway-group-enforcement
Oct 22, 2026
Merged

jbouder merged 3 commits into
mainfrom
feat/gateway-group-enforcement

Conversation

@jbouder

@jbouder jbouder commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Closes #153

Summary

nebari-operator v0.2.0 (released 2026-10-21) is the first release that enforces a NebariApp's auth.groups at the gateway (nebari-operator#198, #209, #214, #215). This PR moves access.groups enforcement there and strips enforcement out of the apps-authz sidecar.

Operator

  • Pin bumped from v0.1.0-alpha.20 to v0.2.0 (.nebari-operator-version, NebariOperatorContract). The dev Makefile reads the pin and now re-clones its cached nebari-operator checkout when the pin changes. The cached one was on alpha.19.
  • A restricted App's NebariApp carries auth.groups (as written) with enforceAtGateway: true. forwardAccessToken is no longer set, so app pods (untrusted code) never receive the user's token.
  • The sidecar runs only in Apps that can idle or run an image. A restricted App that does neither has no sidecar, and a Python task there moves from 8081 to 8080 ($PORT).
  • The operator refuses access.groups entries that name no group (/, blank). nebari-operator would turn those into a deny-all policy (GroupsEmpty). Commas are now allowed, since nothing joins the list into an env var any more.
  • RoutingReady now carries the NebariApp's reason, so GroupsNotResolved (a path missing in Keycloak), GroupsEmpty and GroupsClaimConflict show up on the App.
  • The --keycloak-url, --keycloak-internal-url and --keycloak-realm flags are removed (only the sidecar used them), along with the chart args.

apps-authz sidecar

  • The sidecar is now passthrough only: verify.go/jwks.go and the go-oidc/go-jose dependencies are gone. It proxies every request and serves /activity.
  • It refuses to start if AUTHZ_GROUPS, AUTHZ_ISSUERS, AUTHZ_JWKS_URL or AUTHZ_CLIENT_ID is set. With a newer image and an older operator, a restricted App stays down instead of running open. AUTHZ_PASSTHROUGH is ignored.

Full-path group matching (API, UI, MCP)

  • finance == /finance, and /finance does not admit someone who is only in /finance/analysts. apps-api (groups.py), the operator (groupPath) and the UI (groupPath) all mirror nebari-operator's groups.NormalizePath.
  • apps-api matches api.adminGroups and access.managers.groups as full paths, and /auth/me reports groups as full paths. It already stripped the leading / before, so the v0.2.0 claim change was handled. This makes subgroup handling exact. It also rejects group entries that name no group.
  • The UI validates the groups field and explains the rule. The MCP launch_app and review_access text say so too.

Docs: architecture (with a gateway vs sidecar table), configuration ("NebariApp fields the chart emits"), launching apps (Access control), troubleshooting, runbook (new "Group access" section), getting started, CRD/API reference, upgrading, README, AGENTS.md, the skill and the examples are all updated. upgrading.md covers:

  • upgrading nebari-operator first
  • the one-time re-login from the renamed access-token cookie
  • the switch of the groups claim to full paths, including subgroups that were listed by bare name
  • pods rolling once
  • the token no longer reaching the app

NebariApp contract changes from v0.1.0-alpha.20 to v0.2.0

I diffed api/v1/nebariapp_types.go and the CRD between the two tags. Besides auth.groups enforcement, the changes are:

  • auth.groups items gained minLength: 1. The pack's validation is stricter than that.
  • New optional fields landingPage.iconLight/iconDark and status.serviceDiscovery.iconLight/iconDark (v0.1.0). They are additive and the pack doesn't set them.
  • New AuthReady/Ready reasons: GroupsNotResolved, GroupsRequireKeycloak, GroupsClaimConflict, GroupsEnforcedByApplication, GroupsEmpty. New events: GroupsMappersRemoved, Configured.
  • Behavior changes:
    • every client's groups mapper now emits full paths
    • groups mappers added by hand are deleted
    • a bare-name entry creates a top-level Keycloak group, while a path is never created
    • every app's client is reprovisioned once
  • KEYCLOAK_EXTERNAL_URL must be set on nebari-operator for Envoy Gateway v1.9.1+ (v0.1.2). This is operator config; the pack doesn't need to change.

None of these changes break a field the pack emits.

Security note

The gateway is now the only group check, and the sidecar used to refuse non-members even for in-cluster traffic. The docs recommend apps.networkPolicy.enabled (ingress only from the gateway namespace). If the pack is upgraded before nebari-operator, restricted Apps are open to every signed-in user, so upgrading.md says to upgrade nebari-operator first. The operator can't detect the nebari-operator version without new CRD read RBAC, which this PR doesn't add.

How it was tested

These are local quality gates only. Nothing ran on a cluster.

  • operator: go build, go vet, go test ./... -race (internal coverage 90.1%, floor 89), golangci-lint run (0 issues), envtest suite (-tags envtest, k8s 1.37.0) passes, controller-gen v0.21.0 regenerated the CRD, gen_crd_types.py was run, and check_crd_compat.py passes.
  • authz: go mod tidy -diff, go vet, go build, golangci-lint run, go test -race (internal coverage 96.8%, floor 95).
  • api: uv lock --check, pytest with coverage (316 passed, 91.9%), ruff check, ruff format --check, mypy.
  • mcp: pytest with coverage (71 passed, 90.2%), ruff, mypy.
  • ui: npm run check:ci, build, npm run size (192.1/245 kB), test:coverage (288 passed), and test:e2e (150 passed).
  • docs: bun test test and bun run build.
  • chart: helm lint, helm template (default and examples/nebari-values.yaml), helm-docs README regenerated.
  • repo: check_doc_paths.py, check_versions.py.
  • Not run locally: pip-audit / npm audit (no dependency changes beyond dropping the authz Go deps), actionlint / zizmor (no workflow changes), kubeconform and promtool.

Follow-ups (not in this PR)

  • Verify on a cluster running nebari-operator v0.2.0: a group member is admitted, a non-member gets 403 from the gateway, and a public App and an idling App (including a restricted idling App and its activator page) still work. Also check the one-time re-login.
  • Bump nebari-local to nebari-operator v0.2.0 and an apps-pack build with this change, and bump gitops/apps/apps-pack.yaml there.
  • Audit existing Apps, access.managers.groups and api.adminGroups on live clusters for subgroups listed by bare name (query in upgrading.md).

🤖 Generated with Claude Code

jbouder and others added 3 commits October 21, 2026 21:45
…rator v0.2.0

nebari-operator v0.2.0 is the first release that enforces a NebariApp's
auth.groups at the gateway. A restricted App's NebariApp now carries
auth.groups with enforceAtGateway: true, so a signed-in non-member gets 403
from the gateway before the request reaches the pod, and the access token
is no longer forwarded to app pods (forwardAccessToken is dropped).

The apps-authz sidecar loses its enforcing mode: it only proxies, recording
activity for scale to zero and fronting image Apps on 8080. Only Apps that
can idle or run an image get it. It refuses to start when given group
settings, so an older operator can never run a restricted App open.

The operator refuses access.groups entries that name no group ("/"), which
nebari-operator would turn into a deny-all policy, and RoutingReady now
carries the NebariApp's reason (GroupsNotResolved and friends). The dev
cluster re-clones nebari-operator when the pin changes.

BREAKING CHANGE: requires nebari-operator v0.2.0; upgrade it before the
pack, or restricted Apps are open to every signed-in user. The operator's
--keycloak-url, --keycloak-internal-url and --keycloak-realm flags are
removed. Restricted Apps that neither idle nor run an image lose the
sidecar, so their Python tasks move from port 8081 to 8080 ($PORT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ap1Tqx1QLHRGVVSXdxeduM
nebari-operator v0.2.0 makes the groups claim carry full paths and matches
auth.groups exactly: "finance" is "/finance", and "/finance" does not admit
members of "/finance/analysts" only. apps-api now matches api.adminGroups
and access.managers.groups the same way (groups.group_path mirrors
nebari-operator's NormalizePath), reports the caller's groups as full paths,
and refuses group entries that name no group instead of entries with a
comma. The UI validates and explains the rule, and the MCP tool and prompt
descriptions say so.

BREAKING CHANGE: a subgroup listed by its bare name in api.adminGroups or
access.managers.groups no longer matches; list its full path.
GET /auth/me returns groups with a leading "/".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ap1Tqx1QLHRGVVSXdxeduM
Architecture, configuration (NebariApp fields the chart emits), launching
apps (Access control), troubleshooting, the runbook, getting started, the CRD
and API references, upgrading (upgrade nebari-operator first, the one-time
re-login from the renamed access-token cookie, full-path groups), README,
AGENTS.md, the skill and the examples now describe the split: the gateway
enforces sign-in and access.groups, and the sidecar only tracks activity and
fronts image Apps. The docs version test also catches non-alpha pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ap1Tqx1QLHRGVVSXdxeduM
@github-actions

Copy link
Copy Markdown

📄 Docs preview for feat/gateway-group-enforcement:
https://feat-gateway-group-enforceme.apps-pack.pages.dev

@jbouder
jbouder merged commit 8fbc0dd into main Oct 22, 2026
15 checks passed
@jbouder
jbouder deleted the feat/gateway-group-enforcement branch October 22, 2026 01:52
jbouder added a commit that referenced this pull request Oct 22, 2026
Since the gateway took over sign-in and access.groups (nebari-operator
v0.2.0 SecurityPolicy, #168), the sidecar authorizes nothing: it proxies
requests, records activity for scale to zero and gives image Apps the
single front port 8080. The name "authz" made readers think the pod checks
access. Rename it before 0.2.0, which already renames the images (#169),
and before the Quay repositories are created (#85).

The directory, Go module, image, chart values, operator flags, container
name, port name and sidecar env vars move from authz to proxy. Ports and
behaviour are unchanged. The sidecar still refuses to start when
AUTHZ_GROUPS (or another setting of the removed enforcing mode) is set,
under its old name on purpose, and now also when it gets its own settings
under the old AUTHZ_ names, so an operator from before the rename fails
closed instead of starting an App open. AUTHZ_PASSTHROUGH, which nothing
read any more, is no longer mentioned.

BREAKING CHANGE: the sidecar image is now ghcr.io/nebari-dev/apps-pack/proxy
(quay.io/nebari/apps-pack-proxy) and the chart's authz.* values are now
proxy.*; the schema rejects the old key. The sidecar container is renamed
from authz to proxy, so every pod that runs it rolls once. Upgrade the
chart and images together.


Claude-Session: https://claude.ai/code/session_01Ap1Tqx1QLHRGVVSXdxeduM

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce access.groups at the gateway and drop the authz sidecar's group check once nebari-operator releases gateway group enforcement

1 participant