Private CA
Last updated on October 14, 2026
A self-hosted instance whose certificate comes from a private or corporate CA must be trusted by every station, by Studio on developer machines, and by the SDKs. This page covers the instance and the CLI. The SDKs are listed on the deploy page.
How trust is established
A server cannot prove that its own CA is trustworthy. The station has to be told by a channel it already trusts. TofuPilot supports two:
| Channel | How | When |
|---|---|---|
| Fingerprint in the install command | The instance publishes its CA bundle at /api/instance/ca.pem. The dashboard puts the bundle's SHA-256 into the install command. The CLI downloads the bundle, refuses it unless the hash matches, then logs in with full certificate verification. | Default once SSL_CA_CERT_PATH is set on the instance. |
| File on the station | The operator copies the CA PEM to the station and passes --ca-cert <path> at login. | Instances that do not publish their CA, air-gapped sites, manual recovery. |
The fingerprint is public; it needs integrity, not secrecy. It is copied from a dashboard page that your browser verified, which is what makes it trustworthy. A browser certificate warning clicked through on the dashboard voids that guarantee.
Every certificate in the bundle must be a CA (basicConstraints CA:TRUE) and none may be expired. The deploy script, the dashboard and the CLI all refuse anything else, so the server certificate itself can never become a trust anchor. Self-signed server certificates are therefore not supported for stations: use a private root and issue the server certificate from it.
Configure the instance
Get the CA bundle
Ask the team that issued the server certificate for the root certificate in PEM form, plus any intermediate your stations should trust. Do not extract it from the server: openssl s_client -showcerts returns the server certificate and its intermediates, usually not the root, and the first block it prints is the server leaf.
Set SSL_CA_CERT_PATH
In the instance .env, point SSL_CA_CERT_PATH at the bundle and re-run the deploy script. It validates the bundle (PEM, every certificate a CA, none expired), checks that the server certificate chains to it, mounts it read-only into the dashboard container and prints its SHA-256. After the services start it also connects to the dashboard, storage and realtime hostnames with that bundle as the only trust anchor and reports any that fail.
SSL_CA_CERT_PATH=/etc/tofupilot/ca/ca-root.pemThe key is honoured in every SSL_MODE. Operators who terminate TLS on their own proxy outside the compose stack set it the same way. Keep the bundle in a directory of its own: that directory is mounted into the dashboard container, and the deploy script refuses it if a private key sits next to the bundle. The bundle itself must hold certificates only; a file that also contains a key is refused.
Check the dashboard
The station install card and the Studio connect screen now show "Private CA pinned" with the CA subject and expiry, and their commands carry --ca-cert-sha256 <fingerprint>. The fingerprint must match the one the deploy script printed.
Set up a station
Copy the install command from the dashboard and run it on the station. Nothing else to copy.
curl -fsSL https://www.tofupilot.app/install | sh -s -- --url https://tofupilot.yourcompany.com --token <SETUP_TOKEN> --ca-cert-sha256 <FINGERPRINT>The installer forwards the flags to tofupilot login. The CLI fetches the bundle from the instance, checks the fingerprint, stores the bundle under ~/.tofupilot/ca/ and saves its path with the station credentials. Every later command and the station daemon reuse it.
If the fingerprint does not match, the CLI stops before saving anything and asks for a fresh command from the dashboard. Do not work around that message: either the instance CA was rotated since the command was copied, or the connection is being intercepted.
Studio on a developer machine
The Studio connect screen builds the same command with --org <slug> instead of a setup token. The pin works identically for a user login.
Manual fallback
When the instance does not publish its CA, copy the bundle to the machine and pass it at login:
tofupilot login --url https://tofupilot.yourcompany.com --token <SETUP_TOKEN> --ca-cert /etc/tofupilot/ca/ca-root.pemThe path is saved with the credentials and reused by every command and by the daemon. An unreadable file or one that is not a CA bundle makes the login fail rather than warn. Re-logging in to the same server without --ca-cert keeps the saved CA; --ca-cert "" clears it; TOFUPILOT_CA_CERT=<path> overrides the saved path for one command.
Air-gapped stations cannot run the one-liner, which downloads the CLI from tofupilot.app. Copy the binary by hand, then run the same tofupilot login command: the pinned fetch only contacts the instance.
Rotate the CA
- Add the new root to the bundle, next to the old one, and re-run the deploy script. It prints the new fingerprint, recreates the dashboard container and the install card shows the new pin.
- Wait a few minutes. Each station daemon checks the instance's bundle on its periodic probe over its existing, verified connection, adopts a changed bundle after validating it, and restarts between runs to use it.
- Swap the server certificate to one issued by the new root.
- Remove the old root from the bundle and re-run the deploy script.
A station that was offline during the overlap and comes back after the swap cannot verify the new certificate. Re-run the install command from the dashboard on that station.
Check a station
tofupilot whoamiPrints the CA in effect, its source (environment, stored or instance-pinned), its subject, SHA-256 and expiry. TOFUPILOT_CA_CERT exported in the service environment wins over the stored path for every command except login, which is why the effective one is shown.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
the CA fingerprint does not match at login | The instance CA changed since the command was copied, or the connection is intercepted | Copy a fresh command from the dashboard |
this instance does not publish its CA bundle | SSL_CA_CERT_PATH not set, or the dashboard predates this feature | Set it and re-run deploy, or use the manual fallback |
| Login works, uploads fail with a certificate error | CLI older than 1.7.17 | Update the CLI |
whoami shows environment but you re-pinned | TOFUPILOT_CA_CERT in the service environment | Remove the variable or point it at the new bundle |
| Certificate error naming a different issuer than your CA | A proxy between the station and the instance re-encrypts TLS | Add the proxy's CA to the bundle, or exclude the instance from interception |
Python procedures or uv fail with certificate errors | Child processes do not inherit the CLI's CA | Set SSL_CERT_FILE=<path> in the station environment |
How is this guide?