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:

ChannelHowWhen
Fingerprint in the install commandThe 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 stationThe 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.

.env
SSL_CA_CERT_PATH=/etc/tofupilot/ca/ca-root.pem

The 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.

copied from the dashboard
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:

file on the station
tofupilot login --url https://tofupilot.yourcompany.com --token <SETUP_TOKEN> --ca-cert /etc/tofupilot/ca/ca-root.pem

The 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

  1. 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.
  2. 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.
  3. Swap the server certificate to one issued by the new root.
  4. 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

effective CA
tofupilot whoami

Prints 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

SymptomCauseFix
the CA fingerprint does not match at loginThe instance CA changed since the command was copied, or the connection is interceptedCopy a fresh command from the dashboard
this instance does not publish its CA bundleSSL_CA_CERT_PATH not set, or the dashboard predates this featureSet it and re-run deploy, or use the manual fallback
Login works, uploads fail with a certificate errorCLI older than 1.7.17Update the CLI
whoami shows environment but you re-pinnedTOFUPILOT_CA_CERT in the service environmentRemove the variable or point it at the new bundle
Certificate error naming a different issuer than your CAA proxy between the station and the instance re-encrypts TLSAdd the proxy's CA to the bundle, or exclude the instance from interception
Python procedures or uv fail with certificate errorsChild processes do not inherit the CLI's CASet SSL_CERT_FILE=<path> in the station environment

How is this guide?

On this page