Outbound requests: the documents say capture is the only one, and the container fetches GeoNames unasked from a fixed path #366
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#366
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908dand postulo.wiki at de1f52467, comparing what the documents say leaves the instance with the callers ofpostulo.plugins.httpandcore/destinations.py, the container entrypoint and the provisioning commands.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atdocs/PLAN.md:256(the No telemetry line),plugins/fetching.py:163(the capture fetch) and:123(itsrobots.txt), andcore/mail.py:180and:240(the twodestinations.approvecalls, the second inGuardedBackend); the entrypoint, the two provisioning commands and the other locations have not moved.SECURITY.md, the README,docs/PLAN.mdand the wiki promise that Postulo makes no outbound request except a capture a person triggers. The core makes about a dozen kinds of outbound request, several to addresses a person typed, and the container downloads GeoNames' tables at first start with nobody asking, through a client of its own rather than the guarded one. The entrypoint also tests a fixed path for those tables, so an operator who movesPOSTULO_GEOLOCATIONS_DIRgets the download again on every start. An operator who writes egress rules or a privacy notice from these documents gets both wrong.1. The documents name capture as the only outbound request
Where:
SECURITY.md:75-76: "Postulo makes no outbound network calls except URL captures you explicitly trigger."SECURITY.md:32-33: "Nothing is fetched from the network unless a person asked for it".README.md:100anddocs/PLAN.md:243, the same sentence in other words.Hardening.md:192-194in the wiki: capture "is the only thing here that makes your server issue an outbound request to an address somebody else supplied".Configuration.md:654in the wiki, thePOSTULO_CAPTURE_RATErow, which says the same.Installing-Postulo.md:368-369in the wiki: "Postulo makes no request on your behalf unless you say so".Configuration.md:104-105in the wiki is the one place that mentions the download at first start.What happens: besides capture (
plugins/fetching.py:208, and itsrobots.txtat:167), the coreresume/links.py:68);jobs/logos.py:120);plugins/webhook/__init__.py:241) and web push (plugins/browser/webpush.py:222);core/mail.py:190, andGuardedBackendat:250, whichplugins/own_mail/__init__.py:192builds for a person's server);plugins/consent.py:238);plugins/catalogue.py:228,:296);accounts/avatars.py:119);POSTULO_UPDATE_CHECKis on (core/updates.py:51);plugins syncafter an upgrade (docker/entrypoint.sh:19,:26-30).Link checks, logo addresses, webhook receivers and own-mail servers are all addresses a person typed, so capture is not the only request to "an address somebody else supplied" either.
Why it matters: these are the documents an operator reads to decide what the instance may reach. Firewalling on their word breaks webhooks, push and mail sign-in; a privacy notice written from them understates what reaches third parties; and an instance on a monitored or metered network contacts
download.geonames.orgwith no explanation anywhere but one wiki paragraph. Hardening already has a section, "Where the server is allowed to dial" (Hardening.md:407), that describes the guards but not the list.Fix: replace the
SECURITY.mdbullet, the README line and thePLAN.mdline with "no telemetry, and outbound requests only for the features listed in Hardening", and rewordSECURITY.md:32-33to name what is fetched without a person asking (GeoNames at first start, plugin packages on sync after an upgrade, deliveries to connections a person set up, the opt-in update check). Add to Hardening → Where the server is allowed to dial a short table of every outbound destination: what triggers it, whose address it is, which guard applies, and which setting turns it off or bounds it. CorrectHardening.md:192-194and thePOSTULO_CAPTURE_RATErow so they no longer call capture the only fetch of a chosen address (a separate finding of this review covers link checks and logos spending no allowance), and the sentence in Installing Postulo.2. The GeoNames download at start is unasked and skips the guarded client
docker/entrypoint.sh:22-30;jobs/management/commands/fetch_geonames.py:55-61,handle;jobs/management/commands/fetch_esco.py:136, the same pattern;docs/THREAT-MODEL.md:74-77, rule 5.manage.py fetch_geonames, which makes two requests throughhttpx.Client(..., follow_redirects=True): no address check, no pinning, redirects followed wherever they lead. The block has no switch of the kind migrations and plugin sync have (POSTULO_SKIP_MIGRATE,POSTULO_SKIP_PLUGIN_SYNC), so when the web container's fetch fails, the scheduler and the worker each try again at their own start. Rule 5 of the threat model asks every new outbound request to go throughpostulo.plugins.http.client();fetch_geonamesandfetch_escoare the two that do not, and the first is the one that runs with no person behind it.postulo.plugins.http.public_only_client()(it passesfollow_redirectsandheadersthrough), or name operator-run provisioning commands as an exception in rule 5. Either make the start-up fetch opt-in or give it a switch (for examplePOSTULO_SKIP_GEONAMES=1, documented in.env.examplebeside how to provide the two files by hand), and say which in the documents of section 1.3. The entrypoint tests a fixed path, not
POSTULO_GEOLOCATIONS_DIRdocker/entrypoint.sh:26-27tests and creates/app/data/geonames;jobs/places.py:37-39readsPOSTULO_GEOLOCATIONS_DIR, whichdocker/Dockerfile:155sets andConfiguration.md:100in the wiki documents as configurable;jobs/management/commands/fetch_geonames.py:137-140,_write, which does not create the directory it writes into.POSTULO_GEOLOCATIONS_DIR=/app/data/maps.places.DATA_DIRfollows it and the entrypoint does not: every start of every container finds no file at the fixed path, creates/app/data/geonames, and downloads several megabytes again. If the new directory does not exist,_writefails on the missing parent after the download, the map stays empty, and the same happens at the next start."${POSTULO_GEOLOCATIONS_DIR:-/app/data/geonames}/geonames-cities1000.txt"andmkdir -pthe same directory; have_writecreatetarget.parent.Done when
tests/test_fetch_geonames.pyfails while the command builds anhttpx.Clientof its own and passes once it uses the guarded client (or rule 5 names the exception).tests/test_shell_scripts.pyfails whiledocker/entrypoint.shnames/app/data/geonameswithoutPOSTULO_GEOLOCATIONS_DIR, and a test intests/test_fetch_geonames.pyfails while_writecannot write into a directory that does not exist yet.SECURITY.md,README.md,docs/PLAN.md, Hardening, Configuration and Installing Postulo no longer say capture is the only outbound request or that nothing is fetched unasked; Hardening lists every outbound destination the core makes, and the start-up download is named with how to turn it off.The maintainer's list of 2026-10-02 leads to new outbound lookups that the inventory here should name once they exist. None is built yet.
Each is meant to go through the guarded client (
postulo.plugins.http, rule 5 atdocs/THREAT-MODEL.md:74-77) with size, time and address limits, and each is opt-in, run by a button press and never in the background:From the list of improvements of 2026-10-02.