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.
Through Teleport (recommended for MSP technicians)¶
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.yamland 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>/tohttps://<host>:443/over HTTPS (re-encrypt; do not offload to plain HTTP), keep theHostheader, 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 fromX-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
cloudflaredsidecar fromdeploy/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.ymlloses itsportssection; cloudflared reaches Steward athttps://steward:8443over 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(orproxy,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, setSTEWARD_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.