From f4d2cf027c48494e3752e5020facdb13e65ee62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 7 Jun 2026 12:21:51 +0200 Subject: [PATCH] =?UTF-8?q?docs(journal):=20federation=20privacy=20manifes?= =?UTF-8?q?t=20+=20runbook=20+=20honest=20home=20blurb=20(=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10.1 Privacy manifest (legal/privacy): - German legal half: Föderation entry under Empfänger/Drittanbieter — what a public profile exposes, what accepted followers' servers receive, the loss-of-control over delivered copies, Art. 6(1)(a) basis, private profiles don't federate. - English manifest half: dedicated Federation (ActivityPub) section — actor object contents, push delivery + retraction semantics (incl. the tombstone caveat), encrypted-at-rest signing keys, remote actor cache, ingested remote content with the followers-only viewer gate, inbox logging/rate limits. PRIVACY_LAST_UPDATED bumped. 10.2 docs/deployment.md federation runbook: enabling per environment, key rotation posture, abuse monitoring, and the troubleshooting checklist distilled from the 2026-06-06/07 soak (IP families first, no outbox backfill, tombstones, the 10s delivery timeout, actor re-fetch). 10.3 Home marketing blurb (en+de) aligned to what actually ships: Mastodon can follow you + trails-to-trails outbound — no more 'follow friends anywhere' overstatement. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/journal/app/lib/legal.ts | 2 +- apps/journal/app/routes/legal.privacy.tsx | 63 +++++++++++++++++++++ docs/deployment.md | 60 ++++++++++++++++++++ openspec/changes/social-federation/tasks.md | 6 +- packages/i18n/src/locales/de.ts | 2 +- packages/i18n/src/locales/en.ts | 2 +- 6 files changed, 129 insertions(+), 6 deletions(-) diff --git a/apps/journal/app/lib/legal.ts b/apps/journal/app/lib/legal.ts index 9105096..fc2a9be 100644 --- a/apps/journal/app/lib/legal.ts +++ b/apps/journal/app/lib/legal.ts @@ -16,4 +16,4 @@ export const TERMS_VERSION = "2026-04-19"; * require re-acceptance (the policy is informational, not contract), so this * is display-only — not persisted. */ -export const PRIVACY_LAST_UPDATED = "2026-04-20"; +export const PRIVACY_LAST_UPDATED = "2026-06-07"; diff --git a/apps/journal/app/routes/legal.privacy.tsx b/apps/journal/app/routes/legal.privacy.tsx index ce4b14a..303f26c 100644 --- a/apps/journal/app/routes/legal.privacy.tsx +++ b/apps/journal/app/routes/legal.privacy.tsx @@ -260,6 +260,22 @@ export default function PrivacyPage() { , der ohne Query-Logs arbeitet. Eine selbst gehostete Instanz ist geplant. +
  • + Föderation (ActivityPub) – Wenn Ihr Profil auf{" "} + public steht, ist Ihr Konto über das + ActivityPub-Protokoll föderiert: Andere Server (z. B. + Mastodon-Instanzen oder andere trails.cool-Instanzen) können Ihr + öffentliches Profil (Anzeigename, Nutzername, Bio, öffentlicher + Signaturschlüssel) abrufen und Ihnen folgen. An die Server + angenommener Follower übermitteln wir Ihre als{" "} + public markierten Aktivitäten (Titel, Statistiken, + Link). Diese Server liegen außerhalb unserer Kontrolle; was dort + mit zugestellten Inhalten geschieht (Speicherung, Anzeige, + Weiterverbreitung), richtet sich nach deren Richtlinien. + Rechtsgrundlage ist Ihre Einwilligung durch das aktive + Veröffentlichen (Art. 6 Abs. 1 lit. a DSGVO). Private Profile + föderieren nicht. +
  • BRouter – Routenberechnung läuft auf einer von uns selbst gehosteten Instanz. Keine Weitergabe an Dritte. @@ -452,6 +468,53 @@ export default function PrivacyPage() { +
    +

    + Federation (ActivityPub) +

    +

    + Applies only when your profile visibility is public. + Private profiles do not federate at all — no actor object, no + WebFinger, no inbox. +

    +
      +
    • + Your actor object (fetchable by any fediverse server) exposes: + username, display name, bio, profile link, and your public + signing key. Never your email, never private content. +
    • +
    • + Activities you mark public are pushed to the + servers of your accepted remote followers and listed in your + public outbox. Once delivered, copies live on those servers + under their policies — un-publishing sends a retraction, but + remote deletion cannot be guaranteed (and remote servers that + processed a retraction will not re-show a later re-publish). +
    • +
    • + Signing keys: one RSA keypair per user. The private key is + stored encrypted at rest and never leaves this server. +
    • +
    • + Remote actor cache: for accounts that interact with this + instance we store their public profile basics (handle, display + name, inbox/outbox URLs, public key) to render follower lists + and feeds without re-fetching. +
    • +
    • + Remote content: public (or followers-only) activities from + trails users you follow on other instances are cached here for + your feed — followers-only items are shown only to the + follower whose follow brought them in. +
    • +
    • + Inbox traffic (signed requests from other servers) appears in + the standard server logs (14-day retention, see section 4) and + is rate-limited per source instance. +
    • +
    +
    +

    Sentry

      diff --git a/docs/deployment.md b/docs/deployment.md index f4998e8..3a785ed 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -102,6 +102,66 @@ curl -sf https://trails.cool/api/health && curl -sf https://staging.trails.cool/ Plan it as a short maintenance window (~2–3 min downtime); don't ship network-option changes expecting the workflows to apply them. +## Federation runbook + +Federation (ActivityPub via Fedify) is gated by `FEDERATION_ENABLED=true` +per environment. Currently: staging **on**, production **off**. The full +change history and design live in `openspec/changes/social-federation/`. + +### Enabling on an environment + +1. Ensure `FEDERATION_KEY_ENCRYPTION_KEY` is in `secrets.app.env` + (SOPS) — keypair generation fails closed in production without it. +2. Set `FEDERATION_ENABLED=true` in the environment's compose env (see + `cd-staging.yml` for the staging pattern). +3. First boot with the flag on enqueues `backfill-user-keypairs` + (generates RSA keys for existing users — idempotent) and registers + the federation jobs (`deliver-activity`, `poll-remote-actor`, + `poll-remote-outboxes`, `federation-kv-sweep`). +4. Smoke: `curl -s "https:///.well-known/webfinger?resource=acct:@"` + (200 for a public user, 404 otherwise) and `/nodeinfo/2.1`. + +`FEDERATION_LOG_LEVEL=debug` turns on Fedify's per-request signature +diagnostics (staging runs debug during soaks; dial back to `info`). + +### Key rotation + +Rotating `FEDERATION_KEY_ENCRYPTION_KEY` requires re-encrypting every +`users.private_key_encrypted` (decrypt with old, encrypt with new) — +there is no script yet; write one against +`apps/journal/app/lib/federation-keys.server.ts` primitives when first +needed. Rotating a single user's keypair: NULL their key columns and +let the backfill regenerate (remotes pick the new key up from the actor +document; deliveries signed with the old key fail until then). + +### Abuse monitoring + +- Inbox is rate-limited 60 req/5 min per source instance + (`federationSourceHost`); 429s show in journal logs. +- Outbound is paced (1 req/s delivery, 1 req/5 s polling per host). +- Watch `docker logs ` for `fedify·federation` lines; Loki + picks them up. No per-instance blocklist yet — see + `docs/ideas/instance-administration.md` (manual emergency lever: + block the source IP/host in Caddy). + +### Troubleshooting deliveries + +Hard-won checklist from the 2026-06-06/07 soak (details in +`openspec/changes/social-federation/design.md`): + +1. **Test both IP families from inside the journal container** before + suspecting signatures (`family: 6` vs `4` — dangling A records and + v6-only instances are common; our containers have IPv6 since + #466/#468). +2. Remote shows the post count but no posts → remotes never backfill + outbox history; only pushed or individually-fetched objects appear. +3. Delivered but invisible, no errors anywhere → check the remote's + `tombstones` table; a previously-retracted URI is refused forever. +4. Mastodon gives deliveries a 10s read timeout — anything synchronous + and slow in the inbox path breaks federation silently. +5. Actor changes (profile fields, keys) need the remote to re-fetch: + `tootctl accounts refresh user@domain` on a Mastodon you control. + ## cd-brouter manual trigger ```bash diff --git a/openspec/changes/social-federation/tasks.md b/openspec/changes/social-federation/tasks.md index 5e5eced..3684b5b 100644 --- a/openspec/changes/social-federation/tasks.md +++ b/openspec/changes/social-federation/tasks.md @@ -76,9 +76,9 @@ ## 10. Privacy + docs -- [ ] 10.1 Update privacy manifest: federation traffic, remote actor cache, what data is exposed in the actor object (display name, avatar, public key) -- [ ] 10.2 `docs/deployment.md`: federation runbook — feature flag, key rotation procedure, abuse monitoring, troubleshooting -- [ ] 10.3 Soften the Journal home "Federated by design" blurb on flagship now (or align it to "ActivityPub federation: inbound + trails-to-trails outbound" once this lands) +- [x] 10.1 Update privacy manifest: federation traffic, remote actor cache, what data is exposed in the actor object (display name, avatar, public key) +- [x] 10.2 `docs/deployment.md`: federation runbook — feature flag, key rotation procedure, abuse monitoring, troubleshooting +- [x] 10.3 Soften the Journal home "Federated by design" blurb on flagship now (or align it to "ActivityPub federation: inbound + trails-to-trails outbound" once this lands) ## 11. Testing diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 347aaba..a1c8d84 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -192,7 +192,7 @@ export default { }, federation: { title: "Föderiert by design", - body: "Deine Journal-Instanz spricht via ActivityPub mit anderen trails.cool-Servern. Folge Freundinnen und Freunden überall.", + body: "Teil des Fediverse via ActivityPub: Mastodon-Nutzer:innen können dir folgen und deine öffentlichen Aktivitäten sehen, und du folgst trails-Nutzer:innen auf jeder Instanz.", }, ownership: { title: "Deine Daten, immer", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 26b26a3..f6ee07f 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -192,7 +192,7 @@ export default { }, federation: { title: "Federated by design", - body: "Your Journal instance talks to other trails.cool servers via ActivityPub. Follow friends anywhere.", + body: "Part of the fediverse via ActivityPub: Mastodon users can follow you and see your public activities, and you can follow trails users on any instance.", }, ownership: { title: "Your data, always",