install.sh options¶
scripts/install.sh installs or upgrades Tenantry on a Linux Docker host: one directory, one Compose project, generated secrets, re-runnable. tctl is embedded in the image (compiled from Teleport's AGPL-3.0 source): nothing to install on the host. scripts/install.sh --help prints the header with the same information.
sudo scripts/install.sh # guided mode (terminal, no option)
sudo scripts/install.sh --wizard # the same, explicitly
sudo scripts/install.sh --name acme --public-url https://tenantry.acme.ch ... # non-interactive
Requires Docker with Compose v2. Run it as root.
Guided mode¶
With no option in a terminal, or --wizard alone, the script asks five groups of questions: Teleport, public name, exposure, TLS, administrator. It shows a summary with the equivalent command, installs, and prints that command again. Secrets are typed hidden, kept in 0600 files in a private temporary folder (removed on exit), and shown in the equivalent command as files at placeholder paths: /root/tenantry-dns.env, /root/tenantry-tunnel-token, /root/tenantry-bot-token. Walkthrough: Quick start. --wizard cannot be combined with other options. With no option and no terminal, the script exits with an error.
Required¶
| Option | Purpose |
|---|---|
--name NAME |
Instance name: lowercase letters, digits, -, 31 characters at most. Compose project tenantry-NAME, folder <dir>/NAME. |
--public-url URL |
The URL people (and client servers) open: https://tenantry.acme.ch. It appears in the generated irm and curl commands. With --tls it must be https://<tls-domain>[:<https-port>]. With --expose cloudflare-tunnel it must be https://<hostname> without a port. |
Teleport on this server¶
| Option | Purpose |
|---|---|
--teleport-local auto\|docker:CONTAINER\|binary[:PATH] |
Teleport runs on this server (the most common setup). auto finds a running Docker container of a Teleport image whose tctl answers, else the tctl binary of a local Teleport service (needs /etc/teleport.yaml). It then uses that local tctl to create the roles tenantry, steward-admin and steward-helpdesk and the Machine ID bot tenantry, and hands the join token to tbot through a 0400 file deleted after the first join. The role texts are embedded in install.sh (never taken from the image being installed; the image's own copy is only compared, and a different text stops the installation), each is checked to be a single YAML document and is sent on tctl's stdin (docker exec -i ... tctl create -f -): nothing is copied into the Teleport container or its data directory. An existing steward-admin / steward-helpdesk role is left untouched; the tenantry role is re-applied at every run (so a new --bot-role takes effect). Implies --auth machine-id. It derives --auth-server (<container>:3025, pinned with the cluster CA pins; for a binary, teleport-host:3025 with --add-host teleport-host:host-gateway), --teleport-version, --proxy (<cluster>:443 when the cluster name contains a dot) and, for a container, --teleport-network. If several local Teleports exist, name one. A bot that already exists gets a new join token. |
--bot-role minimal\|full |
minimal (default): register the Steward application and list the fleet; no right to create roles, join tokens or locks. full (opt-in, prints a warning): also create Teleport roles (Steward integration with other role names, per-client roles), enrolment tokens ("Add a proxy") and locks (the lock button). Creating roles amounts to cluster administration and a join token can enrol any agent: with full, a compromise of Tenantry or of its identity file is a compromise of the cluster. --print-tenantry-role minimal\|full and --print-console-roles print the exact roles. |
--teleport-network NETWORK |
Attach Tenantry and tbot to the Docker network of a local Teleport container (set by --teleport-local). A container only on Docker's default bridge cannot be reached by name: run Teleport on a user-defined network, or pass --auth-server and --add-host. |
--proxy host:port |
Public Teleport proxy address, offered by default in the wizards (DEFAULT_PROXY_ADDR). |
Teleport elsewhere: authentication¶
| Option | Purpose |
|---|---|
--auth machine-id\|identity\|local |
How tctl authenticates. machine-id is recommended: a tbot sidecar renews a short-lived identity. identity: a tctl auth sign file you provide, which expires. local: tctl in local-admin mode with /etc/teleport.yaml and /var/lib/teleport mounted read-write: discouraged (a compromise of Tenantry is a compromise of the cluster; prefer --teleport-local). Default local, kept for re-runs of old installs: always set it explicitly. |
--auth-server host:port |
The Teleport proxy (host:443) or the auth service (host:3025). Required with machine-id and identity. |
--bot-token-file FILE |
machine-id without --teleport-local: file with the bot join token printed by tctl bots add (first line). Required on the first install. Deleted by the script once tbot has joined. Not allowed with --teleport-local. |
--ca-pin sha256:HEX |
Repeatable. Pins the cluster CA when --auth-server is the auth service (port 3025). tctl status prints the pins. Derived by --teleport-local. |
--identity-file FILE |
identity: the identity file. Only the file is mounted. The container user (uid 1000) must read it. |
--tbot-renewal-interval, --tbot-certificate-ttl |
machine-id: renewal_interval (default 20m) and certificate_ttl (default 1h). The interval must be shorter than the TTL. |
--tbot-image REF |
machine-id: overrides the tbot image (default public.ecr.aws/gravitational/tbot-distroless:<--teleport-version>, for example a mirror). The script pulls it and pins the digest it resolved (TENANTRY_TBOT_IMAGE=...:<version>@sha256:... in .env): tbot keeps running exactly that image even if the tag moves. |
--teleport-version X.Y.Z |
Version of the cluster (default 18.11.1): the tbot image and the tctl compiled by --build. A warning is printed when it differs from a local cluster's version. |
--teleport-commit SHA |
--build: pins the 40-hex commit of the Teleport tag vX.Y.Z that tctl is compiled from (git ls-remote https://github.com/gravitational/teleport 'refs/tags/vX.Y.Z^{}'). The Dockerfile pins the commit of the embedded version; required for any other tctl version. |
--ca-file FILE |
Private CA in front of the Teleport proxy: sets SSL_CERT_FILE (for tctl and tbot) and NODE_EXTRA_CA_CERTS (JWKS fetch). |
--add-host name:ip\|name:host-gateway |
Name resolution the host's DNS does not provide (repeatable). Applies to Tenantry and tbot. |
--join-network NETWORK |
Also attach to the Docker network of a Steward instance (for example steward-<name>_default), to reach the console at https://steward:8443. |
Exposure¶
See Exposure: never expose raw.
| Option | Purpose |
|---|---|
--expose local\|proxy\|cloudflare-tunnel |
local (default): Tenantry on 127.0.0.1:--port; with --tls, an HTTPS front (Caddy) on --https-port. proxy: behind your reverse proxy or WAF, which terminates TLS; pass --trust-proxy with the proxy's address (without it X-Forwarded-For is ignored and a warning is printed). cloudflare-tunnel: a pinned cloudflared sidecar opens an outbound tunnel, no port is published. |
--bind ADDR |
Host address of Tenantry's own port (default 127.0.0.1). Set a private address when your proxy runs elsewhere. 0.0.0.0 prints a warning. |
--port N |
Host port of Tenantry (default 8443). |
--tunnel-token-file FILE |
cloudflare-tunnel: the tunnel token (first line). Otherwise typed hidden on the first install. Stored in <dir>/cloudflared/tunnel-token (0400, uid 65532). |
--trust-proxy IP\|CIDRs |
TRUST_PROXY: the address(es) of your proxy as Tenantry sees them; X-Forwarded-For is honoured on connections from them only. For a proxy on the same host (through --bind:--port): the gateway of Tenantry's Docker network, printed at the end of the install. A hop count is still accepted but trusts any peer that reaches the port (warning). With --tls and --expose cloudflare-tunnel it defaults to the front's fixed address on the edge network. |
--edge-subnet CIDR |
--tls / cloudflare-tunnel: the private /24 to /29 of the dedicated edge Docker network (Tenantry + Caddy or cloudflared only; the front at .2, the only trusted proxy). Default: the first /29 of 172.31.255.0/24 (then 10.255.255.0/24) that overlaps no Docker network and no host route, kept across re-runs (TENANTRY_EDGE_SUBNET). |
TLS (with --expose local)¶
See TLS certificates. --tls cannot be combined with --expose proxy or cloudflare-tunnel.
| Option | Purpose |
|---|---|
--tls acme-dns\|acme-http\|files\|selfsigned |
HTTPS front end (pinned Caddy with static certificate files). Default acme-dns when --dns-provider is given, otherwise no HTTPS front. acme-dns: DNS-01. acme-http: HTTP-01 (port 80 from the Internet). files: your PEM files. selfsigned: lab only. |
--tls-domain NAME |
Certificate name. Default: the host of --public-url, which must then be https://NAME[:--https-port]. |
--https-port N |
Published HTTPS port (default 443). Must differ from --port. Use another port when Teleport owns 443. |
--no-http-redirect |
Do not publish port 80 for the redirect (for example when port 80 is used by something else). |
--tls-cert FILE, --tls-key FILE |
files: PEM chain and private key. Replace them and re-run to renew. |
--dns-provider dns_xxx |
The acme.sh DNS hook. Known providers are in the list; --print-dns-providers prints them with their variables. Any other hook also needs --dns-secret-var. |
--dns-setting VAR=value |
Non-secret provider setting (repeatable). Secrets are refused here. |
--dns-secret-var NAME |
Another acme.sh hook only (repeatable): the names of its secret variables. See Other acme.sh provider. |
--dns-credentials-file FILE |
VAR=value lines for the secret variables (and ACME_EAB_HMAC_KEY). Otherwise typed at a prompt. Stored only in <dir>/secrets/acme-dns.env (0600). |
--acme-email ADDR |
ACME account contact. |
--acme-server SERVER |
letsencrypt (default), letsencrypt_test, zerossl, or an ACME directory URL. |
--eab-kid KID |
External Account Binding key id (zerossl or a custom server). The HMAC key is typed or ACME_EAB_HMAC_KEY. |
--acme-ca-bundle FILE |
CA certificate of a custom ACME server that uses a private CA. |
--challenge-alias ZONE |
DNS-01 through a CNAME-delegated zone: the token needs rights on that zone only. |
--print-dns-providers |
Print provider|secret variables|settings and exit. |
Administrator and secrets¶
| Option | Purpose |
|---|---|
--admin-user NAME |
Administrator login (default admin). |
--admin-password-file FILE |
Password read from a file (12 characters minimum). Otherwise generated and shown once; only the Argon2id hash is stored. |
--reset-secrets |
Regenerates the admin password, the session secret and the secrets encryption key (console tokens and alert secrets must then be entered again). |
--non-interactive |
Never prompt: a missing secret is an error. |
Teleport sign-in (optional)¶
| Option | Purpose |
|---|---|
--sso-issuer, --sso-audience, --sso-jwks-url, --sso-roles |
Sign in through Teleport Application Access. All four are required together. See Teleport SSO. |
--sso-admin-roles ROLES |
TELEPORT_JWT_ADMIN_ROLES: the role(s), among --sso-roles, of the MSP administrators. Explicit only: empty means nobody signing in through Teleport is an administrator (every SSO user is a scoped technician; the local password stays the break-glass administrator). Asked when a terminal is attached and in the guided mode; a warning is printed when empty. |
--sso-only |
Refuse password sign-in. |
Image¶
| Option | Purpose |
|---|---|
--version X.Y.Z |
Required (unless --build or --image): the release to pull, verttigo28/tenantry:<version> from Docker Hub (mirror ghcr.io/verttigo28/tenantry). latest is refused. The image is pinned by digest (TENANTRY_IMAGE=verttigo28/tenantry:<version>@sha256:...); a re-run without --version keeps that pinned image. The published image embeds tctl 18.11.1 (any 18.x cluster). |
--require-signature |
Make the cosign verification mandatory: the install stops when cosign is missing or the signature does not verify. Without it, the signature is verified whenever cosign is installed (a failure stops the install), and a note is printed otherwise. Not compatible with --build. |
--image REF |
Use this image instead (private registry, digest). |
--build |
Build the image from this checkout (tag tenantry:local). Use with --teleport-version for a cluster of another major version. Needs Docker BuildKit (docker-buildx-plugin with docker-ce, docker-buildx with Ubuntu's docker.io) and takes a few minutes. The tag vX.Y.Z must exist on GitHub; if its go.mod needs another Go than the GO_VERSION build argument, the build stops and says so. |
-h, --help print the header. --teleport-sha256 was removed (no Teleport tarball is downloaded any more).
What the script does¶
- Compose project
tenantry-<name>, data and.envin<dir>/<name>(default--dir /opt/tenantry).umask 077:.envis 0600 from the moment it exists. - Inputs are validated before anything touches Docker or the disk: simple absolute paths,
host:port, network names, DNS names. - Assembles the Compose files in
<dir>/<name>:compose.yml, one ofcompose.machine-id.yml(with the generatedtbot.yaml),compose.auth-identity.ymlorcompose.auth-local.yml, plus as neededcompose.join-network.yml,compose.teleport-network.yml,compose.extra.yml(private CA, hosts),compose.tls.yml(Caddy and acme.sh) andcompose.tunnel.yml(cloudflared). It rewrites them at every run. - Published image: pulled, pinned by digest and, when
cosignis installed, its signature verified against the release workflow identity of tagv<version>(see below). - With
--teleport-local, creates the roles and the bot, then waits fortbot's first identity and deletes the join token. - Waits for
/api/health(up to 60 s) and prints the administrator password if it generated one. - Hardening: non-root container, read-only filesystem,
cap_drop: ALL,no-new-privileges, 512 MB, 256 pids,/tmpasnoexectmpfs.
Layout of <dir>/<name>¶
| Path | Content |
|---|---|
.env |
Settings and generated secrets (0600). |
tbot.yaml, tbot-secrets/ |
Machine ID configuration; the join token exists here only until the first join. |
secrets/acme-dns.env |
DNS API credentials and EAB HMAC key (0600). Mounted read-only into the acme.sh container and sourced by its process: never env_file/environment (not visible in docker inspect), never on a command line. |
acme/, certs/, Caddyfile, caddy-run.sh |
ACME scripts, published certificate, Caddy front end. |
cloudflared/tunnel-token |
Tunnel token (0400, uid 65532). |
.machine-id-joined |
Marker: tbot has joined. Re-runs need no token. |
The /data volume¶
The container is read-only. The only writable persistent path is the named volume tenantry-data on /data: audit log (audit.jsonl, SHA-256 chained), clients, consoles, alerts, technicians. See Backups.
Verify the image (supply chain)¶
Every release image (verttigo28/tenantry on Docker Hub, mirrored on ghcr.io/verttigo28/tenantry; linux/amd64 and linux/arm64) is built by the Release GitHub Actions workflow, carries an SBOM and a max-mode provenance attestation, and is signed by digest with cosign keyless signing (the GitHub OIDC identity of that workflow, no key to manage). Check it before installing:
VERSION=1.0.0
cosign verify "verttigo28/tenantry:$VERSION" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/Verttigo28/tenantry/.github/workflows/release.yml@refs/tags/v$VERSION"
install.sh runs the same check on the digest it pulled when cosign is installed (--require-signature makes it mandatory). A successful run proves that the image was produced by the release.yml workflow of the Verttigo28/tenantry repository for that tag. For stricter pinning, install by digest (--image verttigo28/tenantry@sha256:<digest>). Without cosign, or on a host that cannot reach the registry, build from the source you reviewed with --build.
Native install on the master node¶
Not supported. The former scripts/deploy-master.sh (Node.js service directly on the Teleport host, run as root, .env
readable by others) was removed. On the Teleport host itself, use install.sh --teleport-local binary (or
docker:CONTAINER): Tenantry runs in a hardened container (non-root, read-only filesystem, no capabilities) with a
short-lived Machine ID identity, its .env is mode 0600, and it stays out of the cluster's data directory. See
Quick start.
Machine ID details: Machine ID.