Publish Steward¶
Steward is a product separate from Tenantry. The Steward page
generates a bundle (a one-line download command, or a .zip) for the target host, which you run yourself. Tenantry runs nothing and
never uses docker.sock. No secret is in the bundle: keys and passwords are
generated or entered on the target host.
Which scenario should you choose?¶
| Situation | Scenario | TLS | What you need to provide |
|---|---|---|---|
| Quick trial, a single customer, no Teleport | Standalone | console-selfsigned (default) |
Nothing: the console generates its certificate (browser warning) |
| Production without a reverse proxy, certificate from your PKI | Standalone | files |
fullchain.pem + privkey.pem in certs/ |
| Production, public name, no existing proxy | Standalone | caddy-acme |
Public DNS, ports 80 and 443 reachable; optional ACME email |
| An nginx / Traefik / Caddy already exists | Standalone | behind-proxy |
Your proxy (snippets provided in proxy/) |
| You already have a Teleport cluster | Existing Teleport | console-selfsigned or files |
tctl, access to an application agent |
| No Teleport, you want one, a single host | All-in-one | certificate for both names | 2 DNS names, port 443, certificate covering both |
| The console runs at the customer's site and its client proxy publishes it through its reverse tunnel (the product's main use case): new proxy | Via a client proxy, new mode |
console-selfsigned (internal hop) |
A join token (30 min, created in Tenantry), outbound TCP to the master, DNS + wildcard certificate on the master |
| Same, with a client proxy that is already connected (chosen from the fleet) | Via a client proxy, existing mode |
console-selfsigned or files |
Root access to the proxy server, console address reachable from it |
| Production, nothing opened on the firewall | Standalone or All-in-one with Cloudflare Tunnel | console-selfsigned, files or ACME DNS-01 |
A Cloudflare account with Zero Trust, a tunnel token (see Cloudflare Tunnel) |
| Password reset (helpdesk) | all | all | Reset account (UPN + password file), helpdesk option |
| Lab / demonstration | All-in-one selfsigned or Standalone default |
self-signed | Nothing (hosts file on client workstations) |
The helpdesk can be added to any scenario: a second steward-pwd container (internal network, no published
port), a binding key generated on the host and a password read from a file or entered interactively.
What each bundle contains¶
All scenarios: README.md (in French, specific to the scenario), compose.yml (hardened: read_only, tmpfs,
cap_drop: ALL, no-new-privileges, two named volumes), .env (no secret), config/steward.yaml,
backup.sh, restore.sh, upgrade.sh, uninstall.sh.
| Scenario | Additional files |
|---|---|
| Standalone | install.sh; certs/README.md (files); Caddyfile + caddy service (caddy-acme); proxy/nginx.conf, proxy/traefik-dynamic.yml, proxy/Caddyfile (behind-proxy) |
| Existing Teleport | install.sh, apply-teleport.sh, teleport/{roles.yaml, app.yaml, app-service.snippet.yaml, tenantry-role.yaml}; config/steward.yaml with Authentication:Teleport pre-filled |
| All-in-one | preflight.sh, bootstrap.sh, config/teleport.yaml, config/extra/roles-steward.yaml, certs/README.md; a single compose with the teleport and steward services |
Via a client proxy, new |
install.sh (token), preflight.sh, config/agent.yaml, teleport/roles.yaml; compose with the steward and teleport-agent services; config/master-ca.pem to drop in if the master has a private authority |
Via a client proxy, existing |
install.sh, proxy/app-service.snippet.yaml, proxy/enable-app-on-proxy.sh, teleport/roles.yaml; certs/README.md (files) |
The console image¶
The image is private: Tenantry does not know it, so the image field is mandatory (exact tag or digest,
never latest). Tenantry administrator can pre-fill it with DEFAULT_STEWARD_IMAGE (exposed by
GET /api/bundles/defaults).
Host without registry access: on a workstation that has access, docker pull <image> && docker save <image> -o image.tar,
then copy image.tar into the bundle folder. install.sh, bootstrap.sh and upgrade.sh load it with
docker load -i image.tar before starting.
Cloudflare Tunnel option¶
The Cloudflare Tunnel option adds a pinned cloudflared service to the bundle. It opens an outbound connection to Cloudflare and no port is published to the Internet. Cloudflare Access (identity and MFA) sits in front. It is available for the standalone and all-in-one scenarios only: with an existing Teleport or a client proxy, Teleport itself publishes the console.
install.sh(standalone) orbootstrap.sh(all-in-one) takes--tunnel-token-file FILE, or asks for the tunnel token, typed hidden. The token is stored only insecrets/cloudflared/tunnel-token(mode 0400, uid 65532). It is never in the bundle.- Standalone: keep a console certificate (
console-selfsigned,filesor ACME DNS-01).caddy-acmeandbehind-proxyare refused: Cloudflare serves the public certificate. The tunnel route ishttps://steward:8443(with No TLS Verify on forconsole-selfsigned, otherwise Match SNI to Host). - All-in-one: the certificate may be DNS-01, files or (lab) self-signed; HTTP-01 is refused because it needs port 80 from the Internet. Two routes, both to
https://teleport:443with Match SNI to Host: the Teleport name and the console name. - All-in-one and
tshor agents: they cannot log in to Cloudflare Access. Set the bind address to a private network interface (WireGuard or NetBird) and port 443 stays published on that address only, or add an Access Bypass for their source IPs. See Teleport behind Cloudflare Tunnel + Access + Duo. - The bundle
README.mdlists the public hostnames to create in the tunnel and the Access applications. Create the Access application before the public hostname. - Tenantry polls the console over HTTPS: if the console sits behind Access, Tenantry's egress IP needs a Bypass or an Allow policy.
Getting the bundle onto the server¶
Two ways, same files:
- One command (default). Tenantry's "Generate the install command" button keeps the rendered bundle for 30 minutes
behind an unguessable URL and gives you a command to paste on the target server. It downloads a self-extracting script,
checks its SHA-256 (computed by Tenantry and embedded in the command), and writes
./steward-<name>/in the current directory. Noscp, nounzip. It only writes files: it installs nothing and refuses to overwrite an existing folder. Then run the lines shown below it (cd steward-<name> && ./install.sh, ...). The server must be able to reach Tenantry's public address (PUBLIC_API_BASE, https in production). - Zip. "Download the Docker bundle (.zip)" is the fallback when the server cannot reach Tenantry: copy it with
scpandunzipit.
The bundle holds no secret, so the link can be used again until it expires (a retry, a second server); the URL is the only
capability, its key is masked in Tenantry's logs and every use is audited (script.download). Generating it is audited as
bundle.link.
The address the console listens on (read this before generating)¶
With an existing Teleport or a client proxy, the console is published by Teleport: the Teleport agent opens a connection to the
console, and nobody else should. The bundle therefore binds the console to STEWARD_BIND (default 127.0.0.1) on port
STEWARD_HTTPS_PORT, and the Teleport application uri is built from the same address.
- Console and Teleport agent on the same host: keep
127.0.0.1. - Console and agent on different hosts (a console on its own VM, the proxy elsewhere):
127.0.0.1cannot work, because the agent would connect to its own loopback. Set the bind address to the private IP of the console host (Advanced settings of the Steward page) before generating, and allow that port in the host firewall for the agent's address only. The warning "the console listens only on 127.0.0.1" on the page means exactly this.
You cannot reach a loopback-bound console from your browser by IP either: open it through Teleport (https://<application>.<master>).
Changing the address after the install¶
The address is written in three places, and the Teleport token audience must match the application uri. If you change one by hand, change all three, then restart:
| Where | What | Command |
|---|---|---|
Console host, .env |
STEWARD_BIND=<ip> |
sed -i 's/^STEWARD_BIND=.*/STEWARD_BIND=<ip>/' .env |
Console host, config/steward.yaml |
Authentication.Teleport.Audience = the application uri, exactly |
sed -i "s#^\( *Audience:\).*#\1 'https://<ip>:443'#" config/steward.yaml |
Proxy host, /etc/teleport.yaml |
uri: of the application |
sed -i "s#uri: 'https://<old>'#uri: 'https://<ip>:443'#" /etc/teleport.yaml |
Then docker compose restart steward on the console host and systemctl restart teleport on the proxy host.
docker compose up -d does not re-read a mounted file
config/ is mounted into the container. If only config/steward.yaml changed, docker compose up -d finds nothing to recreate and the
console keeps the old values. Use docker compose restart steward (or up -d --force-recreate).
Scenario 1: standalone¶
cd steward-acme # after the install command above (zip: unzip steward-acme.zip && cd steward-acme)
./install.sh # with helpdesk: ./install.sh --reset-password-file /path/password.txt
install.sh is idempotent: folders, secrets (0600), image loading, up -d, wait for the healthy state,
then display of the address and the setup token (single use, also in /data/setup-token). Open the
address, enter the token and follow the wizard (domain and LDAPS, technical accounts, OUs, groups, first AD
administrator).
The TLS variants¶
The wizard asks a decision (details in TLS certificates): an automatic public certificate by default ("reachable from the Internet on port 80?" -> HTTP-01 with Caddy, else DNS-01), your own ACME / PKI, your own files, your reverse proxy, or lab (self-signed, with a warning).
acme-dns(default): anacmeservice (official acme.sh image, pinned by digest) obtains a publicly trusted ECDSA P-256 certificate through DNS-01 with your DNS provider's API (Infomaniak, OVHcloud, Cloudflare, Gandi, Hetzner, Route 53, Azure DNS, deSEC, DigitalOcean, Linode, PowerDNS), renews it (acme.sh --cron) and publishes it atomically in theacme-certsvolume; the console reads it throughTls:CertificatePemPath/Tls:KeyPemPathand hot-reloads it. No inbound port, internal-only hosts work. The DNS API token is never in the bundle:./install.shasks for it (or--dns-credentials-file FILE) and writessecrets/acme-dns.env(0600). Also available with an existing Teleport or an existing client proxy (the agent then verifies the console with its system trust store: no CA to distribute, noinsecure_skip_verify).console-selfsigned(lab / testing): the console generates a self-signed certificate (2 years) on first start, with the host name entered. Ports 443 and 80 (redirect) published. Every client sees a certificate error.files: the console reads a PFX (Tls:CertificatePath+Tls:CertificatePasswordFile). Drop incerts/fullchain.pemandcerts/privkey.pem;install.shchecks the match, the validity and the name, buildscerts/console.pfxand a random password, and hands them to the container read-only. Renewal: replace the PEMs, deleteconsole.pfxandpfx-password, reruninstall.sh,docker compose restart steward.caddy-acme: acaddyservice (imagecaddy:2.10.2, pinned) obtains a public certificate for the name and relays to the console's HTTPS listener. The console is not published. The Caddy -> console hop stays in the private Docker network (console's self-signed certificate, not verified). Requires 443 and 80 (ACME challenge) and public DNS. The certificates live in thecaddy-datavolume.behind-proxy: the console listens only onSTEWARD_BIND:STEWARD_HTTPS_PORT(default127.0.0.1:8443). Your proxy terminates the public TLS and relays over HTTPS to that address; forwardHost,X-Forwarded-For,X-Forwarded-Proto. The three snippets are ready to paste.
Scenario 2: existing Teleport¶
./install.sh
./apply-teleport.sh # --identity FILE --auth-server HOST:PORT if tctl uses a dedicated identity
- The console is published only on
STEWARD_BIND(default127.0.0.1). If the agent is on another host, put the console's private address in.envand filter with a firewall (the bundle'sREADME.mdgives theufwrule; with Docker, filter inDOCKER-USER). apply-teleport.sh: checkstctl, refuses to overwrite a role or an application of the same name that does not carry thetenantry-managedlabel (unless--overwrite), creates thesteward-admin/steward-helpdeskroles and the application, then checks that they exist.- The agent must trust the console's certificate (
insecure_skip_verify: false): usefilesmode with an authority known to the agent, or addteleport/console-cert.pem(exported byinstall.sh) to the agent's trust store. Registration: dynamic (app.yaml, applied by the script) or static (app-service.snippet.yaml). - Give the Teleport roles to the users, then open the console from Teleport: the first administrator is the Teleport user who completes the wizard. Then map the roles in the console (Delegations > Teleport).
teleport/tenantry-role.yaml (Tenantry identity) is not applied: the right to create roles is equivalent
to administering the cluster.
Scenario 3: all-in-one (Teleport + console)¶
# certMode acme-dns (default): the DNS API token is asked for (or --dns-credentials-file FILE)
# certMode files: drop in certs/fullchain.pem and certs/privkey.pem (covering BOTH names)
./bootstrap.sh # with helpdesk: --reset-password-file /path/password.txt
-
Certificate:
acme-dns(default) oracme-http(port 80 reachable from the Internet): theacmeservice issues ONE certificate covering both names (Teleport and console) before Teleport starts; Teleport re-reads it everyhttps_keypairs_reload_interval(1 h) after a renewal.filesandselfsigned(lab) remain. -
A single compose:
teleport(public.ecr.aws/gravitational/teleport-distroless, version in.env, port 443,cap_add: NET_BIND_SERVICE, DAC_OVERRIDE) andsteward(not published). A network alias makes Teleport's public name resolve to its container for reading the public keys (JWKS). config/teleport.yaml: multiplexing on 443,https_keypairs, static applicationsteward(uri: https://steward:8443,public_addr= console name).insecure_skip_verify: trueis used there only because this hop stays in the compose's private Docker network.config/steward.yaml:Issuer= cluster name,Audience=https://steward:8443(the application URI),JwksUrl=https://<teleportFqdn>/.well-known/jwks.json,JwksCaFileif self-signed certificate,TrustSessionMfa: true.preflight.sh: docker/compose, port 443 free, both names resolved to the host, clock, certificate. Infilesmode, the certificate must cover both names (exact names or wildcard): checked byopenssl x509 -ext subjectAltNameand-checkhost. Inselfsignedmode: generation of a certificate with both SANs, LAB ONLY banner.bootstrap.sh: preflight, startup, wait for healthy (Teleport, console, and passwords if helpdesk), creation of the roles, then of the first administrator (tctl users add). The invitation link is displayed only once.- Why not Teleport's built-in ACME? Application access requires the certificate to cover each application's address; the built-in ACME only issues the proxy's name. The acme.sh sidecar issues the two-name certificate instead.
- Open
https://<console>through Teleport; the first administrator is the Teleport user who completes the wizard; then map the Teleport roles to the console roles.
Scenario 4: via a client proxy¶
The console runs at the customer's site; a client proxy (Teleport agent with a reverse tunnel to the master) publishes it as a
Teleport application. Unlike scenario 2 (application registered globally on the master, with no choice of
agent), the chosen proxy serves the application: spec.host_id / spec.hostname of tctl apps ls.
Common prerequisites (master side)¶
- Public address of the application: by default
<application>.<master proxy domain>(e.g.steward.teleport.acme.ch). Teleport application access requires wildcard DNS (*.teleport.acme.ch) and a wildcard certificate (or one covering this name) on the master. Otherwise provide an explicit address (appPublicAddr) that your DNS and certificate cover. It must differ from the master proxy's name and the console's name. - Roles:
teleport/roles.yaml(steward-admin,steward-helpdesk, selectorapp: steward) to create on the master (tctl create -f teleport/roles.yaml), then to assign to the Teleport users. - Versions: the agent must not be newer than the master (the
masterVersionfield makes a higher version get refused). - In the bundle,
config/steward.yamlis pre-filled:Issuer= cluster name,Audience= the application URI,JwksUrl=https://<master proxy>/.well-known/jwks.json,JwksCaFileif private authority,TrustSessionMfa,LaunchUrl.
new mode: a new proxy installed with the console¶
# 1. In Tenantry: "Generate a join token (30 min)" (POST /api/bundles/proxy-token, 30 minutes); copy it into a 0600 file
umask 077 && cat > token.txt
# 2. on the customer host:
unzip steward-*.zip && cd steward-*
./install.sh --token-file token.txt # or with no option: masked prompt
- Compose:
steward(not published, no port) andteleport-agent(teleport-distroless:<x.y.z>pinned in.env,cap_drop: ALL+DAC_OVERRIDE,agent-datavolume). The agent opens an outbound connection to the master's proxy. config/agent.yaml(mounted at the image's default path/etc/teleport/teleport.yaml, without overridingcommand):proxy_server= master proxy,join_params: {method: token, token_name: /run/secrets/join-token}(the token is read from a file), static applicationsteward-<name>→https://steward-<name>:8443(instance-specific name and audience),public_addr, labelapp: steward.insecure_skip_verify: trueis used there only because this agent → console hop stays in the compose's private Docker network (commented in the file).preflight.sh(run byinstall.sh): docker/compose, master DNS, outbound connection to the master proxy (/dev/tcp), clock, public address resolution, private authority.install.sh: writes the token tosecrets/join/join-token(0600), starts, waits for the agent to join, then deletes the token and asks you to delete your copy. If the join is not confirmed, the token is kept, the script fails and explains what to do. Rerun without a token, it restarts an already-joined agent (the identity is inagent-data).- Private master authority: drop in
config/master-ca.pem(mounted in the agent viaSSL_CERT_FILEand read by the console viaJwksCaFile). - Backup:
backup.shadds an archive of the agent's identity;restore.sh --agent-archive <archive>restores it;upgrade.sh --agent-version X.Y.Zchanges the agent version (never newer than the master, no major version jump).
existing mode: an already-connected client proxy¶
The proxy is chosen from the fleet (the bundle carries only its host name, for information and for verification).
./install.sh # on the console host
# copy the proxy/ folder to the client proxy server, then, ON THAT SERVER, as root:
umask 077 && cat > token.txt # NEW token (app + windowsdesktop roles), created in Tenantry: "A client proxy already installed" option
sudo ./enable-app-on-proxy.sh --token-file token.txt --both-roles
Why a re-enrollment (proven in the lab, Teleport 18.2). A Teleport agent gets its service rights from the identity issued when it joins: adding app_service to an already-joined proxy fails, even with a valid token (control stream not configured to support app server heartbeats, or can not join the cluster with role "App"). Variants tested: Application-only token, identity kept: failure; Application + windowsdesktop token, identity kept: failure; two-role token + setting <data_dir>/proc aside (re-enrollment) + original configuration + app_service: success (apps ls shows the application served by the same host name, the static desktop stays registered, restart without the token works). Consequences to announce to the customer: the proxy keeps its name and its servers but takes a new host identifier (a new instance in the fleet, the old one expires; locks placed by identifier do not follow) and the sessions going through it are cut for a few seconds.
- The console is published only on
STEWARD_BIND(default127.0.0.1, if console and proxy share the host). If the proxy is elsewhere: the console's private address + a firewall rule allowing only the proxy host (see the bundle'sREADME.md). proxy/app-service.snippet.yaml: static application,uri: https://<console address as seen from the proxy>:<port>,insecure_skip_verify: falseby default. To have a self-signed certificate trusted: authority bundle (system store +proxy/console-cert.pem) pointed to by the proxy's Teleport serviceSSL_CERT_FILE(proxyCaFiledocuments the path).trueonly if the operator explicitly asks for it.enable-app-on-proxy.sh(root, on the proxy): refuses to merge if anapp_service:block exists (any form; it prints the entry to add by hand) or if the token is in clear text in the configuration; without systemd it does nothing and prints the manual procedure. Otherwise, in order: validation of the new configuration (teleport configure --teston a temporary file in the same folder), confirmation (type the machine name, or--yes),systemctl stop teleport, setting<data_dir>/procaside (proc.before-app-<date>), backup of the configuration and the old token, atomic installation of the configuration and the token (0600, location read fromauth_token:orjoin_params: token_name:),systemctl start teleport, wait for the active state and for "All applications successfully started" with no join error injournalctl. At the slightest failure or signal: stop, restoration ofproc, the configuration and the token, restart, non-zero code. On success: token deleted (shred) and old identity deleted (stale keys), after displaying what is deleted and what is kept.--both-rolesattests that the token has both roles (the script cannot read them). The token is never displayed. Idempotent.
Review 4 (re-enrollment robustness): restoration is uninterruptible (HUP, INT, TERM and SIGPIPE ignored, output redirected to a .teleport-app-enrol.log.* log next to the configuration); run the script under systemd-run --unit=teleport-app-enrol --wait --collect (or nohup, tmux). State flags are set before each action and the restoration tests the actual existence of the backups; if it partially fails, an "INCOMPLETE RESTORE: MANUAL ACTION REQUIRED" frame lists precisely what remains and where (non-zero code). After "All applications successfully started", a 15 s observation (service active, no join error) precedes the conclusion; a join refusal (can not join the cluster with role, token expired or not found, control stream not configured) makes it fail immediately. The old identity is deleted only after this observation; after a restoration, the aborted identity (private keys) is deleted (shred). The configuration is snapshotted at the start: if it changes during the confirmation, the script stops. data_dir is read without comments or CRLF; duplicate or flow form: failure with --data-dir DIR; <data_dir>/proc missing: failure before any modification. Your token file is deleted (shred) after success; the token stays valid for 10 minutes and multi-use in the cluster: tctl tokens rm <the token created in Tenantry> as soon as you are done. The two-role token lives 10 minutes (30 for an application-only token); the cap of 5 tokens counts only application tokens with a readable future expiry, and the creation lock is per process.
Review 3: the application name is steward-<instance name> (specific to each site); token creation is limited (3/min, 409 refusal at 5 valid application tokens, all types combined); backup.sh never archives secrets/join. The roles: 'app+windowsdesktop' option of POST /api/bundles/proxy-token creates a --type=app,windowsdesktop token (only the values app and app+windowsdesktop are accepted, 400 otherwise; the audit records the role set).
Verify¶
In Tenantry, Verify (POST /api/steward/verify) checks the roles, the application, its URI, its label and displays
served by <host>: the proxy that actually serves the application. With expectServedBy (the chosen proxy), any other host makes the
verification fail (old global registration on the master, wrong proxy). An unreadable list returns 502, never "nothing is served".
Operations (all scenarios)¶
| Action | Command |
|---|---|
| Backup | ./backup.sh [folder]: consistent copy of the database + /secrets (master key) + host archive (.env, config/, secrets/, certs/, data/). Archives 0600, to be encrypted. |
| Restore | ./restore.sh <console-archive.tgz>: stops the console, sets the current state aside, restores, waits for healthy. |
| Upgrade | ./upgrade.sh <new-image> [--teleport-version X.Y.Z] [--agent-version X.Y.Z]: backs up, changes the tag in .env, loads image.tar or pulls the image, recreates, waits for healthy. Refuses latest and, for Teleport, a major version jump. |
| Rollback | cp .env.before-upgrade-<date> .env && docker compose up -d, then ./restore.sh if the database has already migrated (migrations follow an automatic backup in /data/backups). |
| Stop | ./uninstall.sh keeps the volumes; ./uninstall.sh --purge deletes them after confirmation (type the project name). |
Troubleshooting¶
| Symptom | Lead |
|---|---|
steward not healthy |
docker compose logs --tail 80 steward: image missing (registry login or image.tar), unreadable PFX, DC not resolved (bundle DNS entries). |
| Setup token lost | docker compose exec steward dotnet Steward.Api.dll reset-setup --confirm. |
| Port already in use | ss -ltnp (filter on 443), then STEWARD_HTTPS_PORT in .env. |
preflight.sh: the certificate does not cover a name |
The certificate must contain both names as SANs (or *.domain). |
| Caddy does not obtain a certificate | Public DNS, ports 80/443 reachable, docker compose logs caddy. |
Client proxy new: the agent does not join |
docker compose logs teleport-agent: token expired (30 min) or already used (create another), private authority (config/master-ca.pem), DNS or outbound firewall, clock. The token is deleted only after a confirmed join. |
| Client proxy: the public address does not respond | Wildcard DNS and wildcard certificate on the master for <application>.<master domain> (or appPublicAddr covered). |
"Verify": served by another host |
The application is served by another agent (or registered globally by scenario 2): delete the old app resource. |
| Teleport: the console answers "access denied" | Open it from Teleport, check Issuer / Audience / JwksUrl in config/steward.yaml and the clock. |
| "The Teleport identity could not be verified" | docker compose logs steward \| grep 'Teleport JWT rejected' gives the reason. SecurityTokenInvalidAudienceException: Audience is not exactly the application uri (after an address change, see above); restart with docker compose restart steward. Also open the console from Teleport, never by its IP. |
"Verify": application ... not registered in the cluster |
Fixed in current Tenantry: a static app of a proxy's teleport.yaml is not an app resource (tctl get app/<name> says "does not exist") but shows in tctl apps ls. Update Tenantry; the check now reads both. |
| The application name does not open in the browser | See the three prerequisites in Teleport behind Cloudflare: wildcard DNS, an edge certificate that covers *.teleport.<domain>, and the tunnel route. |
| Steward setup token | docker compose exec steward cat /data/setup-token from the bundle folder (a new token at every start until setup is finished). |
| Behind a proxy, all clients have the same IP | See below. |
Points to check behind a reverse proxy¶
The console's documentation mentions neither support for X-Forwarded-For / X-Forwarded-Proto, nor a list
of trusted proxies. Behind a proxy, the console may therefore log the proxy's address as the client address and
apply its per-IP rate limiting to all clients at once. Test with your proxy before production; the
existing Teleport or all-in-one mode behaves the same way (the client seen is the Teleport agent).