Skip to content

Exposure: never expose raw

Tenantry holds a live Teleport identity and can create roles, tokens and locks. It also serves scripts that run as root on Linux gateways and as domain administrator on domain controllers. A compromise of Tenantry is a compromise of your cluster and of your clients' Active Directory.

So Tenantry is never published directly to the Internet. install.sh has three exposure modes (--expose), each with something in front of it.

Rule

No bare port 8443 on the Internet. Always one of: a private network, a WAF or reverse proxy with authentication, Cloudflare Access, or Teleport itself.

--expose Who terminates TLS Who authenticates people Inbound port
local (default) Tenantry's Caddy front on this server (--tls ...) Tenantry login, behind your network or VPN --https-port on this server
proxy Your reverse proxy or WAF Your proxy, plus Tenantry login Tenantry listens on --bind:--port (default 127.0.0.1:8443)
cloudflare-tunnel Cloudflare Cloudflare Access, plus Tenantry login None: outbound tunnel only

In every mode, Tenantry's own administrator login stays on. You can also add Teleport single sign-on (Teleport SSO).

1. Local network or VPN (--expose local)

Tenantry listens on 127.0.0.1 and an HTTPS front (Caddy) serves it on --https-port. Only expose that port to your management network or VPN. Choose the certificate in TLS certificates.

sudo scripts/install.sh --name acme --public-url https://tenantry.acme.ch:9443 \
  --teleport-local auto --tls acme-dns --dns-provider dns_cf --https-port 9443 \
  --dns-setting CF_Zone_ID=<zone-id> --dns-credentials-file /root/tenantry-dns.env

Restrict the port with the host firewall. Docker publishes ports around ufw; filter in the DOCKER-USER chain.

2. Your own reverse proxy or WAF (--expose proxy)

sudo scripts/install.sh --name acme --public-url https://tenantry.acme.ch \
  --teleport-local auto --expose proxy --bind 127.0.0.1 --port 8443 \
  --trust-proxy <your proxy's address as Tenantry sees it> --version <x.y.z>
  • Tenantry listens on --bind:--port (default 127.0.0.1:8443). If the proxy runs on another host, set --bind to a private address of this server and filter it with a firewall. --bind 0.0.0.0 publishes the plain HTTP port on every interface and prints a warning.
  • Your proxy terminates TLS and forwards to http://<bind>:<port>. Keep the Host header and add X-Forwarded-For. WebSocket support is not needed.
  • --trust-proxy names the address (or CIDR) your proxy connects from as Tenantry sees it: X-Forwarded-For (used by rate limiting and sign-in lockout) is honoured on connections from that address only. For a proxy on the same server, connecting through --bind:--port, that is the gateway of Tenantry's Docker network: the installer prints it at the end (--trust-proxy 172.x.0.1). Without --trust-proxy, X-Forwarded-For is ignored (warning): every user then shares the proxy's rate-limit bucket. A hop count (--trust-proxy 1) is still accepted but trusts any peer that reaches the port (warning). The proxy must replace X-Forwarded-For, not append to it.
  • With --tls (Caddy) and --expose cloudflare-tunnel, the front runs on a dedicated edge Docker network shared with Tenantry only, at a fixed address (.2 of a free /29, --edge-subnet to choose it), and TRUST_PROXY is set to that single address.
  • --public-url must be the URL users open.

3. Cloudflare Tunnel + Cloudflare Access (--expose cloudflare-tunnel)

A pinned cloudflared container opens an outbound tunnel to Cloudflare. No port is published. Cloudflare Access sits in front with identity and MFA.

sudo scripts/install.sh --name acme --public-url https://tenantry.acme.ch \
  --teleport-local auto \
  --expose cloudflare-tunnel --tunnel-token-file /root/tenantry-tunnel-token

The tunnel token is a secret. It is read from --tunnel-token-file (or typed hidden), copied to <dir>/cloudflared/tunnel-token (mode 0400, owned by the cloudflared user 65532), and never written in .env, a compose file or a command line. Delete your copy afterwards (shred -u). To change the token: delete <dir>/cloudflared/tunnel-token and run install.sh again.

Set it up in Cloudflare

  1. Create the tunnel in Cloudflare Zero Trust: Networks, Tunnels, Create a tunnel, cloudflared, environment Docker. Copy the token (the long string after --token).
  2. Create the Access application first: Access, Applications, Add, Self-hosted, domain tenantry.acme.ch. Policy Allow for your MSP team (e-mails or an identity-provider group) with MFA. Session 8 hours or less.
  3. Create the public hostname in the tunnel: tenantry.acme.ch, service HTTP tenantry:8443.
  4. Add the Bypass application described below.

Until step 2 is done the hostname is not protected. Do step 2 before step 3.

The two paths that must bypass Access

Client servers call Tenantry without a browser, so they cannot log in to Access:

Path Called by Why it is safe to bypass
/api/i/... curl / irm on the client's Linux gateway and domain controller, to download generated scripts Answers only to a one-time, unguessable id (256 bits); install.sh is served once
/api/callback/... The generated scripts, to send back detected values (NetBIOS, SID, LDAPS CA) Same: per-deployment bearer token

Create a second self-hosted application for tenantry.acme.ch/api/callback and tenantry.acme.ch/api/i with a Bypass policy. For more control, replace Bypass with an Allow policy for the egress IPs of your clients' sites.

Mask the key in logs

The key segment of /api/i/<id>/<key>/... is a secret. Make sure your WAF and proxy logs do not keep it.

A server gets 403 on /api/i/...

Tenantry never answers 403 on this path (it answers 401 for a wrong or expired key, 404, 409 or 410). A 403 comes from in front of it: an Access policy (Allow list without the server's egress IP), a WAF rule, a country or ASN block. From the server, probe with an invented id (no secret involved):

curl -s -o /dev/null -D - https://tenantry.acme.ch/api/i/00000000-0000-0000-0000-000000000000/x/steward-bundle.sh | head -12

401 with x-ratelimit-* headers: the request reaches Tenantry, so the link was wrong or expired (a Steward bundle link lives 30 minutes). 403 with cf-ray and no x-ratelimit-*: Cloudflare blocked it. Look in Zero Trust, Access, Logs and in Security, Events, then add the server's egress IP to the Allow (or Bypass) policy of /api/i and /api/callback.

Steward consoles behind Access

Tenantry polls each Steward console every 60 seconds over HTTPS. If a console sits behind Cloudflare Access, that poll is a non-browser client: add a Bypass or an Allow for Tenantry's egress IP, or reach the console on a private network path.

What the installer ran

<dir>/compose.tunnel.yml holds the cloudflared service: image pinned by digest, read_only, cap_drop: [ALL], no-new-privileges, 256 MB, 128 pids, no published port. Logs: docker compose -p tenantry-<name> logs cloudflared.

Checklist

  • [ ] No port of Tenantry reachable from the Internet except through the tunnel or your WAF.
  • [ ] Access application in place before the public hostname.
  • [ ] Bypass application limited to /api/callback and /api/i.
  • [ ] Token files deleted after install.
  • [ ] Tenantry login password strong; consider Teleport SSO with --sso-only.

Teleport itself also needs a way in for tsh and agents: Teleport behind Cloudflare Tunnel + Access + Duo.