Technicians and client scopes¶
Tenantry runs the Teleport side of many clients. A technician who looks after two clients must not see, export or act on the other forty. This page describes how Tenantry limits each operator to his clients, and how the same convention limits his RDP access in Teleport.
1. Access levels¶
| Who | How he signs in | What he sees and does |
|---|---|---|
| MSP administrator | Teleport SSO with a role of TELEPORT_JWT_ADMIN_ROLES, a user of TELEPORT_JWT_ALLOWED_USERS (break-glass), or the local password login |
Everything: every client (registered or not), global settings (alert channels, Technicians), cluster-wide actions (Steward integration, Teleport cluster bundle, join tokens for new proxies, creating Teleport roles). |
| Scoped technician | Teleport SSO only, with any other allowed role, or only a tenantry-client-<code> role |
Only the registered clients of his scope, at a level per client: read only (viewer) or operator. |
The local password login is the administrator's break-glass access: it is always an administrator and never scoped. A technician always comes through Teleport (the JWT carries his user name and roles, see 06 §8.1).
Before this feature, an allowed Teleport role outside
TELEPORT_JWT_ADMIN_ROLEScould read every client. It is now a scoped technician who sees nothing until clients are assigned to him (deny by default). Check your role lists when you upgrade (section 5).
2. Where a technician's clients come from¶
A technician's scope is the union of:
- Assignments in Tenantry — Settings › Technicians (
/settings/technicians, administrators only): a Teleport user name (exact, case-sensitive) and a list of client codes, each with a level. Stored in/data/technicians.json(same atomic, versioned, quarantined-if-unreadable store as the client register), every change audited (technician.create,technician.update,technician.delete). - Teleport roles of the convention
tenantry-client-<code>present in the JWT, whenTELEPORT_JWT_CLIENT_ROLESis on (default). Such a role grants the levelTELEPORT_JWT_CLIENT_ROLE_LEVEL(defaultviewer). Holding one is enough to sign in (as a technician, never as an administrator), even without a role ofTELEPORT_JWT_ALLOWED_ROLES.
…intersected with the client register: a client code that is not registered is visible to administrators only, even to
the holder of tenantry-client-<that code>. When both sources name the same client, the stronger level wins.
The scope is computed again on every request (the Teleport JWT is verified per request, there is no session): an assignment change, a new role or a deleted client takes effect at the technician's next click. Deleting a client also removes it from every technician, so a client re-created later with the same code does not silently inherit old assignments.
The header always shows the current scope: Clients: acme, demo-sa for a technician (hover for the level of each),
Clients: all for an administrator. GET /api/auth/me returns it as scope ({ all: true } or
{ all: false, clients: [{ code, level, sources }] }).
Levels¶
| Level | Allows |
|---|---|
viewer (read only) |
Proxies and their desktops (list, deep link, inventory CSV export), the client page, its consoles and their status, its alerts (active and history), its audit entries, the guided-install status of its deployments. |
operator |
Everything above, plus: lock / unlock a proxy of the client, edit the client's register entry (name, notes, contacts), edit a console of the client (client code, new token; not its URL or CA) and Poll now, validate and preview a proxy deployment for the client, act on its deployments (manual AD output, finish, desktop check that revokes the token), generate a Steward instance bundle for the client (clientCode in the body). |
Always administrators only: create / delete a client, add / remove a console (a new outbound target for Tenantry), change a
console's URL or CA (where its token is sent), alert settings and channels (read included), Technicians, Steward integration
(/api/steward/*), Teleport cluster bundle, every join token: generating a proxy deployment (/api/deployment/generate)
and tokens for a new proxy (/api/bundles/proxy-token*), creating the per-client Teleport role in the cluster,
GET /api/audit/verify.
Why join tokens are administrator only. The labels of a Teleport join token (
client_code=acme) are labels of the token: they do not bind the agent that joins with it, which advertises whatever labels its own configuration says. Awindowsdesktoptoken created "for acme" can enrol a desktop service claiming any client. Client scoping cannot constrain that, so only administrators create tokens. An operator still follows the guided install of a deployment an administrator generated for his client.
3. Enforcement (server side)¶
Everything is enforced by the backend; the interface only hides what would be refused. One central guard,
packages/backend/src/auth/access.ts, is used by every route:
- the authentication guard resolves
req.accesswith the identity (fail closed: an unreadable state gives no client); - reads are filtered:
/api/nodes(and its summary counts),/api/nodes/inventory,/api/clients,/api/consoles(also with?clientCode=of another client),/api/alerts(active and history; an alert without a client — Tenantry identity — is for administrators),/api/audit(entries filed under one of his clients, plus his own actions),/api/teleport/gateways. A proxy without a client code (the master, an unlabelled agent) is visible to administrators only; - a resource addressed by id whose client is out of scope answers 404 exactly like an unknown id: deep links
/proxies/:id,/api/nodes/:id, lock / unlock,/api/consoles/:id,/api/deployment/:id/*,/api/clients/:code; - an action that names its client (deployment body, console moved to another client, Steward instance bundle) answers
403 client_scope; a client that cannot be determined is refused (deny by default); - the
viewerlevel on a visible client answers403 insufficient_levelto an action; - every refusal is audited as
auth.forbidden(route, required level, client).
The checks run before any side effect: a refused lock never reaches tctl, a refused deployment creates no token.
There is no SSE or WebSocket channel; the polling endpoints are the same filtered GET routes.
packages/backend/test/scoping.test.ts exercises every route with an administrator and five technicians (operator, viewer,
convention role only, role of an unregistered client, nothing), and fails if a new /api route is not classified
(public, any signed-in user, administrators only, scoped).
Audit entries carry their client¶
Entries now carry an optional client field (client code), included in the HMAC chain when present: older entries keep
their digest and still verify; changing or removing the client of a new entry breaks the chain. A technician's view of the
audit log is computed from it (most recent 5 000 lines scanned).
4. Teleport side: the per-client role tenantry-client-<code>¶
The same name scopes RDP access in Teleport. Tenantry renders, for each client:
kind: role
version: v7
metadata:
name: 'tenantry-client-acme'
description: 'Tenantry technician scope: client acme'
labels:
tenantry-managed: 'true'
client_code: 'acme'
spec:
allow:
app_labels:
client_code: 'acme'
windows_desktop_labels:
client_code: 'acme'
windows_desktop_logins:
- '{{internal.windows_logins}}'
It grants RDP to the desktops labelled client_code: acme (the label every proxy deployed by Tenantry puts on its desktops)
with the user's own windows_logins trait, and the Teleport applications labelled client_code: acme (the client's Steward
console, published as acme-steward with that label by the Steward page), and nothing else. Where to get it:
- Clients ›
› Technician access : the YAML with a copy button (anyone in scope); administrators also get Create in Teleport (POST /api/clients/:code/teleport-role, auditedclient.role.apply). Tenantry never replaces a role of the same name it did not create (409 would_overwrite, then an explicit Replace it). Tenantry's own Teleport role needscreateonrolefor this (it already has it for the Steward integration;updatetoo to refresh an existing one). - At the end of a proxy deployment (guided install), with the command to create it.
- By hand:
tctl create -f tenantry-client-acme.yaml.
Assigning it¶
# Local Teleport users: keep their other roles (the list replaces the current one)
tctl users update jdoe --set-roles=access,tenantry-tech,tenantry-client-acme,tenantry-client-demo-sa
With SSO, map it from the connector (attributes_to_roles for SAML, claims_to_roles for OIDC, teams_to_roles for GitHub),
for example one IdP group per client: { name: groups, value: "msp-acme", roles: ["tenantry-client-acme"] }. Access Requests
can also grant it temporarily (request: roles: ["tenantry-client-*"] in a requester role).
The technician also needs to reach Tenantry itself: Teleport only proxies the Tenantry app to users whose roles allow its
app_labels. Give technicians a common role that grants the Tenantry application (e.g. tenantry-tech, also listed in
TELEPORT_JWT_ALLOWED_ROLES if you prefer explicit sign-in rights) — the per-client roles grant only that client's desktops and applications (client_code label), never the Tenantry application.
5. Configuration¶
| Variable | Default | Effect |
|---|---|---|
TELEPORT_JWT_ADMIN_ROLES |
empty | MSP administrators, explicit list only (never derived from the allowed roles). Empty: no Teleport role is an administrator; break-glass users and the local password login still are. install.sh asks for it (--sso-admin-roles). |
TELEPORT_JWT_ALLOWED_ROLES |
— | Roles allowed to sign in; those outside TELEPORT_JWT_ADMIN_ROLES are scoped technicians. |
TELEPORT_JWT_CLIENT_ROLES |
true |
Honour tenantry-client-<code> roles (sign-in and scope). false: only assignments count, and such a role alone no longer signs in. |
TELEPORT_JWT_CLIENT_ROLE_LEVEL |
viewer |
Level granted by a convention role alone (viewer or operator). Assignments can raise it per user. |
Typical setup: TELEPORT_JWT_ALLOWED_ROLES=tenantry-admin,tenantry-tech, TELEPORT_JWT_ADMIN_ROLES=tenantry-admin; MSP admins
get tenantry-admin, technicians tenantry-tech plus their tenantry-client-<code> roles; operator rights per client are
given in Settings › Technicians.
6. API¶
| Route | Who | Purpose |
|---|---|---|
GET /api/auth/me |
signed in | { username, method, admin, scope } |
GET /api/technicians |
admin | assignments, plus clientRolesEnabled and clientRoleLevel |
PUT /api/technicians |
admin | create or replace { username, clients: [{ code, level }] } (registered codes only: 400 unknown_client) |
DELETE /api/technicians/:username |
admin | remove the assignments |
GET /api/clients/:code/teleport-role |
viewer of the client | { name, yaml } |
POST /api/clients/:code/teleport-role |
admin | create / refresh the role in the cluster ({ overwrite: true } to replace an unmanaged one) |
7. Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| A technician sees "Clients: none" | No assignment, convention role absent from his JWT, or client not registered | Assign him in Settings › Technicians; check tctl get users/<user> roles; register the client. |
| A technician sees every client | His role is in TELEPORT_JWT_ADMIN_ROLES |
Set TELEPORT_JWT_ADMIN_ROLES to the administrators' role only. |
| An MSP administrator signs in as a technician without clients | TELEPORT_JWT_ADMIN_ROLES is empty or misses his role (it no longer defaults to the allowed roles) |
Add the administrators' role to TELEPORT_JWT_ADMIN_ROLES (or re-run install.sh --sso-admin-roles ROLE). |
403 admin_required when saving a console |
An operator changed the console's URL or CA | Ask an administrator: the endpoint decides where the console token is sent. |
403 at sign-in with only tenantry-client-* |
TELEPORT_JWT_CLIENT_ROLES=false, or a role name that does not match a valid code (tenantry-client-ACME) |
Enable it, or add an allowed role; codes are lowercase letters, digits and hyphens. |
403 insufficient_level |
Read-only level on that client | Raise it to operator in Settings › Technicians (or TELEPORT_JWT_CLIENT_ROLE_LEVEL=operator). |
403 client_scope |
The request names a client outside the scope | Expected; ask an administrator. Logged as auth.forbidden. |
404 on a proxy link sent by a colleague |
The proxy belongs to another client | Expected (an out-of-scope resource answers like an unknown one). |
| RDP works in Teleport but not for the expected desktops | The desktops do not carry client_code=<code> |
Proxies deployed by Tenantry label them; for others, add the label to the windows_desktop_service configuration. |