Skip to content

Teleport sign-in

Teleport is the recommended way for MSP technicians to reach client consoles. Technicians sign in once to the MSP's Teleport (with its second factor, or through the MSP's identity provider), launch the client's console, and are in: no button to click, no password typed into the console, no VPN, and no console port exposed to the Internet. Client power users on the client's LAN can keep signing in with their AD account on the same instance.

Tenantry generates the Teleport side

Tenantry, the MSP control plane, generates the Teleport configuration for each client console (application, labels, roles) and registers the console in the fleet view. If you use it, follow its documentation for the Teleport side and only do the Console side below. The rest of this page explains what is generated and how the console uses it.

How it works: the console trusts only the JWT that Teleport signs into the Teleport-Jwt-Assertion header, and checks everything about it:

  • the signature, against the cluster's published keys (RS256 or ES256 only);
  • the issuer, which must be the cluster name;
  • the audience, which must be the app URI;
  • the expiry and not-before times (with ClockSkew, 30 seconds by default);
  • the roles and traits claims, read strictly: a malformed claim makes the whole token unusable.

The console ignores every other identity header. Signing in through Teleport grants no permission by itself: rights come from role mappings (Teleport role → console role) or from roles an administrator assigns by hand.

Install the instance with --exposure teleport (or choose "Through Teleport" in the guided install): the installer then prints these steps at the end. See Exposure for how this compares with the other ways to reach a console.

Teleport side

Register the console as an application, with a label the roles can select:

app_service:
  enabled: true
  apps:
    - name: steward
      uri: https://steward.client-example.ch:443   # how the Teleport agent reaches the console
      insecure_skip_verify: false                  # keep false: the console has a trusted certificate (see HTTPS certificate)
      labels:
        app: steward

Create one Teleport role per kind of console user. They only grant access to the app; the console maps them to its own roles:

kind: role
version: v7
metadata:
  name: steward-admin
spec:
  options:
    require_session_mfa: true      # optional: WebAuthn at every launch of the app (see "Step-up" below)
  allow:
    app_labels:
      app: steward
---
kind: role
version: v7
metadata:
  name: steward-helpdesk
spec:
  allow:
    app_labels:
      app: steward

Allow WebAuthn as a second factor (passkeys, Windows Hello, security keys), next to OTP if you still use authenticator apps:

auth_service:
  authentication:
    type: local
    second_factors: ["otp", "webauthn"]
    webauthn:
      rp_id: teleport.msp.example     # the Teleport proxy host name

No Teleport password either. Teleport can delegate sign-in to an identity provider: GitHub SSO in Teleport Community Edition, OIDC (Entra ID, Okta, Google...) and SAML in Teleport Enterprise. The identity provider's groups become Teleport roles or traits, which the console maps in turn.

Console side

Set these in <instance>/config/steward.yaml (see deploy/steward.example.yaml), or as STEWARD_Authentication__Teleport__* environment variables, then restart the console. A changed config/steward.yaml does not make docker compose up -d recreate the container (the file is a mounted volume, the service definition is unchanged): use docker compose restart steward, or re-run install.sh:

Authentication:
  Teleport:
    Enabled: true
    Issuer: "<cluster name>"                                       # JWT "iss"
    Audience: "https://steward.client-example.ch:443"             # EXACTLY the app "uri" above (JWT "aud")
    JwksUrl: "https://<proxy public address>/.well-known/jwks.json"
    # JwksCaFile: "/config/teleport-ca.pem"                        # only if the proxy certificate is not publicly trusted
    # ClockSkew: "00:00:30"
    # LaunchUrl: "https://<proxy>/web/launch/<app public addr>/<cluster>/<app public addr>"   # optional, informative only
    # StepUp: Session                # default: the Teleport session is the step-up. Deny: Teleport operators never make step-up-gated changes
  ActiveDirectory:
    Enabled: true            # host maximum; the "Teleport only" setting can switch password sign-in off on top of it

Audience must be the application uri, character for character

