Skip to content

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 .env in <dir>/<name> (default --dir /opt/tenantry). umask 077: .env is 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 of compose.machine-id.yml (with the generated tbot.yaml), compose.auth-identity.yml or compose.auth-local.yml, plus as needed compose.join-network.yml, compose.teleport-network.yml, compose.extra.yml (private CA, hosts), compose.tls.yml (Caddy and acme.sh) and compose.tunnel.yml (cloudflared). It rewrites them at every run.
  • Published image: pulled, pinned by digest and, when cosign is installed, its signature verified against the release workflow identity of tag v<version> (see below).
  • With --teleport-local, creates the roles and the bot, then waits for tbot'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, /tmp as noexec tmpfs.

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.