Skip to content

Exposure and access

Steward administers a client's Active Directory. Treat its URL like the door of the server room: never publish it raw on the Internet, not even "just for a while", not even with a strong password policy. Every request from outside the client's network must first pass something that authenticates the person (with MFA) before a single packet reaches Steward.

Choose

Who opens the console Exposure install.sh Inbound ports on the host
Client power users and on-site technicians, on the client's network Client LAN only --exposure lan (default) 443 (and 80, redirect) from the LAN only
MSP technicians from anywhere (recommended) Through Teleport --exposure teleport 443 from the Teleport agent only
Users behind an existing reverse proxy or WAF you operate Behind my reverse proxy --exposure proxy 443 from the proxy only
Users from the Internet, without Teleport and without opening any port Cloudflare Tunnel + Cloudflare Access --exposure cloudflare-tunnel --tunnel-token-file <file> none

Mixing is normal: a console can be on the client LAN for power users and published through Teleport for the MSP (choose teleport; the LAN keeps working). The guided install asks this question (step 4/6) and prints what is left to do.

flowchart LR
  lan["Client LAN users"] -->|HTTPS| s["Steward"]
  t["MSP technicians"] -->|"SSO + MFA"| tp["Teleport proxy"] -->|"app access (agent)"| s
  i["Internet users"] -->|"SSO + MFA"| cf["Cloudflare Access + WAF"] -->|"outbound tunnel (cloudflared)"| s
  x["Anyone on the Internet"] -. "never: no raw exposure" .-x s

Client LAN only

The default. Steward listens on 443 (HTTPS) and 80 (redirect to HTTPS; it also answers ACME HTTP-01 challenges).

  • Give the console's name an internal DNS record pointing to the host. With a DNS-01 certificate the name can stay internal: nothing is opened to the Internet.
  • Restrict 443/80 on the host firewall to the client's networks (and your management network).
  • Do not forward these ports from the Internet. For remote access use Teleport or Cloudflare Access below.

