Skip to content

Scope-based RBAC for restricted docs and inline sections.

Updated Jun 9, 2026

Access Control

Olly docs uses scope-based RBAC so individual pages and inline sections can be restricted to specific Keycloak roles, while the bulk of the corpus stays public.

Model

Every viewer has zero or more scopes (Keycloak realm roles). Pages and blocks declare required scopes. A viewer matches if the intersection with required scopes is non-empty (mode: any, default) or covers all (mode: all). The docs-admin scope is a super-scope - it satisfies any check.

ScopeGrants access to
docs-adminEverything. Super-scope; satisfies any check.
docs-internalOlly-internal content (this page, internal design docs, raw eval reports).
partner-healthPages shared with NHS / health-partner reviewers.
partner-insurancePages shared with reinsurance and underwriting partners.
partner-clinical-opsPages shared with clinical operations partners (triage hint review, MSK pathway evidence).

How to gate a whole page

Add access: to the page frontmatter:

yaml
---
title: My Internal Doc
access:
  scopes: [docs-internal]
  mode: any
---

The page is still emitted into dist/ but nginx refuses to serve it without a matching session. Use the page-gate when the entire page is sensitive - the URL itself is enough of a signal that you don't want the body cached anywhere public.

How to gate a block within an otherwise-public page

Wrap the sensitive content in a <Internal> element. The block content is extracted at build time and never appears in the public static HTML - only a <GatedBlock id="..." /> placeholder remains in the page.

markdown
Most of this page is public.

<Internal>

## Internal-only section

This content is sha256'd, written to `/_gated/<id>.html`, and only fetched
after oauth2-proxy confirms the viewer has the required scopes.

</Internal>

Public content continues below.

The audit script scripts/audit-gated-leak.sh greps the built dist/ after every build to confirm no inner block text leaked into the public HTML.

How to grant scopes to a user

  1. Sign in at https://auth.dev.hiolly.com/admin.
  2. Realm olly -> Users -> pick user -> Role mapping.
  3. Assign realm role(s) from the table above.

Roles propagate at next sign-in or after the oauth2-proxy session refreshes (max 1h).

Architecture

LayerWhere it runsWhat it does
Auth gatewaysystemd unit oauth2-proxy-docs on dev-1Validates the session cookie against Keycloak, exposes /oauth2/auth for nginx subrequests, serves userinfo at /_user.json.
Cookie domain.dev.hiolly.comSingle sign-on across all *.dev.hiolly.com hosts.
nginx/etc/nginx/sites-enabled/ollyauth_request /oauth2/auth?allowed_groups=... per path. Per-page scope map at /var/www/docs.dev.hiolly.com/_access/page-scopes.conf, regenerated on every build.
Gated blocksmarkdown-it plugin gated-block.tsWrites /var/www/docs.dev.hiolly.com/_gated/<sha256>.html at build time. nginx serves with the same auth_request pattern.
Sidebar filterSidebarFilter componentFetches /_user.json (oauth2-proxy userinfo) and hides items the viewer can't access.
Login UILoginButton in the navAnonymous "Sign in" pill, signed-in email pill with sign-out dropdown.
403 page/403.html (static, public)Shown when a signed-in user lacks the required scope.

Defense-in-depth: every layer enforces scopes independently. The sidebar filter only hides links - it never gates content. nginx + oauth2-proxy is the source of truth.

Operational notes

  • Rotate the docs-auth client secret in Keycloak quarterly. Update /etc/oauth2-proxy/docs-client-secret and systemctl restart oauth2-proxy-docs.
  • Cookie secret in /etc/oauth2-proxy/cookie-secret rotates annually. Rotating invalidates all live sessions.
  • nginx access maps regenerate on every npm run build and live under /var/www/docs.dev.hiolly.com/_access/. If a scope check looks wrong, check the generated page-scopes.conf first - it pins the build-time view of frontmatter.

SSO across all olly hosts

cookie_domain=.dev.hiolly.com means signing in at any olly host (member portal, employer portal, admin console, docs) gives a session at docs too. This is intentional SSO. Tighten in /etc/oauth2-proxy/docs.cfg if you don't want it.

Olly Health Insurance Platform