0004-fediverse-trust-model.md 7510 octets

RFC 0004 — Fediverse: modèle de confiance (allowlist / blocklist + scope d'interop)

  • Auteur : EBO (rustwarden)
  • Date : 2026-05-09
  • Statut : Accepted (auto-revue mainteneur unique 2026-05-09, score 27/35)
  • Apps consommatrices concernées : toute app utilisant rustwarden-core avec la feature experimental-fediverse qui a besoin de contrôler quels domaines Fediverse peuvent interagir avec elle (auth cross-instance, invitations, signaux de lifecycle).
  • Plan associé : docs/plans/2026-05-09-fediverse-identity-federation.md, Phase 1.
  • Dépendances : RFC 0001 (FediverseConfig).

Summary

Introduire le modèle de confiance configurable par l'admin : table fediverse_trusted_domains (allowlist), table fediverse_blocked_domains (blocklist + sévérité), entrées dans app_settings (fediverse_enabled, fediverse_trust_mode, fediverse_interop_scope, fediverse_authorized_fetch, fediverse_node_name, fediverse_node_description), et service FediverseTrustService exposant le check central is_domain_allowed(domain) que les RFCs suivantes appliqueront avant toute opération avec un domaine distant.

Motivation

Sans modèle de confiance, n'importe quelle instance Fediverse peut authentifier ses utilisateurs ou être ajoutée à des équipes — surface d'attaque inacceptable. Le master plan §2.1 a tranché : configuration runtime par admin entre allowlist (zero-trust par défaut) ou open_with_blocklist (modèle Mastodon-classique).

Sans cette RFC, RFC 0006 (auth AP-native) n'a pas de check à appliquer et tout domaine peut tenter une connexion.

Non-goals

  • Pas de check signature HTTP — RFC 0010.
  • Pas de UI admin — rustwarden-ui ajoutera plus tard les écrans /admin/fediverse/....
  • Pas de monitoring / métriques sur les rejets — différé.
  • Pas de propagation transitive (A trust B, B trust C ⇒ A trust C) — explicite seulement.

API proposée

Migration m20260510_000010_fediverse_trust_and_blocks

  • Table fediverse_trusted_domains : id UUID PK, domain TEXT UNIQUE NOT NULL, note TEXT NULL, added_by_user_id UUID NULL (nullable car cycle de bootstrap), created_at TIMESTAMPTZ NOT NULL.
  • Table fediverse_blocked_domains : id UUID PK, domain TEXT UNIQUE NOT NULL, severity TEXT NOT NULL DEFAULT 'suspend' (suspend/silence/reject_auth), reason TEXT NULL, added_by_user_id UUID NULL, created_at TIMESTAMPTZ NOT NULL.
  • Insertions app_settings (clé/valeur, tous nullable au niveau lecture) :
    • fediverse_enabled = false
    • fediverse_trust_mode = allowlist
    • fediverse_interop_scope = rustwarden_only
    • fediverse_authorized_fetch = false
    • fediverse_node_name = Rustwarden
    • fediverse_node_description = (vide)

Entités SeaORM

  • models::fediverse_trusted_domain::Model
  • models::fediverse_blocked_domain::Model

Enums et service

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustMode { Allowlist, OpenWithBlocklist }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InteropScope { RustwardenOnly, RustwardenPlusUniversalRead, Open }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockSeverity { Suspend, Silence, RejectAuth }

pub struct FediverseTrustService { db: DatabaseConnection }

impl FediverseTrustService {
    pub fn new(db: DatabaseConnection) -> Self;

    /// Read-through to `app_settings.fediverse_enabled`.
    pub async fn is_enabled(&self) -> Result<bool, AppError>;
    pub async fn trust_mode(&self) -> Result<TrustMode, AppError>;
    pub async fn interop_scope(&self) -> Result<InteropScope, AppError>;

    /// Central check applied BEFORE any AP signature verification or
    /// fetch towards a remote domain. Returns `false` when:
    /// - the feature is disabled at runtime
    /// - mode = allowlist and the domain is not in trusted (or is in blocked)
    /// - mode = open_with_blocklist and the domain is in blocked
    pub async fn is_domain_allowed(&self, domain: &str) -> Result<bool, AppError>;

    pub async fn add_trusted_domain(&self, domain: &str, note: Option<&str>, added_by: Option<Uuid>) -> Result<Model, AppError>;
    pub async fn remove_trusted_domain(&self, domain: &str) -> Result<bool, AppError>;
    pub async fn list_trusted_domains(&self) -> Result<Vec<Model>, AppError>;

    pub async fn add_blocked_domain(&self, domain: &str, severity: BlockSeverity, reason: Option<&str>, added_by: Option<Uuid>) -> Result<BlockedModel, AppError>;
    pub async fn remove_blocked_domain(&self, domain: &str) -> Result<bool, AppError>;
    pub async fn list_blocked_domains(&self) -> Result<Vec<BlockedModel>, AppError>;
}

is_domain_allowed normalise toujours en lowercase + strip www. éventuel. Domain comparé strictement.

Analyse de compatibilité

  • Non-breaking, additif derrière feature.
  • Schéma DB : 2 nouvelles tables + insertions app_settings. Migration up/down réversible.
  • Pas de modification d'API publique existante.

Alternatives considérées

Alternative A (recommandée) — Tables dédiées + service avec enums

Pros : RBAC clair, requêtes simples, audit trail (added_by_user_id).

Alternative B — Tout dans app_settings (CSV serialisé)

Stocker la liste des domaines comme CSV dans une clé app_settings.

Cons : pas indexable, pas auditable, race conditions sur l'écriture concurrente.

Rejeté.

Alternative C — Configuration en ENV vars

Passer la trust list via env.

Cons : statique, nécessite redéploiement, ne respecte pas la décision plan §2.1 (admin-configurable runtime).

Rejeté.

Testing plan

12 tests :

  1. trust_mode_defaults_to_allowlist_when_setting_missing (pure)
  2. interop_scope_defaults_to_rustwarden_only_when_setting_missing (pure)
  3. severity_parses_known_values (pure)
  4. severity_falls_back_to_suspend_for_unknown (pure)
  5. normalize_domain_lowercases (pure)
  6. normalize_domain_strips_www_prefix (pure)
  7. is_domain_allowed_returns_false_when_disabled (DB-mocked)
  8. is_domain_allowed_returns_true_when_in_allowlist_and_enabled (DB-mocked)
  9. is_domain_allowed_returns_false_when_not_in_allowlist (DB-mocked)
  10. is_domain_allowed_returns_true_in_open_mode_when_not_blocked (DB-mocked)
  11. is_domain_allowed_returns_false_in_open_mode_when_blocked (DB-mocked)
  12. add_trusted_domain_inserts_record (DB-mocked)

Score d'évaluation : 27/35 (auto-revue) → Accepted.

Open questions

Toutes résolues à l'auto-revue : enums TrustMode/InteropScope/BlockSeverity exposés pub, normalisation domain par lowercase + strip www., fallback BlockSeverity::Suspend pour valeurs inconnues.

Score

CritèreNoteCommentaire
Généralité5Modèle de confiance applicable bien au-delà de l'auth.
Stabilité API4Additif gated, mais 2 nouvelles tables publiques.
Performance5Lookups indexés (UNIQUE sur domain), cache éventuel facile à ajouter plus tard.
Sécurité5Hardening net : zero-trust par défaut, gate avant signature AP.
Complexité d'implémentation3~500 lignes (2 entités + service + 12 tests).
Maintenance3Schéma stable, evolution probable mineure (ajouter severity levels).
Documentation2Rustdoc EN, doctest, CHANGELOG ; pas encore d'exemple examples/.
Total27/35≥ 25 → Accepted