Skip to content

TLS certificates

Goal: nobody sees a certificate error, and nobody deploys a CA by GPO. Everything Tenantry installs can get a publicly trusted certificate, internal-only servers included.

You may not need this page

If Tenantry is behind Cloudflare Tunnel or behind your own reverse proxy, that layer serves the certificate users see. TLS options of install.sh (--tls) apply only to --expose local, where Tenantry serves HTTPS itself.

Which method?

Your situation Method Installer option Page
The server is reachable from the Internet on port 80 HTTP-01 --tls acme-http below
Not reachable (LAN, behind NAT, firewall) or you are not sure DNS-01 through your DNS provider's API --tls acme-dns --dns-provider dns_xxx one page per provider (below)
You already have a certificate (PEM chain and key) Your own files --tls files --tls-cert FILE --tls-key FILE Own certificates
You run your own ACME server (step-ca, internal PKI) Your own ACME server --acme-server https://.../directory Own certificates
Lab, nobody should trust it Self-signed --tls selfsigned Own certificates

Not sure? Choose DNS-01. It works everywhere, needs no inbound port, and the name may resolve on your internal network only.

DNS-01 providers

DNS-01 proves that you own a name by writing a TXT record _acme-challenge.<name> in its public DNS zone through the API of the company that hosts the zone. The name must live under a public domain you own (tenantry.acme.ch, not tenantry.acme.local).

Pick the page of your DNS host. Each page lists the exact variables, the least-privilege token and the install.sh options.

DNS host --dns-provider Page
Infomaniak dns_infomaniak Infomaniak (contains a worked example)
OVHcloud dns_ovh OVHcloud
Cloudflare dns_cf Cloudflare
Gandi LiveDNS dns_gandi_livedns Gandi
Hetzner DNS dns_hetzner Hetzner
AWS Route 53 dns_aws Route 53
Azure DNS dns_azure Azure DNS
deSEC dns_desec deSEC
DigitalOcean dns_dgon DigitalOcean
Linode / Akamai dns_linode_v4 Linode
PowerDNS (API) dns_pdns PowerDNS
Anything else supported by acme.sh dns_xxx + --dns-secret-var Other acme.sh provider

scripts/install.sh --print-dns-providers prints the list with the secret variables and settings of each provider.

HTTP-01

The acme.sh container answers the challenge on port 80. Port 80 of the server must be reachable from the Internet and the name must resolve publicly to this server.

sudo scripts/install.sh --name acme --public-url https://tenantry.acme.ch \
  --teleport-local auto --tls acme-http --acme-email ops@acme.ch

With acme-http, port 80 belongs to the acme container, so there is no HTTP to HTTPS redirect. If Teleport already uses port 443 on the same server, add --https-port 9443 and use https://tenantry.acme.ch:9443 as the public URL.

How it works

The installer adds two containers next to Tenantry (file <dir>/compose.tls.yml):

  • acme: the official neilpang/acme.sh image, pinned by tag and digest, never upgrades itself, cap_drop: ALL plus the three capabilities it needs (CHOWN, FOWNER, DAC_OVERRIDE; NET_BIND_SERVICE for HTTP-01 only). The Docker socket is never mounted. It issues an ECDSA P-256 certificate and renews it with acme.sh --cron (four times a day, renewal 30 days before expiry).
  • caddy: a pinned Caddy that serves https://<name>:<https-port> with static certificate files (Caddy's own ACME is off). A watcher checks the certificate hash every 5 minutes and reloads Caddy after a renewal. Caddy reaches Tenantry on a dedicated edge Docker network, at a fixed address that is the only one Tenantry trusts for X-Forwarded-For (TRUST_PROXY).

Tenantry itself stays on 127.0.0.1. Certificates are published atomically (copy, then rename) in <dir>/certs/.

The same acme.sh sidecar is used by the Steward bundles (certificate read by the console, hot reload) and by the Teleport master bundle (https_keypairs with https_keypairs_reload_interval: 1h, so no restart is needed).

Secrets: never in a bundle or on a command line

  • The DNS API secrets are typed (hidden) at the prompt of install.sh, or read from --dns-credentials-file FILE (one VARIABLE=value per line). They are stored only in <dir>/secrets/acme-dns.env (mode 0600). That file is mounted read-only into the acme container (/run/secrets/acme-dns.env) and sourced by its process: the secrets are never in the container's configured environment, so docker inspect does not show them.
  • The EAB HMAC key never appears on a command line (where any user of the host could read it with ps): the issue script writes it, with the shell's printf builtin, into the CA's ca.conf in the acme-state volume, where acme.sh reads it for the account registration.
  • Non-secret settings (for example OVH_END_POINT, CF_Zone_ID) go in <dir>/acme/provider.env and are passed with --dns-setting VAR=value. The installer refuses a secret variable passed as a setting.
  • acme.sh keeps a copy of the credentials in its acme-state volume for unattended renewals: treat host backups as sensitive.
  • To replace a token: delete <dir>/secrets/acme-dns.env and run install.sh again.
  • Least privilege: a token limited to the zone, dedicated to one host, with an expiry.

Challenge alias

If your DNS host cannot restrict a token to one zone, delegate the challenge: create a CNAME _acme-challenge.<name> pointing to _acme-challenge.<alias zone>, and pass --challenge-alias <alias zone>. The token then only needs rights on the alias zone.

Certificate authorities

--acme-server accepts letsencrypt (default), letsencrypt_test (staging, untrusted: use it to test and avoid rate limits), zerossl (needs --eab-kid; the HMAC key is typed or comes from ACME_EAB_HMAC_KEY in the credentials file), or an ACME directory URL. --acme-email gives the CA a contact address for expiry notices.

Monitoring

Tenantry reads the certificate presented by each Steward console (during the status poll) and by the Teleport proxy (one TLS handshake per round). The alert certificate_expiring fires under 14 days (critical when expired or under 3 days). See Alerts.

Troubleshooting

  • Logs: docker compose -p tenantry-<name> logs acme.
  • Force a renewal: docker compose -p tenantry-<name> exec acme acme.sh --renew -d <name> --ecc --force.
  • Authentication error at issuance: wrong or under-privileged token. Delete <dir>/secrets/acme-dns.env and run install.sh again.
  • --tls-domain ...: a public CA cannot issue for this name: the name ends with .local, .lan, .internal and so on. Use a name under your public domain, or your own ACME server.
  • Test with --acme-server letsencrypt_test first to avoid rate limits.