Teleport signs the token with the uri of the application as audience. If the uri changes (a different address, port or scheme), Audience must change with it, or every sign-in fails with "The Teleport identity could not be verified" and the container log says Teleport JWT rejected (SecurityTokenInvalidAudienceException). Also open the console from Teleport: a direct request to the console has no Teleport token and gives the same message.

config/ is mounted read-only at /config in the container, so a JwksCaFile placed in <instance>/config/ is read as /config/<file>.

On a fresh instance reached only through Teleport, open the setup wizard through Teleport: the first administrator is the Teleport user who completes it.

Seamless sign-in

When the browser asks for GET /api/me without a console session but with a valid Teleport JWT, the console signs the operator in (JWT checked, a disabled operator refused, roles synced) and redirects to /api/me?sso=1, which the browser follows with the new session cookie (no loop if cookies are blocked). The sign-in is audited as auth.login with the attribute sso.

  • Unknown Teleport user: the console creates the operator with no role, then applies the mappings. With no matching mapping, the operator lands on a "No access yet" page that shows the Teleport roles received and what to ask an administrator. "Check again" signs in again with the current Teleport identity, so a new mapping applies at once.
  • Disabled operator: no session; the login page says that this Teleport account is disabled in the console.
  • Sign out: ends the console session, then the Teleport application session (/teleport-logout). The next visit goes through Teleport again.

Role mapping (provisioning from Teleport)

Delegations › Teleport lists the mappings. A mapping has a Teleport role, or a trait (name = value), or both (both must match), and a console role. Saving needs delegations.manage and a step-up, replaces the whole set, and is audited (teleport.mappings_changed). A mapping onto a powerful role (one that grants delegations.manage, settings.manage or vip.manage) is audited a second time as teleport.mappings_powerful: whoever can give that Teleport role then holds those console rights.

  • At every Teleport sign-in the operator's mapped roles follow the JWT: added or removed, audited as operators.teleport_roles_changed. The same sync runs at revalidation (every 15 minutes, and when a newer Teleport app session shows up).
  • Teleport roles change at Teleport sign-in only: after a role change in Teleport, the user signs out of Teleport and back in, then launches the app (the "No access yet" page says so).
  • Removing a mapping removes the roles it granted at once, for every Teleport operator. Adding one applies at each operator's next sign-in: a grant is never made from old claims.
  • Manual roles stay. Roles assigned by hand are never touched by the mapping. The Operators tab shows mapped roles as "via Teleport"; ticking such a role in the operator's role dialog makes it manual too.
  • AD operators are never affected. Only Teleport operators are synced.
  • Fail closed: a roles claim that is not an array of names, or a traits claim that is not an object of string arrays, makes the token unusable. A missing claim is an empty set (mapped roles are removed).
  • Last administrator: the console never ends up with no enabled delegations.manage holder. A mapping change that would leave none is refused (409 last_admin). If Teleport takes the role away from the last manager, the console keeps it and audits a warning (operators.teleport_last_admin_kept) until another manager exists. Keeping one administrator assigned by hand is the simplest.
  • Deleting a console role deletes its mappings too (audited teleport.mappings_removed).

Step-up: the Teleport session is enough

