flintstackbuild-cache daemon · a hosted project
Project status: activeLicense: Apache-2.0Latest: 0.11.3 (2026-09-28)releases feed

Flintstack › Security model

Security model

A build cache hands compiler output to a build. Anything that can write to it can influence the binaries you ship, so the useful question is not «is it encrypted» but «who can write an entry, and what does a reader trust».

Scope

What the daemon does defend:

  • Entry integrity at rest. Every entry carries a digest over its header and payload, checked on read; a bit-rotted or truncated entry is discarded and counted as a miss.
  • Path confinement. The store root is opened once and all access is relative to it; symlinks that point outside are refused rather than followed.
  • Credential file hygiene. Credentials and token files are rejected unless the mode is 0600 and the owner matches the daemon user.
  • Constant-time comparison of the shared token, so a remote cannot learn it one byte at a time.

What it does not defend, and will not pretend to:

  • Confidentiality of your source. Entry payloads are compiler output, stored compressed but not encrypted. Anyone who can read the store can read your object files.
  • A compromised builder. If a machine that is allowed to write the cache is owned, the cache is owned too, and every machine that reads from it inherits the problem.
  • Build hermeticity. An undeclared input is a correctness hole first and a security hole second; the design notes explain why we do not close it.
  • Multi-tenancy. There is one namespace and one token per listener. Two teams who must not read each other's artifacts need two daemons, not one with labels.

Local trust

In the default configuration there is no network at all: a unix socket, the store, and whoever is in the flint group. That group is the security boundary. Membership should mean «is allowed to contribute to builds», and on most CI machines that is exactly the set of users who already run the compiler.

The store directory is deliberately not group-writable. A build can ask the daemon to store an entry; it cannot reach into the shards and rewrite one. That distinction is what keeps an entry's digest meaningful - the digest is computed by the daemon on the way in, not supplied by the caller.

Authentication

Two mechanisms, and they compose:

Shared token

head -c 32 /dev/urandom | base64 > /etc/flint/token
chmod 0600 /etc/flint/token
flintd --listen 0.0.0.0:7914 --token-file /etc/flint/token

The token is sent on every request and compared in constant time. It authenticates the peer as «allowed to use this cache» and nothing more: there are no per-client identities and no revocation list, so rotating the token means restarting both ends. For a rack-local cache behind a firewall that is a reasonable amount of machinery.

Mutual TLS

[remote]
tls_ca = "/etc/flint/ca.pem"
tls_cert = "/etc/flint/client.pem"
tls_key = "/etc/flint/client.key"
verify_name = "cache-b.rack7.internal"

With these keys present the peer transport negotiates TLS 1.3 and requires a client certificate signed by the given authority. This gives you revocation and per-machine identity, which the token alone does not. The listener still wants a token as well; belt and braces on a component this sensitive is not wasted effort.

The objs backend is different. It speaks to an object store over HTTPS with signed requests, so its security is your object store's security: bucket policy, credential scope, and whether you gave the CI role write access when read would have done.

Poisoning

The realistic attack is not breaking the hash. It is convincing a trusted writer to store a bad entry under a key that someone else will later request - through a patched header on one machine, a build flag nobody audits, or simply a compiler that was replaced locally.

Three practical mitigations, in order of value:

  1. One writer. Make the shared tier read_only = true everywhere except on the builders you actually trust. This is the single most effective setting on this page.
  2. toolchain_probe = "digest". Hash the compiler binary rather than its version string, so a locally swapped toolchain produces different keys instead of silently sharing them.
  3. Periodic verification. Build one target with flint wrap --no-store and once with the cache bypassed entirely, then compare the outputs. If your build is reproducible they match byte for byte; if it is not, you have learned something more important than the comparison.

Hardening checklist

CheckWhy
Dedicated unprivileged userthe daemon never needs root after it opens its root and socket
Store mode 0750builds talk to the socket, not to the shards
No --listen unless sharedremoves the entire network surface on single builders
Token and credentials 0600enforced; the daemon exits rather than start with loose modes
Shared tier read-onlylimits poisoning to the machines you chose
Separate store per trust domainthere is no namespacing inside one store
flint doctor in provisioningcatches a store on a network mount before it bites

Reporting

Mail security@flintstack.dev. We answer within three working days, and we would rather hear a weak report than none - the clock-offset bug fixed in 0.11.3 arrived as «this looks wrong but I cannot exploit it», and it was a real cache-correctness hole.

There is no bounty programme and no embargo policy longer than 30 days. Fixes land on the public branch with a note in the changelog, credited if you want to be.

Last updated: 2026-09-28