Security & Threat Model
The short version: Tempo protects your tokens and your data at rest, runs no cloud and no telemetry, and assumes the network it sits on is a home LAN you control. By default the ingestion port speaks plain HTTP — a deliberate, documented choice for a trusted LAN, not an oversight. Since 1.1 you can also turn on a native TLS listener and require it per sender. This page states all of that without hand-waving. Secure by option, honest by default.
The trust boundary Tempo assumes
Tempo is built for homelab and self-hosted setups: Home Assistant, a NAS,
UniFi controllers, backup and monitoring stacks, your own scripts — usually
running on dedicated hosts, not on the same Mac as Tempo. To receive their
events, Tempo's ingestion listener binds 0.0.0.0 and is
reachable from other machines on your local network. The threat model
treats your LAN as a semi-trusted zone: the devices on it
are ones you own or have admitted, and you are the network's administrator.
This is the same posture most homelab tooling takes, and it is the
assumption everything below is calibrated against.
If that assumption does not hold for your network, the in-transit and hardening sections tell you exactly what changes and what to do about it.
What is protected at rest
These protections ship in Tempo 1.x today. The architecture & security post covers the reasoning in more depth; the summary:
- Per-provider tokens. Every sender gets its own token, bound to a provider-identifier prefix. Tokens are not shared, not derived from a master secret, and not bundled with the app. A leaked token can impersonate that one sender only — it cannot pose as another provider or escalate to an app-wide compromise — and you revoke each one independently.
- Keychain-stored secrets. Tokens live in the macOS
Keychain, scoped to the Tempo process — not in
UserDefaults, flat files, or environment variables. Access mediation is enforced by the OS. - Strict input validation and rate limiting. Every accepted request passes a centralized validator: unknown fields are rejected (not silently absorbed), every string has a size cap, and URL schemes in action triggers are explicitly allowlisted. Per-token rate limits stop a misconfigured sender from drowning the database.
- Local audit log. Accepted and rejected requests are recorded through macOS unified logging — enough metadata to reconstruct an incident (token, source IP, response code, timestamp), never the payload contents. A rejection inspector in the app surfaces refused requests so a misconfigured or hostile sender is visible, not silent.
- Local data, no telemetry. The SQLite database stays on your disk. There is no third-party analytics SDK, no phone-home, no license server — v1 is freeware. Backups go to a path you choose. The full data story is on the privacy page.
In transit, by default: cleartext HTTP
This is the part a careful reader most needs stated plainly, so here it is:
by default, Tempo's ingestion listener speaks plain HTTP.
(Since 1.1 you can turn on native TLS — see
encrypted ingestion below; this section is about the
default.) A
device that can capture packets on the same LAN segment — a machine running
tcpdump or Wireshark, a compromised switch, an attacker on the
same Wi-Fi — can read the per-provider token in the request header and the
JSON payload, including any sensitive fields you put in the event
(hostnames, IPs, account identifiers, message bodies). HTTP does not encrypt
any of that.
Why is the default plain HTTP, given how carefully the tokens are protected at rest? Two concrete reasons, not laziness:
- The calibrated threat model. The default assumes a home LAN you administer, where passive packet capture by a hostile party is not the primary risk. For that environment, HTTP keeps setup frictionless and every webhook source works out of the box.
- Mandatory TLS would break senders, not just add a lock. A self-signed certificate that every third-party tool had to accept would lock out the many webhook sources that cannot skip certificate verification or import a custom CA. Forcing TLS on everyone trades a narrow risk for broad breakage.
So the HTTP default is a documented decision for a trusted LAN — but it is a real limit, and it is stated as one rather than glossed over. If your network is not one where you trust every device that can see the wire, read on.
When the HTTP default is fine — and when it is not
Fine, as-is:
- A home network you administer, wired or on Wi-Fi you control.
- No untrusted guests, IoT, or capture-capable devices sharing the segment with Tempo and its sources.
Reconsider, and harden:
- A shared office, coworking, or apartment-building LAN.
- A flat segment that also carries guest devices or untrusted IoT.
- Anywhere a peer could plausibly run a packet sniffer.
- Anywhere a sender's payload carries data you would treat as sensitive — credentials, personal identifiers, regulated information.
Hardening you can do today
All of these work with the current release. Pick what fits your network:
- Loopback-only. If every source runs on the same Mac as
Tempo, Settings → Ingestion → Limit to loopback only rebinds the
listener to
127.0.0.1, and nothing crosses the wire at all. - Network segmentation. Put Tempo and its sources on a trusted VLAN, isolated from guest and IoT segments. This shrinks the set of devices that can see the traffic to ones you already trust.
- A VPN or mesh overlay. Carry cross-host traffic over Tailscale, WireGuard, or a similar overlay so the network layer encrypts it, independent of Tempo. Point each sender at the overlay address.
- Native TLS, or a proxy in front. Since 1.1 Tempo has a built-in TLS listener (see encrypted ingestion below) — the first-class way to encrypt the wire. If you prefer, a small Caddy, nginx, or stunnel in front of Tempo's port still works: it lets TLS-capable senders connect over HTTPS while the proxy forwards plaintext to Tempo on loopback.
- Scope and rotate tokens. One token per sender is already the model — keep it that way, and rotate any token you suspect was exposed. Revocation is independent and immediate.
Encrypted ingestion (since 1.1)
Tempo 1.1 added a native TLS listener. It is off by default — the plain LAN path stays the frictionless default — and you turn it on in Settings → Ingestion → Secure (TLS). How it works:
-
A TLS port alongside the plain one —
8776by default, separate from the cleartext7776. The two coexist, so a mixed fleet (some senders speak HTTPS, some don't) keeps working while you move one sender at a time. -
A self-signed certificate, generated the first time you enable
TLS — local only, no certificate authority and no external
service. Download it from Settings → Security → TLS certificate to
pin it in senders that verify (
curl --cacert, a CA file in a library's TLS context); a sender can also encrypt without verifying when pinning is impractical. The app warns 60 days before the certificate expires, and re-mints it if your Mac's LAN IP changes. -
A per-provider
secureflag — set Require TLS on a token and that sender is refused on the cleartext port with an opaque 401, so a stolen token replayed over plain HTTP, or a sender that silently reverted, is rejected and surfaced rather than quietly downgraded. A padlock on the source shows its transport state and turns orange when a downgrade attempt hits it.
The cleartext default does not go away — it stays the frictionless path for simple, trusted LANs. You opt an individual sender into TLS when its data or its network warrants it. That is what secure by option, honest by default means in practice. (This supersedes the "TLS is a v2 item" note in the earlier architecture post.) Loading your own certificate in place of the self-signed one is planned for a later 1.x update.
What Tempo does not defend against
Stated explicitly, so the boundaries are clear:
- A compromised host. If the Mac running Tempo is itself compromised, the local database and the Keychain-scoped tokens are within reach of that attacker. Tempo protects against network and sender-level threats, not against an adversary who already owns the machine.
- A leaked token used from inside the trusted zone. Until you revoke it, a stolen token impersonates its one sender. Per-provider scoping limits the blast radius to that single provider; it does not make a leaked token harmless. Revoke it.
- Intrusion detection. Tempo validates and rate-limits input; it is not a WAF or an IDS and does not inspect payloads for attack signatures.
- Autonomous action. This one cuts the other way, in your favour: Tempo never acts on a payload by itself. A hostile sender cannot make Tempo run a command, open a URL, or write to a source — every action is a human click. The payload is input; your clicks are the only authority that fires anything.
Reporting a vulnerability
If you find a security issue, please report it privately first at [email protected] rather than opening a public issue, and give a reasonable window to fix it before disclosure. General security questions and feedback are welcome on the tempo-scores GitHub repo and in the Discord.
Changes to this page
This is a living document. When the security posture changes in a substantive way, the date at the top will change and the change will be noted in the release notes of the version that introduced it.