Skip to content

A new Teleport cluster

The Teleport master server page generates a .zip bundle for a self-hosted, single-node Teleport 18.11.4 cluster (Auth + Proxy + application service in a single container). Tenantry does not run it: you copy the bundle to the target host and launch it yourself. The bundle runs the official Teleport image, under Teleport's license: see Teleport licensing for MSPs.

Prerequisites

  • Linux host with Docker and Docker Compose v2, clock synchronized (NTP).
  • A fully qualified domain name (FQDN) for the cluster, e.g. teleport.acme.ch: an IP will not do (WebAuthn requires a name).
  • Port 443 free and reachable by users (it is the only published port). With a Cloudflare Tunnel no inbound port is needed: you need a Cloudflare account holding the zone, with Zero Trust.
  • Certificate, depending on the decision of the wizard (see TLS certificates): reachable from the Internet: Teleport's built-in ACME (existing DNS A record, port 443 reachable from the Internet, contact e-mail); not reachable: ACME DNS-01 through an acme.sh sidecar and your DNS provider's API (no inbound port, optional wildcard *.<cluster> for application access, the API token is asked for by bootstrap.sh on the host, never in the bundle); your own ACME / PKI; your own certificates (PEM files in certs/) or self-signed (lab only: clients see certificate errors).

The wizard (4 steps)

  1. Cluster: FQDN, version (pinned, never latest), certificate decision ("Is this server reachable from the Internet on port 80?" -> Teleport ACME, else DNS-01 with the provider picker and the wildcard option).
  2. Administrator: name of the first user and their logins, factors (WebAuthn / OTP).
  3. Applications: built-in application service (dynamic registration) and, optionally, Steward roles + tenantry role.
  4. Preview and download: file list, warnings, .zip.

The options Cloudflare Tunnel, Private address for tsh and agents and Install Tenantry on this server are described below.

Bundle contents: config/teleport.yaml, compose.yml, preflight.sh, bootstrap.sh, backup.sh, README.md and, if requested, config/extra/roles-steward.yaml. With ACME DNS-01: acme/issue.sh, acme/publish.sh, acme/provider.env (non-secret settings) and an acme service in compose.yml; teleport.yaml uses https_keypairs with https_keypairs_reload_interval: 1h, so renewals need no restart. ./bootstrap.sh [--dns-credentials-file FILE] asks for the DNS API token once (secrets/acme-dns.env, 0600).

WebAuthn and rp_id. The rp_id equals the cluster FQDN. It is bound to the registered keys: changing the cluster or domain name invalidates them and forces all users to re-enroll. The cluster name cannot change after the first start: choose a final name.

Launching the deployment

On the target host, after extracting the .zip:

./preflight.sh     # checks Docker, DNS, port 443, clock, certificates
./bootstrap.sh     # preflight, docker compose up, wait for healthy state (180 s), first admin

bootstrap.sh displays the administrator's invitation link only once (valid for 1 hour, not stored): open it to set the password and enroll WebAuthn / OTP. If the user already exists, the creation is skipped.

First sign-in

Open https://<FQDN>, sign in, then on the command line:

tsh login --proxy=<FQDN>:443 --user=<admin>

Cloudflare Tunnel (no inbound port)

Turn on Cloudflare Tunnel in the advanced options to keep port 443 closed. The bundle then contains a pinned cloudflared service that connects out to Cloudflare, and bootstrap.sh asks for the tunnel token (typed hidden, or --tunnel-token-file FILE). The token is stored only in secrets/cloudflared/tunnel-token (mode 0400).

  • Teleport's built-in ACME needs port 443 from the Internet, so with a tunnel use ACME DNS-01, your own files or (lab) self-signed.
  • The bundle README lists the public hostnames to create in the tunnel: <cluster> (and *.<cluster> with the wildcard, for application access) to https://teleport:443 with Match SNI to Host on; and Tenantry's name to http://tenantry:8443 when it is installed with the cluster.
  • Private address for tsh and agents: tsh, agents and tbot cannot log in to Cloudflare Access. Give the address of a WireGuard or NetBird interface and port 443 stays published on that address only. Without it, the generator warns you.

The whole picture (Access, Duo, the three options for non-browser clients): Teleport behind Cloudflare Tunnel + Access + Duo.

Installing Tenantry on the same server

With Install Tenantry on this server (public name, permission level), the bundle also contains tenantry/ (its installer). At the end of bootstrap.sh:

tenantry/scripts/install.sh --name <first label of the name> --public-url https://<tenantry name> \
  --teleport-local docker:<teleport container> --bot-role minimal|full --version <x.y.z> \
  --proxy <cluster>:443 --expose proxy --non-interactive

