Installer reference¶
deploy/install.sh installs, reconfigures and upgrades one client instance on a Linux Docker host. Instances share
nothing: each has its own directory (/opt/steward/<name>), compose project (steward-<name>), volumes, database, secrets and
audit log. It never touches Active Directory: AD rights are applied separately by a domain administrator
(Delegation model and AD rights).
New here? Follow the Quick start first; this page is the reference behind it.
Linux only
Run the console on a Linux member server with Docker Engine and Docker Compose v2, never on a domain controller. Windows hosting is not supported: the LDAPS certificate validation follows a different code path there that is not tested.
Two ways to run it¶
| Mode | How | When |
|---|---|---|
| Guided | sudo deploy/install.sh in a terminal, without options (or with --interactive) |
First install, occasional changes |
| Scripted | sudo deploy/install.sh --name ... --hostname ... [options] |
Automation, documented re-runs, upgrades |
Both run the same checks and the same code: the guided install only builds the option list, shows it as the "equivalent
command" in its summary, and hands it to the scripted path. Options are the reference; --non-interactive makes sure nothing is
ever asked (for automation), and --dry-run checks the options, prints the resolved plan and stops before changing anything:
sudo deploy/install.sh --name acme --hostname steward.client-example.ch --tls acme-dns --dns-provider ovh \
--dns-credentials-file /root/ovh.env --dry-run
Get the installer and verify the image¶
git clone https://github.com/Verttigo28/steward.git && cd steward && git checkout v1.0.0 # or unpack the release archive
Without --image or --build, the instance pulls verttigo28/steward:<version> from Docker Hub (mirror:
ghcr.io/verttigo28/steward). --version defaults to latest: pin a release in production.
Verify the image (supply chain)¶
Every release image (linux/amd64 and linux/arm64) is built by the Release GitHub Actions workflow, carries an SBOM and a
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/steward:$VERSION" --certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/Verttigo28/steward/.github/workflows/release.yml@refs/tags/v$VERSION"
# same for the mirror: ghcr.io/verttigo28/steward:$VERSION
A successful run proves that the image was produced by the release.yml workflow of Verttigo28/steward for that tag.
The installer does it too. After the pull, install.sh resolves the image to its digest and writes
STEWARD_IMAGE=verttigo28/steward:<version>@sha256:<digest> into <instance>/.env: restarts, recreates and re-runs without
--version run exactly that image, whatever later happens to the tag. When cosign is installed on the host it then runs
cosign verify <image>@<digest> with the issuer https://token.actions.githubusercontent.com and the identity
https://github.com/Verttigo28/steward/.github/workflows/release.yml@refs/tags/v<X.Y.Z> (any release tag), and stops when the
signature does not verify. Without cosign it warns and continues; --require-signature makes the verification mandatory (no
cosign, no digest, or an image that is not the official one: refused). For a mirror or your own build pipeline, set
STEWARD_COSIGN_IDENTITY_REGEXP to the identity that signs it. You can also install by digest yourself
(--image verttigo28/steward@sha256:<digest>). On a host that cannot reach the registry, build from the source you reviewed
with --build.
The guided install¶
The questions, in order (defaults in brackets; on an existing instance, its current values are the defaults):
- Instance: name (
acme: lowercase letters, digits,-), DNS name users will open, Steward version. - Active Directory (optional): domain and DC names. They are not stored: the browser setup wizard asks for
them. Their only use here: a DC that this host cannot resolve can be mapped to its IP inside the containers (
--add-host). - Helpdesk (off by default): the reset account's UPN and password; optionally the LAPS account's UPN and password.
- Exposure: LAN only, behind your reverse proxy/WAF, through Teleport, or Cloudflare Tunnel + Access (then the tunnel token).
- HTTPS certificate: public CA through ACME (port 80 reachable from the Internet? yes: HTTP-01; no: DNS-01 with a provider from the list: Infomaniak, OVHcloud, Cloudflare, Gandi, Hetzner, AWS Route 53, Azure DNS, deSEC, or any other acme.sh provider with its variables), your own ACME CA (directory URL, root, EAB), your certificate files, or self-signed (labs).
- Summary and confirmation, with the equivalent command.
Secrets (AD passwords, typed twice; DNS API tokens; tunnel token; EAB key) are read without echo, written to private
temporary files (mode 0600, removed when the script exits) and passed as --*-file options, like a scripted install. They are
never printed (the equivalent command shows <file holding ...> placeholders), never logged, and stored only under
/opt/steward/<name>/secrets/. On a re-run, leave a secret empty to keep the stored one.
Options¶
| Option | Meaning |
|---|---|
--name <name> |
Instance name: lowercase letters, digits, -, 31 characters at most. Required. |
--hostname <dns name> |
Name users open; it goes into the certificate. Required on a first install (a re-run keeps it, and the ports) |
--version <x.y.z> / --image <ref> / --build |
Release from Docker Hub (first install: latest unless given; a re-run without any of them keeps the image in use) / any image reference / build from this source tree |
--dir <path> |
Base directory (default /opt/steward); the instance lives in <dir>/<name> |
--https-port <n> / --http-port <n> |
Host ports (default 443 / 80; port 80 only redirects to HTTPS and answers HTTP-01) |
--add-host <fqdn>:<ip> (repeatable) / --no-add-host |
Map DCs the host DNS cannot resolve (Steward connects to DCs by name only) / remove the mappings |
--helpdesk --reset-upn <upn> --reset-password-file <file> / --no-helpdesk |
Start the separate password service (Helpdesk) / stop it |
--laps-upn <upn> --laps-password-file <file> |
Dedicated LAPS account, with --helpdesk (LAPS) |
--pin-password-service |
With --helpdesk: pin the password service to the console's current domain, DCs, LDAPS CA and managed OUs, shown for review first. After the setup wizard and after every change of them (Helpdesk) |
--pwd-domain <dns name> --pwd-dcs "<fqdn> ..." --pwd-ca-file <pem> --pwd-managed-ous <guid>,... |
The same pins given explicitly |
--trusted-proxy <ip\|network> (repeatable) / --no-trusted-proxy |
Reverse proxy whose X-Forwarded-For is believed (Exposure); automatic with Cloudflare Tunnel |
--require-signature |
Refuse an image that cosign cannot verify (cosign missing included): see Verify the image |
--tls acme-dns --dns-provider <name> --dns-credentials-file <file> |
Publicly trusted certificate, DNS-01 (HTTPS certificate) |
--tls acme-http |
Publicly trusted certificate, HTTP-01 (port 80 reachable from the Internet) |
--acme-email, --acme-server, --acme-eab-kid, --acme-eab-hmac-file, --acme-ca-bundle, --wildcard, --acme-dns-sleep, --acme-challenge-alias |
ACME details: see HTTPS certificate |
--tls files --cert <fullchain.pem> --key <key.pem> |
Your own certificate files |
--tls selfsigned |
Labs only: every browser shows a certificate error (the default when nothing else is given, with a warning) |
--exposure lan\|proxy\|teleport\|cloudflare-tunnel |
Who reaches the console and how (default lan): Exposure and access |
--tunnel-token-file <file> |
With --exposure cloudflare-tunnel: file holding the tunnel token (first time, or to rotate it) |
--interactive / --non-interactive |
Force the guided install / never ask anything |
--dry-run |
Check everything, print the plan (secrets: "from file", "kept" or "none", never values), change nothing |
-h, --help |
Usage |
Files given with --*-file hold one value (or NAME=value lines for --dns-credentials-file); create them with mode 600, and
delete them after the install: the installer keeps its own copy.
Every choice is kept across re-runs (it is recorded in <dir>/<name>/.env, without secrets): re-running with only
--name and --hostname keeps the image version, the TLS method, the exposure, the helpdesk and the DC mappings, and passing
an option changes only that.
Examples¶
# Client LAN, DNS-01 with Infomaniak (example provider), helpdesk with LAPS
sudo deploy/install.sh --name acme --hostname steward.client-example.ch --version 1.0.0 \
--dns-provider infomaniak --dns-credentials-file /root/infomaniak.env --acme-email ops@msp.example \
--helpdesk --reset-upn svc-steward-reset@corp.client-example.ch --reset-password-file /root/reset.txt \
--laps-upn svc-steward-laps@corp.client-example.ch --laps-password-file /root/laps.txt
# MSP technicians through Teleport, client's own certificate
sudo deploy/install.sh --name beta --hostname steward.beta.example.org --version 1.0.0 --exposure teleport \
--tls files --cert /root/fullchain.pem --key /root/key.pem
# Cloudflare Tunnel + Access: no inbound port at all
sudo deploy/install.sh --name gamma --hostname steward.gamma-client.ch --version 1.0.0 --exposure cloudflare-tunnel \
--tunnel-token-file /root/tunnel-token.txt --dns-provider cloudflare --dns-credentials-file /root/cf.env
What it does¶
- Checks every option (and that credential files hold the variables the DNS provider needs: names only, values are never read).
- Pulls (or builds) the image, and the pinned sidecar images it needs (acme.sh, cloudflared).
- Writes the instance directory and copies the secrets into it, owned by the container user that reads them.
- With ACME: obtains the certificate and checks it before Steward starts (when that fails, Steward is not started and the script says why).
- Starts the compose project, waits until it is healthy, and checks that the console serves a certificate that verifies for the name users open.
- Prints the URL, the TLS and exposure summary with what is left to do (for Cloudflare: the public hostname and the Access
application to create), the one-time setup token (also in
/data/setup-tokeninside the container) and the backup command.
What it creates¶
/opt/steward/acme/
├── compose.yml the instance's compose file (no "build"; no ports behind Cloudflare Tunnel)
├── compose.acme.yml ACME sidecar (acme-dns / acme-http)
├── compose.tls-files.yml your certificate files (files)
├── compose.cloudflared.yml Cloudflare Tunnel sidecar (cloudflare-tunnel)
├── compose.hosts.yml DC name -> IP mappings (--add-host)
├── .env every non-secret choice (re-runs read it)
├── config/ steward.example.yaml; put your steward.yaml here (optional settings)
├── acme/ ACME sidecar script (+ the internal CA root)
├── tls/ your certificate and key (files), readable by the container user only
└── secrets/ 0700: reset/LAPS passwords and link key (0600), ACME credentials (0400, root),
tunnel token (0400, cloudflared's user)
Plus two Docker volumes per instance: steward-data (database, backups, LDAP trust store, setup token) and steward-secrets
(master key, Data Protection keys, generated certificate). The containers run as a non-root user, with a read-only root file
system, no capabilities and no-new-privileges.
Optional settings (session lengths, throttling, audit retention, Teleport sign-in, ...) go in config/steward.yaml: copy
config/steward.example.yaml, which documents each key with its default. Environment variables STEWARD_<Section>__<Key>
override the file.
Re-runs, changes and upgrades¶
Re-run the script to change anything (it keeps what you do not pass), or to upgrade: --version <newer> pulls the new image;
pending database migrations run at start, after an automatic backup. Details: Operations.