Certificates
You do not need to read this page. The one-time notice on staff pages is harmless, and most rooms never look further. It is here for the Sunday the notice stops being harmless — a new phone, a changed address, an iPhone that will not tap through — and for anyone who wants a clean padlock on every device.
Why there's a notice at all
Lithos serves two addresses on your WiFi:
- A plain http address for listeners. Reading captions and hearing audio needs nothing special, so the congregation joins with no warning of any kind. That is deliberate.
- An https address for anything that uses a microphone — the Presenter page, the Speaker page, the operator page, room speakers, and private groups. Browsers only hand out the microphone on a secure page, full stop.
Secure means a certificate, and a certificate has to be issued for a
name by someone every phone already trusts. A box on church WiFi has
neither: its address is 192.168.1.50, and no public authority will
issue for a private address. So Lithos makes its own certificate, and each
device that meets it asks you once whether to trust it. That question is the
notice.
Tapping through it on your own server is safe: you know what is on the other end. It only matters that the address you typed is really your box.
The built-in certificate
On first run the app issues a certificate for localhost and
the machine's current LAN address (plus any name you give the server with
LT_PUBLIC_HOST), and stores it beside its other data. Each phone or laptop accepts it once and remembers. Two things make a
new certificate, which every device then has to accept again — worth
knowing before a service rather than during one:
- The machine's LAN address changed. The address is written into the certificate. Set the laptop up at home, carry it to church, and it gets a different address — so at startup the app notices and re-issues, carrying the old address along too. A laptop that moves between two buildings keeps working in both; every phone just sees the notice one more time.
- About once a year. Apple devices reject certificates valid for more than 825 days and give you no way to tap through — Safari's "visit this website" link simply does nothing. Lithos therefore issues for 397 days and rolls over 30 days before expiry, so the re-accept lands on an ordinary weekday, not mid-service.
Where to tap
- Android / Chrome / Edge: Advanced → Proceed to 192.168.… (unsafe).
- iPhone / Safari: Show Details → visit this website → Visit Website.
- Firefox: Advanced → Accept the Risk and Continue.
One subtlety explains most "it loaded but nothing works" reports: the notice is shown for the page, but a page's live connection — the WebSocket that actually carries audio — gets no notice, because browsers have nowhere to show one. So the presenter page can load and the microphone silently never connect. After you accept, reload once; the connection then goes through. Current builds detect this and tell you to reload rather than leaving you guessing.
Give the box a fixed address
The single most useful thing you can do, whichever option below you pick or none: give the translation machine a DHCP reservation on the router (or a static IP). A lease that changes re-issues the certificate and breaks every phone's bookmark at once.
Desktop app vs. server
The three options below replace the built-in certificate with one devices
already trust. They apply to the server — the Docker or Node install
from rung 2. The desktop app has no setting for
bringing your own certificate: it always uses the built-in one, stored in its
data folder (%APPDATA%\Lithos Translate\tls on Windows,
~/Library/Application Support/Lithos Translate/tls on a Mac).
That is an honest limitation, not a hidden switch. If a clean padlock on every device matters to your room, the path is the always-on server, which is where a certificate makes sense to manage anyway. On the desktop app the notice is once per device per year, and phones that only listen never see it.
Everything from here on assumes you start the server yourself
and can set environment variables — the same place your API keys and
LT_OPERATOR_PIN go.
mkcert on the LAN
mkcert makes a tiny certificate authority of your own, installs it on the machine, and issues certificates that machine trusts. Install the same authority on the staff phones and laptops, and they see a padlock with no notice — for this server, and for any certificate you ever issue with it.
- On the server machine, once:
That writes two files in the current folder, something likemkcert -install # create and trust the local CA mkcert 192.168.1.50 lithos.local localhost # issue for the LAN address (and any names you use)192.168.1.50+2.pemand192.168.1.50+2-key.pem. - Point the server at them and restart:
Under Docker, mount the folder and pass the same two variables withLT_TLS_CERT=/certs/192.168.1.50+2.pem \ LT_TLS_KEY=/certs/192.168.1.50+2-key.pem \ GEMINI_API_KEY=… LT_OPERATOR_PIN=… npm run server-e. If the server cannot read them it says so at startup (LT_TLS_CERT/KEY unreadable — falling back to self-signed) and carries on with the built-in one. - Install the authority on staff devices.
mkcert -CAROOTprints the folder; the file to copy isrootCA.pem(never the-keyfile). AirDrop or email it to the phone and install it — on iPhone: open the profile, then Settings → General → About → Certificate Trust Settings and switch it on; on Android: Settings → Security → Install a certificate → CA certificate.
Devices with the authority see no notice anywhere. Devices without it still work exactly as before — one tap.
A caution. A root authority installed on a phone is a
powerful thing: whoever holds its key can issue a certificate for any site
that phone will believe. Install it only on devices you own, keep the
rootCA-key.pem file on the server machine and nowhere else, and
prefer Option B for phones that are not yours.
A real certificate for a private address
Public authorities will not issue for an IP like 192.168.1.50.
They will issue for a name — and the way they check you own the name
(the DNS-01 challenge) is by asking you to publish a record in your
domain's DNS. The authority never connects to your server. So a public name
can point at a private address, and the certificate is still real:
church.example.org. A 192.168.1.50 # public DNS, private target
Phones on the church WiFi resolve the name to the LAN address and validate the certificate against the name, like any other site. Off the WiFi the name resolves to an address that goes nowhere — which is exactly what you want.
- Have a domain whose DNS provider has an API (most do — Porkbun, Cloudflare, Namecheap, Route 53…). Add the A record above, pointing at the server's reserved LAN address.
- Issue with acme.sh (any ACME client with your provider's DNS
plugin works; this is the common one):
A wildcard (curl https://get.acme.sh | sh -s email=you@example.org export PORKBUN_API_KEY=… PORKBUN_SECRET_API_KEY=… # or your provider's variables ~/.acme.sh/acme.sh --issue --dns dns_porkbun -d church.example.org ~/.acme.sh/acme.sh --install-cert -d church.example.org \ --fullchain-file ~/lithos-certs/fullchain.pem \ --key-file ~/lithos-certs/privkey.pem \ --reloadcmd 'systemctl restart lithos' # whatever restarts your server-d '*.example.org') costs nothing extra with DNS-01 and saves re-issuing for every future name. - Point the server at it — and name the host:
If phones will reach the box by more than one name, list the others inLT_TLS_CERT=/home/lithos/lithos-certs/fullchain.pem LT_TLS_KEY=/home/lithos/lithos-certs/privkey.pem LT_PUBLIC_HOST=church.example.org # the name goes on QR codes and printed links, and is accepted — see below LT_TLS_PORT=8443 # optional; default 8438 (Docker image: 8443)LT_ALLOWED_HOSTS=name1,name2. - Open
https://church.example.org:8443/presenteron a phone on the WiFi. Padlock, no notice. Put that address on the staff flyer.
Why naming the host is not optional
Point the server at the certificate but skip LT_PUBLIC_HOST /
LT_ALLOWED_HOSTS and you get a failure that looks like a
certificate problem and is not: the page loads with a clean padlock, and then
every live connection is refused —
⚠ The page loaded, but the connection to /operator-ws was refused …
The reason is a guard, not a bug. Live connections are exempt from the
browser's same-origin rule, so the server only accepts them from
localhost, .local names and private addresses.
Otherwise a web page you happened to open elsewhere could point a name of its
own at your LAN address and script connections to your service. A real
hostname that resolves to a private address is indistinguishable from that
trick — and it is precisely what Option B gives you. Naming the host (either
variable will do) tells the server this one is yours. The server reports whether it recognises a
name, so a current build says which of the two problems you have instead of
sending you to accept a notice that will never appear.
Three things that bite
- Use the fullchain, not the bare leaf. iPhones reject a chain
they cannot complete. If your provider ships the leaf and the intermediate
separately:
cat leaf.pem intermediate.pem > fullchain.pem. - Reserve the address. The A record points at one fixed address; a lease change breaks every phone at once and no certificate can save you.
--reloadcmdis not optional. These certificates last 90 days. Without it the renewal lands on disk and the running process keeps serving the expired one — discovered, invariably, on a Sunday morning.
The private key should be chmod 600 and owned by
the account that runs the server. If it arrived in a browser's download
folder, move it out.
A reverse proxy in front
If Caddy, nginx or Traefik already terminates TLS on the box — or you are deploying on a cloud host with a real domain — let it do the certificate and put Lithos behind it. Tell Lithos so with:
LT_BEHIND_PROXY=1
With that set, the app believes the proxy's X-Forwarded-Proto
header about whether a request arrived over https, stops redirecting staff
pages to its own https port, and stops sending phones to that port for
private groups — everything stays on the proxy's one clean address. The
proxy has to send that header and pass WebSockets through (Caddy does both
by default; in nginx that is proxy_set_header X-Forwarded-Proto
$scheme; plus the usual Upgrade/Connection
pair on the location). Name the public host with LT_PUBLIC_HOST
here too, for the same reason as above.
This is also the way to put the main listener service behind https if you want the congregation's stream encrypted on the wire — the proxy serves everything on one clean address, and the one-tap notice is gone for listeners and staff alike.
A minimal Caddyfile, for the record:
church.example.org {
reverse_proxy 127.0.0.1:8080
}
When something's wrong
The page loaded, the notice is gone, but the mic / operator page never connects
LT_PUBLIC_HOST (or LT_ALLOWED_HOSTS) to that
name and restart. The page itself says which of the two it is.Safari says "This Connection Is Not Private" and won't let me continue
openssl x509 -in <data>/tls/cert.pem -noout -text | grep -A1 "Subject Alternative Name"
If the address you are dialing is missing, delete the tls
folder in the data directory and restart: a correct one is regenerated.
(<data> is /data under Docker,
LT_DATA_DIR or the working directory for Node, and the app's
data folder for the desktop app — see above.)Every phone had to accept the notice again this week
It worked for months and then stopped on a Sunday
--reloadcmd to the
acme.sh install step and restart the server now. With the built-in
certificate, the rollover makes a new one every phone accepts once
more — that is expected, not a fault."LT_TLS_CERT/KEY unreadable — falling back to self-signed"
chmod 600, owned by the server's user) and restart.The mkcert padlock is clean on my laptop but not on the phones
Can I skip all this and just use http for staff pages?
http://localhost is
treated as secure by every browser, so a presenter laptop that is the
server never sees a notice. Any other device needs https for the
microphone; there is no switch to change that, because it is the
browser's rule, not ours.Still stuck? Write to support@lithos.community with the server's startup lines (they say which certificate it loaded and which hosts it allows) and a photo of what the phone shows.