Teleport Application Access puts the console behind the MSP's Teleport cluster: technicians sign in once with SSO and MFA, every session is recorded in Teleport's audit log, and Steward signs them in seamlessly from the Teleport identity (no password typed into the console, optional "Teleport only" mode). A Teleport agent at the client (or the client's proxy) reaches https://<console>:443; nothing else needs to.

  • Tenantry generates the Teleport side for you (application, roles, agent bundle): Tenantry documentation.
  • On the console side, enable Teleport sign-in in config/steward.yaml and map Teleport roles to Steward roles: Teleport sign-in.
  • Firewall: allow 443 to the host from the Teleport agent only (plus the client LAN if power users use it directly).

Behind my reverse proxy or WAF

For an existing reverse proxy, load balancer or WAF (for example one already publishing the client's other applications with pre-authentication).

  • Forward https://<name>/ to https://<host>:443/ over HTTPS (re-encrypt; do not offload to plain HTTP), keep the Host header, and verify Steward's certificate against <name> (a publicly trusted certificate makes this trivial).
  • Require authentication at the proxy (SSO + MFA) before forwarding, and allow only the proxy to reach port 443 of the host.
  • Tell Steward which proxy to believe: --trusted-proxy <ip> or --trusted-proxy <network/prefix> (repeatable; the guided install asks for it), i.e. the address(es) the proxy connects from, as this host sees them. Steward then takes the client address from X-Forwarded-For (the right-most entry not added by a trusted proxy), only on requests coming from those addresses: from anyone else the header is ignored. The sign-in throttle, the rate limits, the fleet budget and the audit then see each real client. Configure the proxy to set (overwrite or append to) X-Forwarded-For. Without --trusted-proxy, Steward trusts no header: every user shares the proxy's address and one per-IP sign-in budget (the per-account throttle still protects each account). Settings: Network:KnownProxies, Network:KnownNetworks, Network:ClientIpHeader (STEWARD_Network__*). Only failed sign-ins spend the per-IP budget.

Cloudflare Tunnel + Cloudflare Access

No inbound port at all: a cloudflared container next to Steward opens an outbound connection to Cloudflare, and Cloudflare Access authenticates every request (your identity provider, MFA, device posture) before forwarding it through the tunnel. The Cloudflare WAF filters the rest.

1. Create the tunnel. Cloudflare Zero Trust > Networks > Tunnels > Create a tunnel > Cloudflared. Name it after the client. Copy the token (the long string after --token in the command Cloudflare shows); do not run that command.

2. Install with the tunnel. Guided install: choose exposure 4 and paste the token (hidden). Scripted:

sudo install -m 600 /dev/null /root/tunnel-token.txt && sudo vi /root/tunnel-token.txt      # the token, one line
sudo deploy/install.sh --name acme --hostname steward.client-example.ch --version 1.0.0 \
     --exposure cloudflare-tunnel --tunnel-token-file /root/tunnel-token.txt \
     --dns-provider cloudflare --dns-credentials-file /root/cf.env
sudo shred -u /root/tunnel-token.txt     # the installer keeps its own copy (secrets/cloudflared-token, 0400)

The installer:

  • adds the cloudflared sidecar from deploy/compose.cloudflared.yml: image pinned by version and digest, auto-update off, read-only, no capabilities, no-new-privileges; the token is a Docker secret read with --token-file (never in the environment, .env, the compose files or the output);
  • publishes no port: the instance's compose.yml loses its ports section; cloudflared reaches Steward at https://steward:8443 over the compose network;
  • refuses --tls acme-http (nothing reachable on port 80): use DNS-01 (the usual choice) or your own files.

3. Route the hostname. On the tunnel: Public Hostname > Add: hostname steward.client-example.ch, service HTTPS, URL steward:8443. Under TLS, set Origin Server Name to steward.client-example.ch so cloudflared verifies Steward's certificate (with a self-signed lab certificate only: "No TLS Verify").

4. Protect it with Access, before it goes live. Access > Applications > Add an application > Self-hosted: domain steward.client-example.ch, session duration of a working day at most, and a policy that allows only the right people (an identity provider group of your technicians, or the client's power users) with MFA. Everyone else is blocked by default.

5. Check. docker logs steward-acme-cloudflared-1 shows "Registered tunnel connection"; opening the URL from outside shows the Access login first, then Steward.

Access is not optional

A tunnel without an Access application publishes the console on the Internet. Create the Access application first, and keep Steward's own sign-in on: Access decides who may reach the console, Steward decides what they may do.

cloudflared and Steward share a small network of their own (tunnel, a fixed 172.30.x.y/28 subnet chosen by the installer and kept in .env as STEWARD_TUNNEL_SUBNET); nothing else joins it. Steward trusts that network, and only it, as its proxy and takes the client address from CF-Connecting-IP, which Cloudflare's edge sets (a client cannot choose it): the sign-in throttle and the audit see each real client. The Access identity is not used to sign in to Steward: operators still sign in with their AD account (or use Teleport instead).

Rotating the token, switching off, updating cloudflared

  • Rotate: Zero Trust > the tunnel > refresh the token, then re-run the installer with --tunnel-token-file <new file> (the guided install asks for it; Enter keeps the stored one). The container is recreated with the new secret.
  • Switch to another exposure: re-run with --exposure lan (or proxy, teleport): the cloudflared container is removed and the ports are published again. Delete the tunnel in Cloudflare.
  • Update cloudflared: the tag and digest are in deploy/compose.cloudflared.yml. Take a release at least two weeks old, read its digest (docker buildx imagetools inspect cloudflare/cloudflared:<tag>), update both and re-run the installer. To use a mirror, set STEWARD_CLOUDFLARED_IMAGE=<reference> when running it.

In every case

  • Operators sign in with their own non-privileged account; privileged AD accounts are refused (Security model).
  • Sensitive actions ask for a step-up (authenticator code, or the Teleport session).
  • Every sign-in (method, account, source address) and every change is in the audit log; problems are in Troubleshooting.