Your IPFS Fetch Tells the Network Exactly Which File You Wanted
Content addressing sounds private: you ask for a hash, not a name. But a hash of a file is a fingerprint of that file, so asking for it is the same as saying which file you want. Anyone who can see the request and already knows the file can read your mind.
This post walks through where IPFS exposes that request, who gets to see it, and which mitigations do something rather than just feel tidy.
A CID is the file, in a form you can look up
A CID (content identifier) is built from a cryptographic hash of the data, plus a few bytes saying which hash and which encoding were used. Same bytes, same settings, same CID. That determinism is the whole point of the system, and it is also the leak.
It means a CID is not a secret and not an access token. It is closer to a very precise search term. If I hold a leaked document and you give me a CID, I can check whether they match without ever contacting you.
Quick detour: confirming a guess offline
Kubo (the main Go implementation) can compute a CID without storing or publishing anything:
ipfs add --only-hash --cid-version=1 --raw-leaves suspect.pdf
If the output equals the CID in a log line, a URL or a lookup, you have confirmed the file. The catch is that chunking and DAG layout settings affect the result for larger files, so you need to match the defaults the other side used. For small files that fit in one chunk it is trivially reproducible.
So the weak case is low-entropy content: a known leaked document, a standard form, a banned image. Hashing does not hide something an attacker can already enumerate. The same reasoning applies to any "we only store hashes" design.
Where the request escapes
There are three places a fetch becomes visible. They differ in who the observer is.
- Bitswap to connected peers. Your node asks the peers it is already connected to whether they have the blocks. Each of them learns the CID and your peer ID, and your peer ID is tied to your IP address.
- The DHT lookup. If neighbours do not have it, the node queries the distributed hash table for providers. The peers on the path, especially those whose IDs are closest to the key, see a request for that CID.
- The provider you finally connect to. That peer sees your address and exactly what you pulled.
None of this needs a malicious protocol. It is just how a peer-to-peer lookup works: the question has to be asked of someone.
Publishing is worse than fetching
Serving content means announcing it. A node that holds a block publishes a provider record saying "this peer ID has this CID". That record is public by design, so anyone can ask the DHT who provides a given CID.
ipfs routing findprovs bafkrei...
That turns a CID into a list of peers, and those peers into addresses, often including a home IP. The reverse direction is also useful to an observer: crawl the DHT and you can see what a given peer ID claims to hold.
Note the side effect. If you fetch something, your node may cache it and, depending on the reprovide settings, announce itself as a provider. Reading becomes publishing without you doing anything deliberate.
Public gateways see the lot
Most people never run a node. They use an HTTP gateway, and the gateway operator sees the full URL (so the CID and any path), your IP address, your user agent and the timing. That is the same exposure as any web server log, plus the CID is a stable identifier for the content.
Subdomain gateways put the CID in the hostname, which means it also lands in DNS queries and in the TLS SNI field unless you use encrypted DNS and Encrypted Client Hello. Hang on, that is a nice irony: the "decentralised" route often has more observers on the path than a normal website.
What actually helps
- Encrypt before you add. If the bytes are encrypted with a random key, the CID tells an observer nothing they can test against. Share the key out of band. Remember the CID is still stable, so it still links the same ciphertext across requests.
- Use a private swarm. A shared swarm key limits the network to nodes that hold it, which removes the public DHT from the picture. It is access control for a group, not anonymity within it.
- Control what you announce. Kubo's reprovider settings (check
Reprovider.Strategyin the current docs for your version) decide whether you advertise everything, only pinned content, or only roots. Narrower is quieter. - Hide the network address. A VPN or Tor moves the observer's view of your IP, but not the CID. Fine for unlinking you from the request; useless for hiding the content from the provider.
- Use delegated routing carefully. Sending lookups to one routing service means one party sees them all. That swaps many weak observers for one strong one.
The honest threat model
Ask who you are hiding from. A casual observer on a public Wi-Fi sees little unless you use a gateway. A well-placed DHT participant or gateway operator sees a lot, and can confirm any file they already have.
I would treat IPFS the way I treat any content-addressed or hash-keyed lookup: it protects integrity extremely well and confidentiality not at all. If a CID would embarrass you when read aloud, it needs encryption on the data, not just obscurity in the name.