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(default127.0.0.1:8443). If the proxy runs on another host, set--bindto a private address of this server and filter it with a firewall.--bind 0.0.0.0publishes the plain HTTP port on every interface and prints a warning. - Your proxy terminates TLS and forwards to
http://<bind>:<port>. Keep theHostheader and addX-Forwarded-For. WebSocket support is not needed. --trust-proxynames 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-Foris 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 replaceX-Forwarded-For, not append to it.- With
--tls(Caddy) and--expose cloudflare-tunnel, the front runs on a dedicatededgeDocker network shared with Tenantry only, at a fixed address (.2of a free /29,--edge-subnetto choose it), andTRUST_PROXYis set to that single address. --public-urlmust 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¶
- Create the tunnel in Cloudflare Zero Trust: Networks, Tunnels, Create a tunnel, cloudflared, environment Docker. Copy the token (the long string after
--token). - 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. - Create the public hostname in the tunnel:
tenantry.acme.ch, service HTTPtenantry:8443. - 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/callbackand/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.