This creates the roles tenantry (minimal unless you chose full), steward-admin and steward-helpdesk and the Machine ID bot with the local tctl (role texts embedded in the installer, sent on stdin), installs the Tenantry release of the bundle (pinned by digest; bootstrap.sh stops when the bundle has no version), joins the compose network of Teleport, and prints the Tenantry administrator password once. Then:

  • With a Cloudflare Tunnel: Tenantry is one more public hostname of the same tunnel (http://tenantry:8443), behind its own Access application. Remember the Bypass application for /api/callback and /api/i.
  • Without a tunnel: Tenantry is published as the Teleport application tenantry (config/extra/tenantry-app.yaml, created by bootstrap.sh). You sign in to Teleport (with MFA), then open https://<tenantry name>. This needs the application service, and a certificate that covers the name: use ACME DNS-01 with the wildcard and a name such as tenantry.<cluster>, or your own certificate files. Teleport's built-in ACME is refused. The DNS name must point to this host.

Upgrade Tenantry later (secrets and the bot identity are kept):

sudo tenantry/scripts/install.sh --name <name> --public-url https://<tenantry name> \
  --teleport-local auto --expose proxy --version <x.y.z>

Giving Tenantry an identity and publishing Steward

Without "Install Tenantry on this server", the "Steward roles" option makes bootstrap.sh apply config/extra/roles-steward.yaml: the console roles (steward-admin, steward-helpdesk) and the tenantry role. The right to create roles is equivalent to administering the cluster: assign tenantry only to Tenantry's identity, and keep it short-lived.

  1. Install Tenantry on this server with sudo scripts/install.sh --teleport-local auto (quick start), or from elsewhere with a bot (variants).
  2. The node's built-in application service (resources with the label *: *) serves any application created by tctl create: no separate agent needed.
  3. In the Steward page, apply the integration, then follow Steward integration.

Updating

  1. Read the Teleport release notes (never skip a major version).
  2. Change the pinned image tag in compose.yml.
  3. docker compose pull && docker compose up -d.

Also keep Tenantry's tctl version (--teleport-version) in line with the cluster.

Backup and restore

  • ./backup.sh archives config/ and data/ (hot copy; stop the service for a copy guaranteed to be consistent). data/ contains the certificate authority keys: mode 700, root access, encrypted backups.
  • Restore: stop the service (docker compose down), restore config/ and data/ identically (same permissions), then docker compose up -d. Test it before you need it.

Using tctl in the container

The image is distroless: no shell. Call the binary directly:

docker compose exec teleport /usr/local/bin/tctl --config=/etc/teleport/teleport.yaml status

Files passed to tctl create -f must come from a mounted directory: the bundle mounts ./config on /etc/teleport (so ./config/extra on /etc/teleport/extra). A docker cp to /tmp (tmpfs) does not persist for the next command, and standard input does not work: drop the YAML in ./config/extra/.

Troubleshooting

Symptom Cause Action
Teleport shows "self-signed certificate", and the built-in application service logs reverse tunnel warnings Self-signed mode: the certificate is not trusted, so the built-in application service does not validate the proxy. Expected. Lab only. In production, use acme or files.
preflight.sh: the FQDN does not resolve No DNS A record (blocking in ACME). Create the record, wait for propagation.
ACME fails 443 not reachable from the Internet, private name (.local, .internal, .lan), missing e-mail. Open 443, or switch to files / selfsigned for a private name.
bootstrap.sh: not healthy after 180 s Configuration or certificate error. docker compose logs --tail 100 teleport.
exec ... sh / bash fails Distroless image without a shell. Call /usr/local/bin/tctl --config=/etc/teleport/teleport.yaml ... directly (see above).
tctl create -f: file not found File copied to the /tmp tmpfs or read from stdin. Put the file in ./config/extra/ (mounted at /etc/teleport/extra).
Cannot bind 443 / read a mounted volume Capabilities removed. The bundle keeps exactly two capabilities: NET_BIND_SERVICE (open 443 as root without the capability) and DAC_OVERRIDE (read/write volumes owned by another host user). Do not remove them; everything else is cap_drop: ALL, no-new-privileges.
Users must re-enroll (WebAuthn) Cluster or domain name changed (rp_id). Go back to the original FQDN, otherwise a general re-enrollment is needed.
Application created but missing from tctl apps ls Application service disabled in the bundle. Regenerate with the built-in application service, or add an application agent.
Tenantry tctl: access denied tenantry role not assigned to the identity, or identity expired. See Steward integration (a) and error codes.

The container is not read-only: Teleport writes to /var/lib/teleport. Only port 443 is published; restrict it with a firewall to the networks that need it.

Teleport licensing for MSPs: see License.