@lamalibre/create-lamaste 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +56 -5
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +80 -12
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +325 -55
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +50 -19
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +576 -131
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
# External Members of a Domain — Design Document
|
|
2
|
+
|
|
3
|
+
> **Status:** agreed 2026-10-26 (all ten decisions as recommended; Redis left for a later, separate design).
|
|
4
|
+
> **Base:** the multi-domain relay (`multi-domain-relay-design.md`, Lamaste 4.7.0, PR lamalibre/lamaste#1, not merged).
|
|
5
|
+
> **Pilot:** corral (`corral.codelama.com.tr`): the client's people are external members of `codelama.com.tr`.
|
|
6
|
+
> **Companions:** `herd/docs/r730-workload-plan.md` §15.2a (scope), §15.2 (delegated invitations, which come after this),
|
|
7
|
+
> `identity-directory-design.md`, `gatekeeper-design.md`.
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
A domain membership has a **kind**: `member` or `external`.
|
|
12
|
+
|
|
13
|
+
- A **member** reaches what 4.7 gives a member: every `authenticated` host
|
|
14
|
+
and OIDC client of the domain, and `restricted` ones by grant.
|
|
15
|
+
- An **external** member reaches only what names them (or a Lamaste group
|
|
16
|
+
they belong to) explicitly:
|
|
17
|
+
- `restricted` tunnels, plugin user surfaces and web surfaces, with a
|
|
18
|
+
Gatekeeper grant;
|
|
19
|
+
- static sites whose `allowedUsers` names them;
|
|
20
|
+
- a web surface's shell only when granted (the shell is the web surface's
|
|
21
|
+
static front, `/authz/user-surface-shell`);
|
|
22
|
+
- OIDC clients granted to them.
|
|
23
|
+
- External members never join an **Authelia** group (`admins`, `feria-*`);
|
|
24
|
+
**Lamaste groups** (Gatekeeper's) in grants still apply to them.
|
|
25
|
+
- External members count toward a plugin's audience when entitled.
|
|
26
|
+
- Enforced in Authelia's rendered configuration **and** in Gatekeeper
|
|
27
|
+
wherever Gatekeeper is in the path.
|
|
28
|
+
- An invitation names the membership kind.
|
|
29
|
+
- Changing an external member's membership or grants does not sign anyone
|
|
30
|
+
out.
|
|
31
|
+
|
|
32
|
+
## What exists in 4.7
|
|
33
|
+
|
|
34
|
+
| Piece | 4.7 |
|
|
35
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| Membership | `domain_members (domain, user_id, added_at, added_by, seq)` (state.db `0016`). No kind. |
|
|
37
|
+
| Authelia `access_control` | `renderAccessControl` (core `server/authelia.ts`): per protected static site — `allowedUsers ∩ members` as `user:` subjects, `[group:admins, user:<m>]` per member, deny; per domain — members as `user:` subjects on `[d, *.d]`, deny; `default_policy: deny`. |
|
|
38
|
+
| OIDC | `renderOidcAuthorizationPolicies`: one policy per domain and factor, members as `user:` subjects; a user-facing client points at its domain's policy. |
|
|
39
|
+
| Rendering | serverd `lib/domains.js` `syncDomainAccess` under `withDomainRendering`: projection `domains.json` `{v:1, domains:[{name, members}]}`, then `updateDomainAccess` (cookies, `access_control`, OIDC policies) through `rewriteAutheliaConfig`, which **restarts Authelia** whenever the section changed. |
|
|
40
|
+
| Users file | `changeUsers` (serverd `lib/directory.js`) writes `users.yml`, renders the domains, and **restarts Authelia** (its file backend has no `watch`: Authelia reads `users.yml` only at start). Authelia's own password reset writes the file from what it read at start, so a panel write can be lost (`expect` re-applies it). |
|
|
41
|
+
| Gatekeeper | `authenticatedSession` → Authelia, then `checkDomain` against the projection (member or refusal page), then the access mode: `public`/`authenticated` allow, `restricted` checks grants. User surfaces and the web-surface shell the same. `setAutheliaHeaders` passes `Remote-Groups` through; plugin tunnels sign them into the relay assertion. |
|
|
42
|
+
| Audience | entitled (Gatekeeper `plugin` grants, Lamaste groups expanded) ∩ members of the plugin's domain (`lib/audience.js`). |
|
|
43
|
+
| Sessions | Authelia's session store is **in memory** (no Redis). A restart ends every session on every domain — residual risk **R3**. |
|
|
44
|
+
|
|
45
|
+
So today **every** membership change, user creation, deletion, disable and
|
|
46
|
+
password-unrelated `users.yml` edit restarts Authelia and signs everyone out.
|
|
47
|
+
With a client's people invited and revoked often (and by delegated
|
|
48
|
+
invitations later), that is the cost this design removes.
|
|
49
|
+
|
|
50
|
+
## Authelia facts this design rests on (4.39.28, read from the source)
|
|
51
|
+
|
|
52
|
+
Checked against `github.com/authelia/authelia` tag `v4.39.28`:
|
|
53
|
+
|
|
54
|
+
1. **Users-file watch.** `authentication_backend.file.watch: true` starts a
|
|
55
|
+
watcher on the file's directory (`internal/service/file_watcher.go`); a
|
|
56
|
+
`Write` or `Create` event on `users.yml` — an atomic rename into place is
|
|
57
|
+
a `Create` — reloads the in-memory database (`FileUserProvider.Reload`).
|
|
58
|
+
A reload within 0.5 s of the previous one (or of Authelia's own save) is
|
|
59
|
+
**skipped** (`ErrWatcherCooldown`), not queued.
|
|
60
|
+
2. **Fresh groups on every request.** With `watch` and no explicit
|
|
61
|
+
`refresh_interval`, the refresh interval becomes `always`
|
|
62
|
+
(`validator/authentication.go`): every forward-auth request
|
|
63
|
+
(`handleSessionValidateRefresh`) re-reads the user's details from the
|
|
64
|
+
in-memory database; a user who is gone or `disabled` invalidates the
|
|
65
|
+
session. So a `users.yml` change applies to existing sessions at their
|
|
66
|
+
next request, without a restart.
|
|
67
|
+
3. **OIDC authorization-code reads fresh groups too — the device-code
|
|
68
|
+
flow does not.** The authorization-code handlers call the same refresh
|
|
69
|
+
before evaluating the client's authorization policy against
|
|
70
|
+
`userSession.Groups` (`handler_oauth2_authorization_consent_core.go`).
|
|
71
|
+
The device-code handlers (`handler_oauth2_device_authorization.go`, the
|
|
72
|
+
consent POST in `handler_oauth2_consent.go`) evaluate the groups the
|
|
73
|
+
session last refreshed (found by the security review, S1; residual R5).
|
|
74
|
+
4. **Group subjects from a hostname.** An `access_control` rule's
|
|
75
|
+
`domain_regex` with a named group `(?P<Group>…)` matches only when the
|
|
76
|
+
captured text is one of the user's groups (`RegexpGroupStringSubjectMatcher`,
|
|
77
|
+
case-insensitive); a rule may carry `subject` too (all conditions ANDed,
|
|
78
|
+
`AccessControlRule.IsMatch`).
|
|
79
|
+
5. **The standard `groups` OIDC claim cannot be redefined or filtered**
|
|
80
|
+
(`validateOIDCScopes` refuses a custom definition of a standard scope or
|
|
81
|
+
claim): an OIDC client that asks for `groups` receives every group the
|
|
82
|
+
user has.
|
|
83
|
+
6. Sessions are in memory unless `session.redis` is configured.
|
|
84
|
+
|
|
85
|
+
## Design
|
|
86
|
+
|
|
87
|
+
### The idea: who is named lives in `users.yml`, the rules stay still
|
|
88
|
+
|
|
89
|
+
4.7 writes _who_ into `configuration.yml` (`user:` subjects), so every change
|
|
90
|
+
of who restarts Authelia. This design writes **who into `users.yml`** as
|
|
91
|
+
groups Lamaste owns, and **rules that name those groups into
|
|
92
|
+
`configuration.yml`**. With the users file watched (fact 1) and refreshed on
|
|
93
|
+
every request (facts 2, 3), a membership, kind, grant or `allowedUsers`
|
|
94
|
+
change becomes a `users.yml` write: Authelia reloads it in place and applies
|
|
95
|
+
it at each user's next request. `configuration.yml` changes — and Authelia
|
|
96
|
+
restarts — only when the **structure** changes: a domain added, activated or
|
|
97
|
+
removed; a static site protected, unprotected, created or removed; the set
|
|
98
|
+
of OIDC clients or a plugin's domain.
|
|
99
|
+
|
|
100
|
+
The same switch removes the restart from every other `users.yml` change
|
|
101
|
+
(user created, invited, deleted, disabled, groups changed) for members too,
|
|
102
|
+
and makes Authelia's own password-reset write follow the file (fact 1) —
|
|
103
|
+
narrowing the lost-write window `changeUsers`' `expect` exists for.
|
|
104
|
+
|
|
105
|
+
### Groups Lamaste owns
|
|
106
|
+
|
|
107
|
+
Rendered by the panel into each user's `groups` in `users.yml`, beside the
|
|
108
|
+
groups an admin assigns:
|
|
109
|
+
|
|
110
|
+
| Group | Holds |
|
|
111
|
+
| ------------------------- | ----------------------------------------------------------------------------------------- |
|
|
112
|
+
| `lamaste:m:<key>` | active members (kind `member`) of the active domain whose group key is `<key>` |
|
|
113
|
+
| `lamaste:x:<key>` | active external members of that domain |
|
|
114
|
+
| `lamaste:s:<site id>` | the listed users (`allowedUsers`) of a protected static site who are people of its domain |
|
|
115
|
+
| `<fqdn>` (a hostname) | **host group**: the external members granted a Gatekeeper-gated restricted host — below |
|
|
116
|
+
| `lamaste:oidc@<clientId>` | active external members of the client's domain with a grant on the client (OIDC, below) |
|
|
117
|
+
|
|
118
|
+
The **group key** is 16 random hex characters on the domain record
|
|
119
|
+
(`domains.group_key`, state.db `0018`): a group names no domain, so a
|
|
120
|
+
relying party that asks for the `groups` claim learns no tenant's name
|
|
121
|
+
(security review, decision 8 as revised). A site group is named by the
|
|
122
|
+
site's id for the same reason (replacing 4.7's `user:` subjects, editing
|
|
123
|
+
`allowedUsers` no longer restarts Authelia).
|
|
124
|
+
|
|
125
|
+
Host group `<fqdn>` — a **Gatekeeper-gated restricted host**: an enabled
|
|
126
|
+
`restricted` app or plugin tunnel (a tunnel without an access mode is
|
|
127
|
+
restricted, as Gatekeeper reads it), a `restricted` tunnel bound as a user
|
|
128
|
+
surface, a `restricted` web surface: the **external** members of its domain
|
|
129
|
+
who hold a grant on it (direct, or through a Lamaste group) — decision 3.
|
|
130
|
+
Members do not need it: the domain rule admits them and Gatekeeper checks
|
|
131
|
+
their grants, as in 4.7. Only these must be hostnames: Authelia's
|
|
132
|
+
`(?P<Group>…)` capture compares a host's name with the user's groups.
|
|
133
|
+
|
|
134
|
+
`:` never occurs in a hostname, and a host group is always a hostname under
|
|
135
|
+
a relay domain, so no Lamaste group can be mistaken for a host or the other
|
|
136
|
+
way round.
|
|
137
|
+
|
|
138
|
+
**Reserved names.** Groups an admin assigns (users API, invitations, the
|
|
139
|
+
panel's editor) must match `^[A-Za-z0-9_-]{1,64}$` (`admins`, `feria-admins`
|
|
140
|
+
fit); a name with `.`, `:` or `@` is refused (400 `reserved_group`). The
|
|
141
|
+
render owns every group that starts with `lamaste:` or is a hostname under a
|
|
142
|
+
relay domain (any domain record, pending included, and every domain removed
|
|
143
|
+
from the relay — `retired_domains`, state.db `0018`), compared folded as
|
|
144
|
+
Authelia compares groups (NFKC, then lower case: Go's `EqualFold` takes `ſ`
|
|
145
|
+
for `s` and the Kelvin sign for `k`): it adds what is desired and removes
|
|
146
|
+
the rest, so a stray or look-alike host group cannot survive a render.
|
|
147
|
+
Groups outside both stay as they are (an older admin group with a dot is
|
|
148
|
+
left alone, but cannot be newly assigned).
|
|
149
|
+
|
|
150
|
+
Lamaste-owned groups are **never shown** as groups (users list, user
|
|
151
|
+
editor, `/api/identity/users`, `/api/identity/self`): the user's domains
|
|
152
|
+
with their kinds are shown instead. Gatekeeper strips them from
|
|
153
|
+
`Remote-Groups` and from a plugin tunnel's relay assertion
|
|
154
|
+
(`groups`); they reach only Authelia — and, unavoidably, an OIDC client that
|
|
155
|
+
asks for the `groups` claim (fact 5; residual R4).
|
|
156
|
+
|
|
157
|
+
### `access_control`
|
|
158
|
+
|
|
159
|
+
`renderGroupAccessControl(domains, sites)` — `domains` are
|
|
160
|
+
`{ name, groupKey }` (no usernames: the rules name groups); `m(d)` /
|
|
161
|
+
`x(d)` below are `lamaste:m:<key of d>` / `lamaste:x:<key of d>`:
|
|
162
|
+
|
|
163
|
+
1. **Per protected static site** (domain `d`):
|
|
164
|
+
- `allowedUsers` set: `subject: group:lamaste:s:<site id>`, `two_factor`
|
|
165
|
+
(a site listing users without an id gets none: admins only);
|
|
166
|
+
- `subject: [[group:admins, group:m(d)]]`, `two_factor`
|
|
167
|
+
(an admin who is a member — never external, below);
|
|
168
|
+
- `deny`.
|
|
169
|
+
2. **Per domain `d`**, in this order:
|
|
170
|
+
- the sign-in portal: `domain: [auth.d]`, `subject: [group:m(d),
|
|
171
|
+
group:x(d)]`, `two_factor` — nginx never asks Authelia about the portal for
|
|
172
|
+
a resource but the desktop's `/api/user-access/authorize`, which refuses
|
|
173
|
+
external members itself; Gatekeeper asks it to learn who a refusal was
|
|
174
|
+
for (below);
|
|
175
|
+
- members: `domain: [d, '*.d']`, `subject: group:m(d)`, `two_factor`;
|
|
176
|
+
- external members on hosts that name them:
|
|
177
|
+
`domain_regex: '^(?P<Group>(?:[a-z0-9-]+\.)+<d quoted>)$'`,
|
|
178
|
+
`subject: group:x(d)`, `two_factor` — admitted only on a host whose
|
|
179
|
+
name is one of their groups (fact 4), and only while they are an
|
|
180
|
+
external member of `d`. The apex is not matched: it is never a
|
|
181
|
+
Gatekeeper-gated host;
|
|
182
|
+
- `domain: [d, '*.d']`, `deny`.
|
|
183
|
+
3. `default_policy: deny` (unchanged).
|
|
184
|
+
|
|
185
|
+
Every rule above is fixed for a given set of domains and protected sites:
|
|
186
|
+
memberships, kinds, grants and `allowedUsers` never change it. A static
|
|
187
|
+
site's rules come first and end with `deny`, so the external rule never
|
|
188
|
+
reaches a static site's host — `allowedUsers` alone decides there, as the
|
|
189
|
+
plan asks.
|
|
190
|
+
|
|
191
|
+
**Restricted hosts appear nowhere in the configuration**: a tunnel created,
|
|
192
|
+
deleted or switched between `authenticated` and `restricted` changes only
|
|
193
|
+
which host groups the panel renders. A new `authenticated` tunnel is closed
|
|
194
|
+
to external members from its first request (no host group, the member rule
|
|
195
|
+
does not match them, deny) — fail closed by construction.
|
|
196
|
+
|
|
197
|
+
### OIDC: per-client grants and policies
|
|
198
|
+
|
|
199
|
+
- **Grant.** A Gatekeeper grant with `resourceType: 'oidc-client'`,
|
|
200
|
+
`resourceId: <client id>`, principal a user or a Lamaste group — the grant
|
|
201
|
+
model and API Gatekeeper already has (validated by the panel: the client
|
|
202
|
+
exists and is user-facing). Gatekeeper is not in the OIDC path; it only
|
|
203
|
+
stores the grant. The panel expands it (Lamaste groups too, as
|
|
204
|
+
`lib/audience.js` does) into `lamaste:oidc@<clientId>` for the external
|
|
205
|
+
members of the client's domain.
|
|
206
|
+
- **Policy per user-facing client** (replacing 4.7's per-domain policies),
|
|
207
|
+
named `lamaste_client_<client id>` (ids are `[a-z0-9-]`, no dot for
|
|
208
|
+
Authelia's loader to split on):
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
authorization_policies:
|
|
212
|
+
lamaste_client_feria-cli:
|
|
213
|
+
default_policy: deny
|
|
214
|
+
rules:
|
|
215
|
+
- policy: two_factor # the client's declared factor
|
|
216
|
+
subject: 'group:lamaste:m:<key of codelama.com.tr>'
|
|
217
|
+
- policy: two_factor
|
|
218
|
+
subject: [['group:lamaste:x:<key of codelama.com.tr>', 'group:lamaste:oidc@feria-cli']]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Members of the client's domain are admitted as in 4.7; external members
|
|
222
|
+
only with the grant. The policy changes only with the client set or a
|
|
223
|
+
plugin's domain; a grant is a `users.yml` change, applied at the user's
|
|
224
|
+
next authorization (fact 3 — the authorization-code path; the
|
|
225
|
+
device-code path reads the session's groups as they were, so a narrowing
|
|
226
|
+
that reaches a device-code client restarts Authelia: security review S1).
|
|
227
|
+
Pure `client_credentials` clients keep `one_factor` (unchanged).
|
|
228
|
+
|
|
229
|
+
- **Tokens already issued** survive a revocation until they expire. Plugin
|
|
230
|
+
clients get no refresh tokens (4.7, R1), so the bound is the access and ID
|
|
231
|
+
token lifespan: Authelia's default is one hour; decision 6 proposes
|
|
232
|
+
15 minutes for user-facing clients. Registry tokens Feria minted from a
|
|
233
|
+
login are revoked by Lamaste when the grant behind them ends (below).
|
|
234
|
+
|
|
235
|
+
### Gatekeeper
|
|
236
|
+
|
|
237
|
+
- **Projection v2** (`domains.json`):
|
|
238
|
+
`{ "v": 2, "domains": [{ "name": "…", "members": ["…"], "externals": ["…"] }] }`.
|
|
239
|
+
An unknown version is a missing projection: every check refuses (fail
|
|
240
|
+
closed, as 4.7). Panel and Gatekeeper ship together (one release).
|
|
241
|
+
- `checkDomain` returns the kind. When Authelia refuses a signed-in
|
|
242
|
+
session (its 403 names nobody), Gatekeeper asks Authelia once more about
|
|
243
|
+
the same session for `https://auth.<d>/` (the portal rule admits the
|
|
244
|
+
domain's members and external members): an external member is then told
|
|
245
|
+
they are not granted (the access-request page, or `forbidden`), anyone
|
|
246
|
+
else gets the domain refusal. Never cached, and the request stays
|
|
247
|
+
refused. Then, on every route:
|
|
248
|
+
- **tunnel** — `public` hosts are not gated; `authenticated`: a member
|
|
249
|
+
passes, an external member is refused with the **access-request page**
|
|
250
|
+
(403, "ask the administrator for access" — they are on the domain, only
|
|
251
|
+
not granted), logged `external_not_granted`; `restricted`: grants, as
|
|
252
|
+
today, for both kinds.
|
|
253
|
+
- **user surface** — `authenticated`: external → 403 JSON `forbidden`
|
|
254
|
+
(`X-Lamalibre-Refusal: forbidden`); `restricted`: the host's grant.
|
|
255
|
+
- **web-surface shell** — `authenticated`: external → access-request
|
|
256
|
+
page; `restricted`: audience + `site` grant (unchanged).
|
|
257
|
+
- `Remote-Groups` and the plugin-tunnel assertion's `groups` drop
|
|
258
|
+
Lamaste-owned groups (core `isLamasteOwnedGroup`).
|
|
259
|
+
- Session cache: unchanged (keyed by session and host, busted on every
|
|
260
|
+
change the panel makes).
|
|
261
|
+
|
|
262
|
+
Authelia and Gatekeeper each refuse an external member on an
|
|
263
|
+
`authenticated` host on their own: Authelia because no rule admits them
|
|
264
|
+
(no host group), Gatekeeper because the projection says `external`. On a
|
|
265
|
+
`restricted` host both check the grant: Authelia through the host group,
|
|
266
|
+
Gatekeeper from its database.
|
|
267
|
+
|
|
268
|
+
### The directory
|
|
269
|
+
|
|
270
|
+
`state.db` migration `0017_membership_kind.sql`:
|
|
271
|
+
|
|
272
|
+
```sql
|
|
273
|
+
ALTER TABLE domain_members ADD COLUMN kind TEXT NOT NULL DEFAULT 'member'
|
|
274
|
+
CHECK (kind IN ('member', 'external'));
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- Existing memberships stay `member` (no change of behaviour).
|
|
278
|
+
- A kind change is an update; a trigger stamps `seq` on it (as on insert),
|
|
279
|
+
audited `directory.membership.changed` `{ domain, userId, from, to }`.
|
|
280
|
+
`directory.membership.added` gains `kind`.
|
|
281
|
+
- **Authelia groups and external members** (decision 4): a user who is an
|
|
282
|
+
external member of any domain holds no Authelia group. `PUT /api/users/:u`
|
|
283
|
+
adding a group to such a user, or making a user with groups external
|
|
284
|
+
anywhere, is refused (409 `external_member_groups`); an invitation naming
|
|
285
|
+
both groups and an external membership is refused (400). The admin removes
|
|
286
|
+
the groups first.
|
|
287
|
+
- **Disable / delete** as in 4.7 (disable keeps memberships and their kinds;
|
|
288
|
+
delete removes them).
|
|
289
|
+
- **New users** on a one-domain relay join it as `member` (4.7, decision 1);
|
|
290
|
+
an invitation or user creation may name `external` explicitly.
|
|
291
|
+
|
|
292
|
+
### Rendering — one writer, two files
|
|
293
|
+
|
|
294
|
+
`withDomainRendering` stays the one writer. `syncDomainAccess` becomes:
|
|
295
|
+
|
|
296
|
+
1. read domains (with their group keys), memberships with kinds, protected
|
|
297
|
+
sites, Gatekeeper-gated restricted hosts, the grants on them and on OIDC
|
|
298
|
+
clients (Gatekeeper's API, Lamaste groups expanded); when the grants
|
|
299
|
+
cannot be read, external members hold no granted host or client group
|
|
300
|
+
(fail closed) and the step counts as failed (audited, notified);
|
|
301
|
+
2. write **`users.yml`'s Lamaste-owned groups** (one file lock, compare
|
|
302
|
+
first — `renderOwnedGroups`, pure, in core);
|
|
303
|
+
3. write the projection;
|
|
304
|
+
4. write `configuration.yml` (cookies, `access_control`) — compare first;
|
|
305
|
+
restart only when it changed (structural); the OIDC provider follows in
|
|
306
|
+
`syncOidcClients`, after the domains at start too;
|
|
307
|
+
5. restart Authelia when it must read `users.yml` anew: its configuration
|
|
308
|
+
does not have it hold the file as Lamaste needs (watch, refresh always,
|
|
309
|
+
its own password writes off) and the file changed; or a narrowing
|
|
310
|
+
reached a device-code OIDC client (security review S1).
|
|
311
|
+
|
|
312
|
+
Then, outside the lock, every enabled user who lost an owned group since
|
|
313
|
+
the last complete rendering — whatever narrowed them, a revocation with
|
|
314
|
+
Gatekeeper's own CLI included — is audited (`directory.access.narrowed`,
|
|
315
|
+
by id and a count) and loses the registry tokens the registry's sign-in
|
|
316
|
+
would refuse now.
|
|
317
|
+
|
|
318
|
+
**Order.** `users.yml` is always written before the configuration: a new
|
|
319
|
+
site rule or policy names a group that is already filled, and a revoked
|
|
320
|
+
group is empty before any rule could matter (a removed site's group goes
|
|
321
|
+
first too: its rule then denies everyone until it goes).
|
|
322
|
+
|
|
323
|
+
**The reload cooldown** (fact 1). A write that lands within 0.5 s of the
|
|
324
|
+
previous reload is skipped by Authelia, silently. So `users.yml` writes are
|
|
325
|
+
serialized under one file lock and each is **followed by a touch** once
|
|
326
|
+
0.6 s have passed (`touchUsersFile`: read again, the owned groups applied
|
|
327
|
+
again, written), guaranteeing a reload after the cooldown with Lamaste's
|
|
328
|
+
groups even if something else wrote the file meanwhile; the identity tick
|
|
329
|
+
and the start touch it too. Authelia's own writes of the file (password
|
|
330
|
+
change and reset) are disabled. A write never drops a user it was not told
|
|
331
|
+
to delete, never adds owned groups to a user the last rendering did not
|
|
332
|
+
know (created since, perhaps under a deleted user's username), and only
|
|
333
|
+
narrows after a rendering failed.
|
|
334
|
+
|
|
335
|
+
**Triggers.** Every membership, kind, user, site, tunnel, user-surface,
|
|
336
|
+
web-surface and plugin-domain change already renders; additionally every
|
|
337
|
+
grant or Lamaste-group write the panel proxies to Gatekeeper
|
|
338
|
+
(`/api/gatekeeper/*`, entitlements) renders afterwards, and the identity
|
|
339
|
+
tick (every minute) renders — which catches grants made with Gatekeeper's
|
|
340
|
+
own CLI on the relay (Gatekeeper enforces them at once; Authelia's host
|
|
341
|
+
group follows within a minute).
|
|
342
|
+
|
|
343
|
+
**Revocations.** A user narrowed loses their registry tokens when none of
|
|
344
|
+
the registry's own sign-in clients — the user-facing clients whose audience
|
|
345
|
+
is the registry — admits them any more: as a member of the client's domain,
|
|
346
|
+
or as an external member granted it. No such client, or one that cannot be
|
|
347
|
+
read, is "no" (fail closed).
|
|
348
|
+
|
|
349
|
+
### What ends when a member is made external
|
|
350
|
+
|
|
351
|
+
Their `lamaste:m:<key>` group becomes `lamaste:x:<key>` in one write:
|
|
352
|
+
at their next request Authelia admits them only on hosts naming them, and
|
|
353
|
+
Gatekeeper (projection, session cache busted) refuses `authenticated` hosts
|
|
354
|
+
— within nginx's 10 s authorization cache (`AUTHZ_SUCCESS_MAX_AGE_SECONDS`,
|
|
355
|
+
plugin tunnels and user surfaces are never cached). Their Authelia groups
|
|
356
|
+
must have been removed first (decision 4). OIDC: their next authorization is
|
|
357
|
+
refused (a device-code client of the domain: Authelia restarts so no
|
|
358
|
+
session keeps the group it last read); issued access tokens live out their
|
|
359
|
+
15-minute lifespan. Desktop user access (primary domain only) checks the
|
|
360
|
+
kind at every request.
|
|
361
|
+
|
|
362
|
+
### Audience
|
|
363
|
+
|
|
364
|
+
Unchanged rule: entitled ∩ (members ∪ external members) of the plugin's
|
|
365
|
+
domain. Whether the plugin is told the kind is decision 7.
|
|
366
|
+
|
|
367
|
+
### Invitations
|
|
368
|
+
|
|
369
|
+
`domains` entries name a kind (decision 2):
|
|
370
|
+
`domains: [{ "name": "codelama.com.tr", "kind": "external" }]`. Accepting
|
|
371
|
+
adds the memberships with their kinds in the same `changeUsers`. The
|
|
372
|
+
invitation list shows the kinds. A delegated invitation (§15.2, later) will
|
|
373
|
+
only be able to create `external` memberships.
|
|
374
|
+
|
|
375
|
+
### API, panel, desktop, CLI
|
|
376
|
+
|
|
377
|
+
- `GET /api/domains/:name/members` adds `kind`; `PUT
|
|
378
|
+
/api/domains/:name/members/:user` takes `{ kind }` (default `member`;
|
|
379
|
+
re-putting with another kind changes it); `GET /api/users` and
|
|
380
|
+
`PUT /api/users/:username` carry `domains: [{ name, kind }]`;
|
|
381
|
+
`GET /api/domains` counts members and external members.
|
|
382
|
+
- `POST /api/gatekeeper/grants` accepts `resourceType: 'oidc-client'`
|
|
383
|
+
(validated by the panel); `GET /api/plugins/:name/oidc-clients` lists each
|
|
384
|
+
user-facing client's grants.
|
|
385
|
+
- **Panel**: an "External" badge on Users, on the domain's member list and
|
|
386
|
+
on invitations; the membership editor picks the kind per domain; the
|
|
387
|
+
Authelia group editor is disabled for external users with the reason; the
|
|
388
|
+
plugin card's OIDC clients section grants and revokes; the People panel
|
|
389
|
+
marks external audience members.
|
|
390
|
+
- **AdminClient** (enforced at build time): membership methods take/return
|
|
391
|
+
`kind`; `getOidcClientGrants`, `grantOidcClient`, `revokeOidcClientGrant`;
|
|
392
|
+
web client, desktop client and `admin_*` Tauri commands.
|
|
393
|
+
- **CLI** `lamaste-server`: `domains members <d> add <user> [--external]`,
|
|
394
|
+
`domains members <d> kind <user> member|external`, the kind in
|
|
395
|
+
`users list`/`domains members` output, `plugins oidc-clients <name>
|
|
396
|
+
grants|grant|revoke <clientId> --user <u>|--group <g>`.
|
|
397
|
+
|
|
398
|
+
### Upgrade in place
|
|
399
|
+
|
|
400
|
+
1. Migrations 0017 (every membership `member`) and 0018 (a group key per
|
|
401
|
+
domain, `retired_domains`); Gatekeeper 0004 (access grants never
|
|
402
|
+
consumed).
|
|
403
|
+
2. First start after the upgrade renders the domains before the OIDC
|
|
404
|
+
clients: `users.yml` gains each member's `lamaste:m:<key>`, then
|
|
405
|
+
`configuration.yml` the users-file settings and the group-based rules —
|
|
406
|
+
**one** restart (sessions end once, at the update, as every update does
|
|
407
|
+
today); the projection becomes v2.
|
|
408
|
+
3. create-lamaste's redeploy keeps `users.yml` writable by Authelia's account
|
|
409
|
+
and the panel (0660 `lamaste:lamaste-authelia`, the directory 2770) — the
|
|
410
|
+
watcher needs nothing more.
|
|
411
|
+
4. A relay with no external members behaves exactly as 4.7 (same people
|
|
412
|
+
reach the same hosts), minus the restarts.
|
|
413
|
+
|
|
414
|
+
## Security notes
|
|
415
|
+
|
|
416
|
+
- Two independent gates, one source: the panel renders both from `state.db`
|
|
417
|
+
(and Gatekeeper's grants). Authelia: host groups, member/external groups,
|
|
418
|
+
`default_policy: deny`. Gatekeeper: the projection's kind and its own
|
|
419
|
+
grants.
|
|
420
|
+
- **Fail closed everywhere new**: a host with no host group refuses external
|
|
421
|
+
members; an unknown projection version refuses everyone; a skipped reload
|
|
422
|
+
is re-triggered; a render failure is audited and notified (4.7, S4).
|
|
423
|
+
- `users.yml` becomes security-relevant for _access_, not only credentials:
|
|
424
|
+
it was already (Authelia groups, `admins`). Who may write it is unchanged
|
|
425
|
+
(the panel and Authelia's account).
|
|
426
|
+
- **R3 narrowed**: no membership, kind, grant, user or `allowedUsers` change
|
|
427
|
+
restarts Authelia — but for a narrowing that reaches a device-code OIDC
|
|
428
|
+
client (R5). A restart (all sessions end) remains for structural changes
|
|
429
|
+
— a domain added/activated/removed, a static site
|
|
430
|
+
protected/unprotected/created/removed, the OIDC client set or a plugin's
|
|
431
|
+
domain changed — all admin actions, none per person. Redis sessions,
|
|
432
|
+
which would remove it, are a later, separate design.
|
|
433
|
+
- **R4 (narrowed by the review)**: an OIDC client that asks for `groups`
|
|
434
|
+
(Feria's `feria login`) sees the user's Lamaste-owned groups. Member,
|
|
435
|
+
external and site groups are opaque (random keys, site ids), so no
|
|
436
|
+
domain's or site's name leaks; what remains is that two users sharing a
|
|
437
|
+
key share a domain, the OIDC client ids granted to an external member,
|
|
438
|
+
and an external member's granted hostnames (on their own domain).
|
|
439
|
+
- **R5 (new)**: Authelia's device-code flow checks a client's policy
|
|
440
|
+
against the groups a session last refreshed, never re-reading them
|
|
441
|
+
(`handler_oauth2_device_authorization.go`, `handler_oauth2_consent.go` in
|
|
442
|
+
4.39.28). A narrowing that reaches a device-code client therefore
|
|
443
|
+
restarts Authelia, ending every session on every domain: membership
|
|
444
|
+
ended or made external on a domain serving one, or a grant on one
|
|
445
|
+
revoked. Corral's external members hold no such grant; Code Lama's
|
|
446
|
+
members leave `codelama.com.tr` rarely. To be reported upstream, so the
|
|
447
|
+
flow refreshes like the authorization-code one.
|
|
448
|
+
- External members can never hold `admins` (decision 4), so the
|
|
449
|
+
`[group:admins, …member@d]` clause cannot admit one, and Authelia's
|
|
450
|
+
`admins` bypass of static-site lists stays members-only.
|
|
451
|
+
|
|
452
|
+
## Proof (rodeo)
|
|
453
|
+
|
|
454
|
+
Written 2026-10-26 in `packages/tools/lamaste-rodeo` (not run yet; each
|
|
455
|
+
`.json` generated from its `.sh` with rodeo's `parseBashTest`, Prettier,
|
|
456
|
+
`testScriptSchema`-valid; `rodeo.config.json` lists 49–51 after 01 and
|
|
457
|
+
single-VM 36 after 03). New three-VM specs on top of
|
|
458
|
+
`multi-domain-harness.sh`, which gained `user_kinds`, `domain_group_key`,
|
|
459
|
+
`user_groups` / `wait_group` (users.yml read as root, groups only),
|
|
460
|
+
`authelia_pid`, `md_kind`, `md_grant`, `wait_status`,
|
|
461
|
+
`assert_access_request` and `signed_in`; `tests/e2e/helpers.sh` gained
|
|
462
|
+
`AUTHELIA_MEMBER_KEY`, `AUTHELIA_USERS_GROUPS_JSON`, a rules filter that
|
|
463
|
+
includes the external `domain_regex` rule, and
|
|
464
|
+
`write_oidc_grant_fixture_plugin`.
|
|
465
|
+
|
|
466
|
+
- **49 — an external member reaches only what names them** (B): the
|
|
467
|
+
external member holds `lamaste:x:<key>` alone; an `authenticated` tunnel →
|
|
468
|
+
access-request page, and still 403 with Gatekeeper's projection held to
|
|
469
|
+
call them a member (Authelia alone refuses); a `restricted` tunnel —
|
|
470
|
+
refused, a direct grant (users.yml gives the hostname group) → through,
|
|
471
|
+
revoked → refused within the settle time, a Lamaste-group grant → through,
|
|
472
|
+
left the group → refused; a protected static site listing the member
|
|
473
|
+
(listing users is structural, so before the baseline) → refused, its list
|
|
474
|
+
edited to name them (`lamaste:s:<site id>`, never the hostname) → through.
|
|
475
|
+
After every change the member's session of before still reaches the
|
|
476
|
+
tunnel and the site, both sessions stay signed in, Authelia's `MainPID`
|
|
477
|
+
never changes.
|
|
478
|
+
- **50 — kind changes**: a member on an authenticated tunnel, turned
|
|
479
|
+
external → the same session refused (access-request page), audited
|
|
480
|
+
`directory.membership.changed` by id, users.yml `m` → `x`; turned back →
|
|
481
|
+
through; making a user in `e2e50team` external → 409
|
|
482
|
+
`external_member_groups`; giving it to an external member → 409; a
|
|
483
|
+
hostname-like or `lamaste:` group name → 400 `reserved_group`; users.yml
|
|
484
|
+
holds the member group, `GET /api/users` and the backend's `Remote-Groups`
|
|
485
|
+
(an echo backend) carry `e2e50team` alone; no restart.
|
|
486
|
+
- **51 — a plugin's web surface and audience** (the corral case): an
|
|
487
|
+
identity + web-surface fixture moved to B, a member and an external
|
|
488
|
+
member entitled as viewers — both in the audience and the agent half's
|
|
489
|
+
projection with their role; an `authenticated` web surface: the member
|
|
490
|
+
reaches the shell and the API, the entitled external member gets the
|
|
491
|
+
access-request page and 403 `{error: forbidden}`; a `restricted` one:
|
|
492
|
+
refused without the site's grant (the member too), the grant → shell and
|
|
493
|
+
API with their directory id and role, revoked → refused; no restart.
|
|
494
|
+
- **single-VM 36 — OIDC by grant**: each client's policy is
|
|
495
|
+
`lamaste_client_<id>` (`m:<key>`, `x:<key>`+`oidc@<id>`); an
|
|
496
|
+
authorization-code client — the member admitted throughout, the external
|
|
497
|
+
member refused, granted → the same session admitted, revoked → refused,
|
|
498
|
+
no restart; a device-code client — revoking the external member's grant
|
|
499
|
+
restarts Authelia (audited `authelia.restarted` `device-code`, new
|
|
500
|
+
`MainPID`) and their session of before is gone.
|
|
501
|
+
- **Upgrade (`tests/e2e-upgrade/01`, extended rather than a new spec)**:
|
|
502
|
+
a user signed in on the previous release and admitted to an
|
|
503
|
+
authenticated tunnel; after the Panel Update every membership is `member`,
|
|
504
|
+
the projection is v2 with no external member, Authelia has `watch: true`,
|
|
505
|
+
`refresh_interval: always`, password change and reset off, rules on the
|
|
506
|
+
member group naming no user, every users.yml user holds it, and the user,
|
|
507
|
+
signed in again, is answered as before.
|
|
508
|
+
- **Existing specs updated** for groups instead of user subjects: three-VM
|
|
509
|
+
45, 46 (a removal now refuses the same session, no restart), 47 (members'
|
|
510
|
+
groups part of the snapshot), 40 (comments), single-VM 34 and 35 (per-client
|
|
511
|
+
policies), the domain listings' `{name, kind}`.
|
|
512
|
+
|
|
513
|
+
Not provable without running: Authelia's admission of external members
|
|
514
|
+
through the `(?P<Group>…)` capture on a real host; the access-request page
|
|
515
|
+
after Gatekeeper's identity probe; timing within rodeo's ten minutes (51
|
|
516
|
+
installs and hosts a plugin). The device-code approval itself is not driven
|
|
517
|
+
(it needs Authelia's consent UI); 36 proves the mitigation — the restart and
|
|
518
|
+
the ended session.
|
|
519
|
+
|
|
520
|
+
## Waves
|
|
521
|
+
|
|
522
|
+
1. **Core** — done. Root `owned-groups.ts` (browser-safe): `memberGroup`,
|
|
523
|
+
`externalGroup`, `oidcClientGroup`, `isLamasteOwnedGroup`,
|
|
524
|
+
`ADMIN_GROUP_NAME_RE` / `isAssignableGroupName`, `renderOwnedGroups`
|
|
525
|
+
(throws on a user both member and external of one domain, or a
|
|
526
|
+
non-username — never naming it) and `mergeOwnedGroups` (replaces exactly
|
|
527
|
+
the owned groups, keeps the administrator's in order, unchanged entries
|
|
528
|
+
stay the same objects). Root `domains.ts`: `MembershipKind`,
|
|
529
|
+
`MEMBERSHIP_KINDS`, projection v2 (`parseDomainsProjection` refuses v1,
|
|
530
|
+
unknown versions and a user of both kinds), `DomainsIndex.kinds`,
|
|
531
|
+
`domainMembership` → `{ domain, kind }`. Root `constants.ts`:
|
|
532
|
+
`OIDC_CLIENT_ID_RE`, `OIDC_CLIENT_ID_MAX_LENGTH`, `isOidcClientId` (the
|
|
533
|
+
manifest schema uses them). Server `authelia.ts`:
|
|
534
|
+
`renderGroupAccessControl`, `externalHostRegex`,
|
|
535
|
+
`renderOidcClientPolicies`, `oidcClientPolicyName`,
|
|
536
|
+
`ensureUsersFileWatched` (run by every `rewriteAutheliaConfig`),
|
|
537
|
+
`watch: true` in `writeAutheliaConfig`, and the 15-minute
|
|
538
|
+
`OIDC_USER_LIFESPAN` named by every user-facing client (rendered into
|
|
539
|
+
`lifespans.custom` only when such a client exists). Tests: serverd
|
|
540
|
+
`external-members-core.test.js`; `multi-domain-core` (projection v2),
|
|
541
|
+
`plugin-oidc` (lifespan), `domains` and Gatekeeper fixtures (v2).
|
|
542
|
+
|
|
543
|
+
**Deviations, for wave 2:**
|
|
544
|
+
- The new renderers sit **beside** 4.7's `renderAccessControl`,
|
|
545
|
+
`renderOidcAuthorizationPolicies` and `oidcAuthorizationPolicyName`,
|
|
546
|
+
which stay wired: switching before users.yml holds the owned groups
|
|
547
|
+
would deny everyone. Wave 2 switches `updateDomainAccess`,
|
|
548
|
+
`buildOidcProviderSection` and `renderOidcClient`'s
|
|
549
|
+
`authorization_policy` to them and removes the old three (4.7-only
|
|
550
|
+
names: edit `public-api.json` by hand). `renderGroupAccessControl` takes
|
|
551
|
+
the domain names only — the rules name groups, so who is in them does
|
|
552
|
+
not matter (the design's `{ name, hasMembers, hasExternals }` was not
|
|
553
|
+
needed).
|
|
554
|
+
- The projection is already v2 on both sides: the panel writes every
|
|
555
|
+
member as `member` and `externals: []` (lib/domains.js, until kinds
|
|
556
|
+
exist), and Gatekeeper admits only `kind === 'member'` (external
|
|
557
|
+
members are refused everywhere until wave 2 checks the kind per route).
|
|
558
|
+
- The user-facing lifespan applies from this wave on (harmless before
|
|
559
|
+
the switch: shorter tokens only).
|
|
560
|
+
|
|
561
|
+
2. **serverd + Gatekeeper** — done. state.db `0017_membership_kind.sql`
|
|
562
|
+
(`kind`, a trigger stamping `seq` on a kind change). `lib/domains.js`:
|
|
563
|
+
`MembershipInputSchema` (a name, or `{ name, kind }`),
|
|
564
|
+
`normalizeMemberships` (400 `conflicting_membership`), kinds through
|
|
565
|
+
`listDomainMembers`, `domainsByUser` / `domainsOfUser` (`{ name, kind }`),
|
|
566
|
+
`domainsOverview` (`externalMembers`), `domainsForNewUser`,
|
|
567
|
+
`insertMembershipsSync`, `addDomainMember(…, { kind })` (a kind change
|
|
568
|
+
audited `directory.membership.changed` `{ domain, userId, from, to }`),
|
|
569
|
+
`setUserDomains` (kinds), invitations (`invitationMemberships`; stored
|
|
570
|
+
4.7 strings read as members); `adminGroups` / `adminGroupsOf`,
|
|
571
|
+
`assertAssignableGroups` (400 `reserved_group`; a group the user already
|
|
572
|
+
holds is kept), `externalMemberGroupsError` / `assertNoGroupsForExternal`
|
|
573
|
+
(409 `external_member_groups` — 400 when an invitation or a new user asks
|
|
574
|
+
for both). `syncDomainAccess` renders, under the one writer: users.yml's
|
|
575
|
+
owned groups (`lib/owned-groups.js` `desiredOwnedGroups` → core
|
|
576
|
+
`renderOwnedGroups` → `lib/authelia.js` `writeOwnedGroups`), the
|
|
577
|
+
projection v2, then `updateDomainAccess` (cookies + `renderGroupAccessControl`
|
|
578
|
+
only — compare-first, so a restart only on a structural change or a
|
|
579
|
+
configuration not yet watching users.yml). The OIDC section renders one
|
|
580
|
+
policy per user-facing client (`renderOidcClientPolicies`; clients name
|
|
581
|
+
`lamaste_client_<id>`); 4.7's `renderAccessControl`,
|
|
582
|
+
`renderOidcAuthorizationPolicies`, `oidcAuthorizationPolicyName`,
|
|
583
|
+
`AutheliaDomain` are gone (`public-api.json` edited). `writeAutheliaConfig`
|
|
584
|
+
takes no members; onboarding renders before Authelia first starts.
|
|
585
|
+
`changeUsers` renders and busts Gatekeeper's sessions — it restarts
|
|
586
|
+
nothing (`renderDomains: false` replaces `restartAuthelia: false` for
|
|
587
|
+
onboarding). Every write of users.yml goes through one file lock in
|
|
588
|
+
`lib/authelia.js` (`writeUsers`, `writeOwnedGroups`; `createUser` and
|
|
589
|
+
`createUserFromInvitation` moved there from core's helpers), keeps the
|
|
590
|
+
owned groups the last rendering decided, and is written again
|
|
591
|
+
`USERS_FILE_TOUCH_DELAY_MS` (600 ms) after the last write unless the file
|
|
592
|
+
changed meanwhile. Users and identity APIs show no owned group; the users
|
|
593
|
+
API keeps them across an edit. Gatekeeper proxy: `oidc-client` grants
|
|
594
|
+
checked (`checkOidcClientGrant`: 400 `unknown_oidc_client` /
|
|
595
|
+
`service_oidc_client`), every change re-renders, and the users a revoked
|
|
596
|
+
client grant or a group change reached are checked for registry
|
|
597
|
+
revocation. `revokeRegistryTokensFor` asks `reachesRegistry`: a member of
|
|
598
|
+
a registry domain, or an external member of one granted a user-facing
|
|
599
|
+
client of it (Gatekeeper unreachable: no). `listPluginOidcClients` adds
|
|
600
|
+
`userFacing` and `grants`. Gatekeeper: `checkDomain` returns the kind,
|
|
601
|
+
`externalRefused` on every route (tunnel → access-request page,
|
|
602
|
+
`external_not_granted`; user surface → 403 `forbidden`; shell →
|
|
603
|
+
access-request page), `shownGroups` strips owned groups from
|
|
604
|
+
`Remote-Groups` and the assertion. Tests: serverd
|
|
605
|
+
`external-members.test.js` (kinds, refusals, hidden and kept groups,
|
|
606
|
+
host groups direct and through Lamaste groups, Gatekeeper unreachable,
|
|
607
|
+
tunnel changes, static sites, OIDC grants, registry revocations, the
|
|
608
|
+
touch), Gatekeeper `authz-external-members.test.js` (kind matrix,
|
|
609
|
+
stripping); `domains`, `identity-directory`, `plugin-oidc`,
|
|
610
|
+
`multi-domain-core`, `agent-hosted-plugins` updated (no restart per
|
|
611
|
+
change; groups instead of user subjects).
|
|
612
|
+
|
|
613
|
+
**Deviations, for review:**
|
|
614
|
+
- **The owned groups are always written first**, a site's removal too
|
|
615
|
+
(the design said configuration first there). A host group never
|
|
616
|
+
outlives the rule that narrows its host: a static site whose rule went
|
|
617
|
+
while its listed external member kept the host group would be opened to
|
|
618
|
+
them by their domain's external rule. Writing the groups first only
|
|
619
|
+
ever narrows in between.
|
|
620
|
+
- **Tunnel and site changes render on their own**: `lib/state.js`
|
|
621
|
+
`writeTunnels` / `writeSites` call `scheduleHostRender` (100 ms,
|
|
622
|
+
coalesced, only while some domain has external members) — every path
|
|
623
|
+
that changes a restricted host (creation, mode, release on revocation,
|
|
624
|
+
web surfaces) is covered without each route asking.
|
|
625
|
+
- **Gatekeeper unreachable**: the hosts and clients each external member
|
|
626
|
+
holds now are kept (a warning), rather than failing the step — a
|
|
627
|
+
revocation is a Gatekeeper write, so it cannot have happened; a
|
|
628
|
+
membership's own group still comes from state.db.
|
|
629
|
+
- Core `renderOwnedGroups` ignores a listed user or a grant's principal
|
|
630
|
+
who is none of the domain's people (it threw): a typo in a site's list
|
|
631
|
+
or a stale grant must not stop the whole rendering.
|
|
632
|
+
- The users lock moved to `lib/users-lock.js`; a kind change takes it
|
|
633
|
+
before the domains lock (never the other way round), so it and a
|
|
634
|
+
groups edit are serialized. The rendering takes neither: the file lock
|
|
635
|
+
and the owned-group cache keep a concurrent read-modify-write from
|
|
636
|
+
writing an older set back.
|
|
637
|
+
- **Left for wave 3**: every client of the changed shapes — the panel UI
|
|
638
|
+
and desktop show `domains` as objects now (`{ name, kind }`), the CLI
|
|
639
|
+
(`domains members`, `users list`), `AdminClient`'s types
|
|
640
|
+
(`AdminUser.domains`, members' `kind`, `externalMembers`, OIDC client
|
|
641
|
+
`grants`), the API reference (users, domains, invitations, Gatekeeper
|
|
642
|
+
grants with `oidc-client`, registry-and-oidc), CHANGELOG, CLAUDE.md.
|
|
643
|
+
|
|
644
|
+
3. **UI, desktop, CLI, docs** — done. Core `types.ts`: `DomainMembership`
|
|
645
|
+
/ `DomainMembershipInput` (`AdminUser.domains`, `domains` on user
|
|
646
|
+
creation, edits and invitations), `DomainView.externalMembers`,
|
|
647
|
+
`DomainMemberView.kind`, `addDomainMember(name, user, { kind })`,
|
|
648
|
+
`getPluginOidcClients` with `PluginOidcClientView` / `OidcClientGrantView`.
|
|
649
|
+
Web client and desktop client implement them (`admin_add_domain_member`
|
|
650
|
+
takes `kind`, checked before curl — `domain_member_body`, cargo-tested;
|
|
651
|
+
`admin_get_plugin_oidc_clients`). Panel: UI helpers `admin/lib/domains.js`
|
|
652
|
+
(`membershipsOf`, `isExternalAnywhere`, `membershipKind`,
|
|
653
|
+
`withMembership`, `sameMemberships`, `externalUserIds`,
|
|
654
|
+
`EXTERNAL_MEMBER_HINT`; tests), `useMembershipChoice` /
|
|
655
|
+
`DomainMembershipChoice` with a kind per domain (a one-domain relay shows
|
|
656
|
+
one **External member** checkbox and sends `domains` only when it is
|
|
657
|
+
ticked), `ExternalBadge` on users, invitations, domain members and a
|
|
658
|
+
plugin's audience, `DomainChips` marking external memberships; Users'
|
|
659
|
+
`GroupsField` closes for an external member and says to remove groups
|
|
660
|
+
first (the form refuses before asking the panel); the Domains page adds
|
|
661
|
+
members of either kind and switches a kind (**Make external** /
|
|
662
|
+
**Make member**) and counts external members; the plugin card's
|
|
663
|
+
`PluginOidcClients` (**OIDC sign-in**) grants a user-facing client to
|
|
664
|
+
external members of the plugin's domain or Lamaste groups and revokes.
|
|
665
|
+
CLI: `domains members <d> add <user> [--external]`,
|
|
666
|
+
`domains members <d> kind <user> member|external` (pure
|
|
667
|
+
`memberRequest`), kinds in `domains members`, `domains list` and
|
|
668
|
+
`users list` (`membershipsText`), `plugins oidc-clients <name> [grants]`
|
|
669
|
+
with each client's grants, `grant|revoke <clientId> --user|--group`
|
|
670
|
+
(`src/oidc-client-grants.js`); tests. Docs: `04-api-reference/domains.md`
|
|
671
|
+
(kinds, where enforced), `users.md` (no restart, kinds, groups Lamaste
|
|
672
|
+
owns), `registry-and-oidc.md` (per-client policies, client grants, 15-minute
|
|
673
|
+
tokens), `gatekeeper.md` (projection v2, the external-member matrix, OIDC
|
|
674
|
+
client grants), `01-concepts/security-model.md` (R3 narrowed, R4),
|
|
675
|
+
`02-guides/second-domain.md` (External members), and the restart
|
|
676
|
+
wording of `managing-users.md`, `state-management.md`,
|
|
677
|
+
`authentication.md`; CHANGELOG 4.7.0; CLAUDE.md.
|
|
678
|
+
|
|
679
|
+
**Deviations, for review:**
|
|
680
|
+
- One new `AdminClient` method, `getPluginOidcClients`: granting and
|
|
681
|
+
revoking go through the existing `createGatekeeperGrant` /
|
|
682
|
+
`revokeGatekeeperGrant` (the panel's proxy checks an `oidc-client`
|
|
683
|
+
grant and re-renders), not dedicated `grantOidcClient` /
|
|
684
|
+
`revokeOidcClientGrant` methods — one grant model, one path.
|
|
685
|
+
- The panel marks an external member of a plugin's audience from the
|
|
686
|
+
users list (their membership of the plugin's domain), not from the
|
|
687
|
+
audience rows, which do not carry the kind (decision 7).
|
|
688
|
+
- The CLI's `revoke` finds the grant through
|
|
689
|
+
`GET /api/plugins/:name/oidc-clients` and revokes it by id; its listing
|
|
690
|
+
now labels a client's OAuth grant types `flows:` (`grants:` is the
|
|
691
|
+
access grants).
|
|
692
|
+
|
|
693
|
+
4. **Security review** — done (2026-10-26, below): three independent
|
|
694
|
+
reviews, every confirmed finding fixed with a test that fails without
|
|
695
|
+
it; four decided with the user (S1 device code, S2 Authelia's own
|
|
696
|
+
writes, S3 grant sweep, S4 opaque names).
|
|
697
|
+
5. **Rodeo** — run on fresh Multipass VMs on 2026-10-26, this branch's 16
|
|
698
|
+
packages published to the dev Feria as 4.7.0 (on top of `aac7ce3`):
|
|
699
|
+
|
|
700
|
+
| Suite | Result |
|
|
701
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
702
|
+
| single-VM | 35/35 specs (36 — OIDC by grant, device-code restart — 35/35 checks) |
|
|
703
|
+
| three-VM | 51/51 specs: 49 (104 checks), 50 (54), 51 (71); 47 passed 62/62 on a rerun after the harness fix below |
|
|
704
|
+
| upgrade | 01 Panel Update from 4.6.0: 38/38 (memberships `member`, watch, `refresh_interval`, password change/reset off, member group on every user, same answers) |
|
|
705
|
+
|
|
706
|
+
**Harness fix.** Spec 47 first failed one sign-in: Authelia refused a
|
|
707
|
+
TOTP code as already used. `md_sign_in` waited for the next 30 s step by
|
|
708
|
+
the orchestrator's clock while `sign_in` makes the code on the visitor,
|
|
709
|
+
so a visitor a moment behind made the same code again. Steps are read
|
|
710
|
+
from the visitor's clock now (`md_visitor_seconds`).
|
|
711
|
+
|
|
712
|
+
## Security review (2026-10-26)
|
|
713
|
+
|
|
714
|
+
Three independent reviews (code, threat model, access-decision matrix) of
|
|
715
|
+
`21c092c..551f36a`, each given the code, not the design's conclusions. Each
|
|
716
|
+
confirmed finding was fixed with a unit test that fails without the fix.
|
|
717
|
+
|
|
718
|
+
| # | Finding | Fix | Test |
|
|
719
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
720
|
+
| S1 | Authelia's device-code flow checks a client's policy against the groups a session last refreshed (fact 3 holds for the authorization-code path only): with no restart, someone narrowed could approve a `feria login` for the session's life. | **Decided: targeted restart.** The rendering measures every narrowing against the last complete rendering; when an enabled user lost a group admitting them to a device-code client (core `reachedOidcClients`), Authelia restarts (audited `authelia.restarted` `device-code`) and Gatekeeper's sessions are busted. Unreadable clients → restart (fail closed). Residual R5. | external-members (made external, grant revoked: restart; web client, host grants: none), external-members-core (reach) |
|
|
721
|
+
| S2 | Authelia's own password change/reset writes users.yml from its in-memory copy, which may predate a skipped reload: revoked groups written back, repeatedly by someone changing their password. | **Decided: disabled.** `authentication_backend.password_change.disable` and `password_reset.disable` rendered (they could not work: the notifier is a file) and migrated on every rewrite, with `refresh_interval: always`; `usersFileWatched` requires all four. | external-members-core (settings, each one required) |
|
|
722
|
+
| S3 | Gatekeeper created tunnel, site, agent-plugin and OIDC-client grants consumed, and its sweep deleted them 90 days later, silently. | **Decided: fixed in 4.7.0 itself** (`ed6f931` on the PR branch, with the same fix for core's user-plugin-access store in `9767c38`). Grants are created unused; the sweep deletes consumed local plugin grants only; Gatekeeper migration 0004 un-consumes the others. | gatekeeper grants-retention |
|
|
723
|
+
| S4 | The `groups` claim told an OIDC relying party every domain a user belongs to (another tenant's name) and static-site hostnames. | **Decided: opaque names** (decision 8 revised). `lamaste:m:<key>` / `lamaste:x:<key>`, a random key per domain record (state.db 0018), `lamaste:s:<site id>`; hostname groups only for external members on Gatekeeper-gated hosts. | external-members-core (names, rules name no domain), domains, external-members |
|
|
724
|
+
| S5 | Revocations relied on Authelia watching users.yml, unchecked; an explicitly set `refresh_interval` would lag them. | The settings are rendered and checked (`autheliaWatchesUsersFile`); a rendering restarts Authelia (`unwatched`) after users.yml changed while it does not hold the file so. Start renders the domains before the OIDC clients (an upgrade's first policies name filled groups). | external-members (unwatched restarts once a change, not otherwise) |
|
|
725
|
+
| S6 | A failed rendering left the owned-groups cache stale; a username deleted and taken again got the deleted user's groups (static sites, OIDC — no Gatekeeper there). | A write adds owned groups only to users the rendering knew (`known`), forgets a deleted user's, and only narrows after a failed rendering (`narrowOnly`, core `mergeOwnedGroups` options). | external-members-core (merge options), external-members (reused username, narrow only) |
|
|
726
|
+
| S7 | Gatekeeper unreachable: external members kept the host and OIDC groups they held — a revocation Gatekeeper took just before stayed in Authelia (OIDC has no second gate). | Fail closed: no granted host or client group until the grants can be read; the membership groups are rendered all the same; the step counts as failed (audited, notified). A grant revoked meanwhile is found lost by the next complete rendering. | external-members (unreachable: groups dropped, audited, restored) |
|
|
727
|
+
| S8 | A grant change whose rendering failed answered success with no warning. | The proxy answers `warnings` and renders again within 5 s (`scheduleRenderRetry`). | external-members (warnings on a failed rendering) |
|
|
728
|
+
| S9 | Registry tokens were kept when the user reached any user-facing client of the registry's domain. | Only the registry's own sign-in clients count — user-facing clients whose audience is the registry; none or unreadable → revoked. Revocations follow every narrowing (a revocation with Gatekeeper's own CLI too, found by the tick). | external-members (another client's grant revoked, the CLI revocation), domains |
|
|
729
|
+
| S10 | `oidc-client` grants outlived their plugin's uninstall: a later install of the same client id admitted whoever was granted. | Uninstall revokes every grant on the plugin's clients (`revokeOidcClientGrants`). | external-members (uninstall) |
|
|
730
|
+
| S11 | The touch skipped a file something else had written; a skipped reload or a panel restart within 0.6 s of a write left Authelia on old groups. | The touch re-applies the owned groups to whatever the file holds and always writes; the start and the identity tick touch too. | external-members (a hand edit gets the groups back) |
|
|
731
|
+
| S12 | Ownership compared with JavaScript lower case; Authelia folds with Go's `EqualFold` (`ſ`, Kelvin sign). | `foldGroupName`: NFKC, then lower case. | external-members-core (look-alike spellings owned) |
|
|
732
|
+
| S13 | Host groups of a removed domain became "admin groups": shown, passed in `Remote-Groups`, blocking a kind change. | Removed domains are kept in `retired_domains`; hostnames under them stay owned and are stripped. | external-members (retired domain) |
|
|
733
|
+
| S14 | The CLI's `revoke` removed only the first of several grants naming the principal. | `findClientGrants` returns every match; all are revoked. | cli oidc-client-grants |
|
|
734
|
+
| S15 | An external member Authelia refused (no host group) got the domain refusal ("not a member") and a user surface `not_a_member`. | The portal rule admits members and external members on `auth.<d>`; on Authelia's 403 Gatekeeper asks once more for `https://auth.<d>/` and, for an external member, answers the access-request page / `forbidden`. Never cached; the request stays refused. The desktop's `authorize` (the one forward-auth on the portal) refuses external members itself. | gatekeeper authz-external-members, authz-domain-membership; external-members-core (portal rule) |
|
|
735
|
+
| S16 | A web surface moved off a tunnel left the disabled tunnel's grants in the host group. | `restrictedHosts` skips disabled tunnels (and agent panels). | external-members (disabled tunnel) |
|
|
736
|
+
| S17 | A tunnel without an access mode (restricted for Gatekeeper) got no host group: the two gates disagreed. | Absent or unknown modes are restricted, as Gatekeeper reads them. | external-members (mode-less tunnel) |
|
|
737
|
+
| S18 | An empty or partial users.yml read could be written back, dropping users. | A write re-reads an empty file before believing it and refuses to drop a user it was not told to delete (`deleting`). | external-members (empty read, undeclared drop) |
|
|
738
|
+
| S19 | Narrowings made past the panel (Gatekeeper's CLI) were neither audited nor followed; Gatekeeper's secret compared with `!==`. | Every narrowing found by a rendering is audited (`directory.access.narrowed`, id and count) and followed (registry, device code); the secret is compared in constant time (`secretMatches`). | external-members (audit), gatekeeper grants-retention (secret) |
|
|
739
|
+
| S20 | Desktop user access (primary domain) admitted external members and kept a member made external signed in. | `authorize`, `exchange` and every session request require a `member` of the primary domain (`isPrimaryMember`). | external-members (desktop session) |
|
|
740
|
+
|
|
741
|
+
The apex of a domain is never a Gatekeeper-gated host, so the external
|
|
742
|
+
host rule not matching it (a reviewer's note) refuses nothing that exists.
|
|
743
|
+
|
|
744
|
+
**Residual, documented in `01-concepts/security-model.md`:** R3 narrowed
|
|
745
|
+
(structural changes restart; a device-code narrowing too — R5), R4
|
|
746
|
+
narrowed (opaque groups; the relation "these users share a domain", client
|
|
747
|
+
ids and an external member's granted hostnames remain), R5 (Authelia's
|
|
748
|
+
device flow does not refresh groups — to be reported upstream).
|
|
749
|
+
|
|
750
|
+
## Decisions (agreed 2026-10-26)
|
|
751
|
+
|
|
752
|
+
The user agreed to every recommendation below; the alternatives are kept for the record.
|
|
753
|
+
|
|
754
|
+
1. **How membership changes avoid restarts.**
|
|
755
|
+
- **(a) Recommended:** Lamaste-owned groups in `users.yml` with
|
|
756
|
+
`watch: true`; rules in `configuration.yml` name groups (this design).
|
|
757
|
+
Removes the restart from every per-person change, for members too, with
|
|
758
|
+
no new component. Revises 4.7's decision 8 (membership as `user:`
|
|
759
|
+
subjects) before 4.7 is released.
|
|
760
|
+
- (b) Keep `user:` subjects and move Authelia's sessions to **Redis**
|
|
761
|
+
(local unix socket, ACL, ~5–10 MB): restarts no longer sign anyone out,
|
|
762
|
+
but every change still restarts Authelia (a few seconds of refused
|
|
763
|
+
authorizations each time, on every domain), and adds a service to
|
|
764
|
+
install, harden and update.
|
|
765
|
+
- (c) Both: (a) now, Redis later to remove the remaining structural
|
|
766
|
+
restarts. _Recommended as a follow-up_, not in this feature.
|
|
767
|
+
2. **Invitation and membership shape.** _Recommended:_
|
|
768
|
+
`domains: [{ name, kind }]` everywhere 4.7 has `domains: string[]`
|
|
769
|
+
(users, invitations, the members API returns objects); a bare string is
|
|
770
|
+
still accepted as `member` on input, so 4.7 clients and specs keep
|
|
771
|
+
working. Alternative: a separate `externalDomains: string[]`.
|
|
772
|
+
3. **What the Authelia host group holds for a restricted host.**
|
|
773
|
+
_Recommended:_ external members **with a grant** on it (Authelia enforces
|
|
774
|
+
grants for external members too; a grant made with Gatekeeper's own CLI
|
|
775
|
+
reaches Authelia within a minute). Alternative: every external member of
|
|
776
|
+
the domain on every restricted host (simpler; Authelia then enforces only
|
|
777
|
+
member/external, Gatekeeper alone the grant).
|
|
778
|
+
4. **Authelia groups.** _Recommended:_ a user who is external on **any**
|
|
779
|
+
domain holds no Authelia group (relay-wide; groups are relay-wide).
|
|
780
|
+
Alternative: per domain — groups kept, but every rule ANDs them with
|
|
781
|
+
the domain's member group (Authelia) and Gatekeeper ignores them for external
|
|
782
|
+
hosts; leaves `feria-*` reaching Feria through OIDC's `groups` claim for a
|
|
783
|
+
user external on Feria's domain, so not recommended.
|
|
784
|
+
5. **OIDC grant model.** _Recommended:_ Gatekeeper grants with
|
|
785
|
+
`resourceType: 'oidc-client'` (one grant model, Lamaste groups apply,
|
|
786
|
+
proxied API exists), one authorization policy per user-facing client.
|
|
787
|
+
Alternative: a panel-side `oidc_client_grants` table (no Lamaste groups).
|
|
788
|
+
6. **OIDC token lifespan.** _Recommended:_ 15 minutes for access and ID
|
|
789
|
+
tokens of user-facing plugin clients (a custom lifespan the clients name),
|
|
790
|
+
bounding what survives a revocation; `feria login` exchanges its token at
|
|
791
|
+
once, so it is unaffected. Alternative: keep Authelia's default (1 h) and
|
|
792
|
+
document it.
|
|
793
|
+
7. **Does the plugin learn the kind?** _Recommended: no_ in this feature —
|
|
794
|
+
the plugin's roles express what a person may do there, and the identity
|
|
795
|
+
feed stays protocol 1 (4.5 agent halves unchanged). Alternative: an
|
|
796
|
+
optional `membership: 'member'|'external'` attribute in audience changes
|
|
797
|
+
(a manifest opt-in, protocol 2).
|
|
798
|
+
8. **R4 — Lamaste-owned groups in the OIDC `groups` claim.** _Recommended:_
|
|
799
|
+
accept and document (readable names, public-anyway hostnames; only
|
|
800
|
+
admin-installed relying parties; Feria matches `feria-*` only).
|
|
801
|
+
Alternative: opaque names (`lamaste:m:<random id>`) for the member and
|
|
802
|
+
external groups — hides which domains, but host groups must stay
|
|
803
|
+
hostnames (fact 4), and the panel and logs get harder to read.
|
|
804
|
+
**Revised 2026-10-26 (security review S4, the user's decision): opaque
|
|
805
|
+
names** — member, external and site groups; hostname groups only for
|
|
806
|
+
external members on Gatekeeper-gated hosts.
|
|
807
|
+
9. **User and web surfaces for external members.** _Recommended:_ as the
|
|
808
|
+
plan says — only `restricted` surfaces with the surface's grant; a plugin
|
|
809
|
+
entitlement (role) alone does not admit them to an `authenticated`
|
|
810
|
+
surface. Alternative: an entitlement counts as "naming them" on an
|
|
811
|
+
`authenticated` surface (fewer grants to manage for corral, but a second
|
|
812
|
+
meaning of `authenticated`).
|
|
813
|
+
10. **Version.** _Recommended:_ fold into **4.7.0** (unreleased; the group
|
|
814
|
+
model replaces 4.7's user subjects before anyone runs them, and the
|
|
815
|
+
upgrade restarts once instead of twice). Alternative: 4.8.0 after 4.7.0
|
|
816
|
+
ships.
|