Sensitive changes (granting powerful rights, editing protection lists, mappings, templates or sign-in settings, resetting, disabling or enabling an account...) need a recent step-up. A Teleport operator is never asked for one: Teleport already enforced its second factor at sign-in, and the console session is bound, on every request, to the JWT Teleport signs for that same user (a request without it, or with another user's, ends the session).

  • Audited: each such change also writes auth.reauth, result succeeded, reason stepup:teleport, with the Teleport subject and the same correlation id as the change. It is written only when the change goes through.
  • No console authenticator for Teleport operators: the Security page says "Confirmed by Teleport"; enrolling one is refused (400 totp_ad_only).
  • Stricter option: Authentication:Teleport:StepUp: Deny forbids Teleport operators every step-up-gated change (the dialog explains; POST /api/auth/reauth answers 403 teleport_stepup_denied); those changes stay with AD operators and their authenticator code. In "Teleport only" mode, Deny means nobody can make them.
  • The former keys TrustSessionMfa and StepUpMaxAge are ignored (a startup warning names each one still present): see Upgrade notes.
  • AD operators of the same instance confirm with an authenticator code: see Security model.

Residual risk: an unattended, open Teleport session can make sensitive changes without a second confirmation. Mitigations:

  • keep Teleport sessions short: max_session_ttl on the roles that grant the console, and Teleport's idle timeout (client_idle_timeout, with disconnect_expired_cert); the console session also ends after 30 minutes idle;
  • optionally require_session_mfa: true on those roles, so each launch of the app asks for WebAuthn (passkey, Windows Hello, security key; Teleport refuses OTP for per-session MFA);
  • lock workstations, and review auth.reauth rows with reason stepup:teleport in the audit.

"Teleport only": nobody types a password

Configuration › Sign-in shows what the host allows and the instance setting Sign-in through Teleport only. When on:

  • AD password sign-in is refused (403 password_disabled, no bind is attempted), and every AD session ends at once;
  • the console serves nothing but /health/* without a valid Teleport JWT;
  • the login page offers only the Teleport path.

Changing it needs settings.manage and a step-up, and is audited (settings.teleport_only). It can only be switched on from a Teleport session (proof that Teleport sign-in works) and only when Teleport is enabled on the host. It never enables what the host disabled: Authentication:ActiveDirectory:Enabled: false keeps password sign-in off whatever the setting says.

Recovery (Teleport broken, nobody can sign in), on the host:

docker exec <container> dotnet Steward.Api.dll allow-password-login --confirm

It switches the setting off (audited off:recovery); password sign-in works again within 5 seconds if the host configuration allows it. If the host itself disables AD sign-in, change config/steward.yaml instead.

Blocking access that bypasses Teleport

  • Network rule (required): only the Teleport agent may reach the console's HTTPS port. Use a firewall, a Docker network, or bind the port to an internal address. See Exposure.
  • Teleport only: nothing except /health/* is served without a valid JWT.
  • Session binding: a Teleport session is valid only together with a JWT for the same user. A request that skips Teleport, or carries another user's JWT, ends the session immediately.

Rate limits and self-protection behind Teleport

  • Rate limits are counted per validated Teleport user, not per IP (every request through Teleport comes from the agent's address). Requests without a valid JWT count against their IP. Seamless sign-ins have the same budget as the sign-in endpoints (30 per minute per Teleport user).
  • Own account: a Teleport session carries no AD account, so the console looks up the JWT username and the windows_logins trait in AD (NETBIOS\name, or the UPN when the name contains @); every account found counts as the operator's own: no group membership change, password reset, unlock, disable or enable on it. If the DC cannot answer, those actions are refused (fail closed). A Teleport user whose AD account has another name needs a windows_logins trait with the AD logon name.
  • Protection of that AD account from OTHER operators (operator-takeover protection): at every sign-in and revalidation the accounts found are recorded on the Teleport operator (claims set by Authentication:Teleport:AdAccountClaims, default username,trait:windows_logins; trait:<name> for another trait, none to switch it off; audited operators.teleport_ad_accounts when they change). Changing those accounts is then allowed only to an operator who holds all of that Teleport operator's console rights, as for AD operators (Security).
  • AD groups behind mappings: a trait mapping whose value is an AD group DN or SID makes that group a protected (sensitive) group when the mappings are saved (refused while the DC cannot answer). Groups the console cannot see this way (Teleport roles given from AD groups by your SSO connector, trait values that are group names) must be added to Configuration › Protection › sensitive groups by hand: whoever can change their members gets the mapped console roles.

Tested

The Teleport integration is tested against real Teleport clusters: tests/teleport/teleport-e2e.sh (API level, throwaway cluster: setup through Teleport, seamless sign-in, mappings, step-up, "Teleport only" and its recovery, cookie replay refused) and tests/teleport/teleport-browser.sh (Chromium, end to end through the Teleport web UI). For a lab Teleport in front of a lab console, see tools/lab-teleport.sh.