543 lines
20 KiB
Markdown
543 lines
20 KiB
Markdown
# Source tierce dans ProcessingAccess
|
|
|
|
## Contexte
|
|
|
|
`ProcessingResourceAccess` (oc-lib) expose trois champs :
|
|
|
|
```go
|
|
type ProcessingResourceAccess struct {
|
|
Source string // URL ou identifiant de la source tierce
|
|
IsReachable bool // true = accessible publiquement, false = chez un peer privé
|
|
Container *models.Container // nil si on passe par Source
|
|
}
|
|
```
|
|
|
|
Quand `Container` est nil et `Source` non vide, le workflow builder doit gérer l'accès à la source tierce selon la valeur de `IsReachable`.
|
|
|
|
---
|
|
|
|
## Cas 1 — `isReachable = true` (source publique)
|
|
|
|
La source est accessible via une URL publique (HTTP/S3 public, etc.).
|
|
|
|
**Comportement dans le builder :**
|
|
|
|
- Insérer une step Argo **avant** la step de processing courante
|
|
- Cette step effectue un `curl` de la source vers le storage lié au processing
|
|
- Si aucun storage lié → erreur immédiate
|
|
- La commande du processing devient `<storage_mount_path>/<nom_du_fichier>`
|
|
|
|
```
|
|
[step précédente] → [step curl download] → [step processing]
|
|
↓
|
|
storage lié (S3 ou local)
|
|
```
|
|
|
|
---
|
|
|
|
## Cas 2 — `isReachable = false` (source privée chez le peer dépositaire)
|
|
|
|
La source (`cmd.bin`) se trouve sur le réseau privé de PeerA, inaccessible de l'extérieur.
|
|
L'exécution a lieu **sur B**, avec des protections pour limiter l'extraction du binaire.
|
|
|
|
### Principe général
|
|
|
|
PeerA expose temporairement le binaire via un **Minio partagé à usage unique**,
|
|
avec des credentials éphémères. B exécute le binaire en mémoire sans le persister sur disque.
|
|
|
|
---
|
|
|
|
### Protocole étape par étape
|
|
|
|
#### 1. Demande d'accès via NATS (à la construction du template)
|
|
|
|
`oc-monitord` (B) envoie un message NATS à `oc-discovery` :
|
|
|
|
```
|
|
Subject : PB_SOURCE_REQUEST
|
|
Payload : {
|
|
executions_id : string,
|
|
source_key : string, // clé opaque — B ne connaît pas le path réel
|
|
peer_id_src : string, // PeerA
|
|
peer_id_dst : string, // PeerB
|
|
ttl : duration, // durée du booking
|
|
coupling : { // résumé du couplage pour vérification AE côté A
|
|
compute_peer : string,
|
|
storage_peers : []string,
|
|
data_peers : []string
|
|
}
|
|
}
|
|
```
|
|
|
|
`oc-discovery` transmet la demande à A et attend sa réponse.
|
|
|
|
#### 2. A vérifie les AE et génère une pre-signed URL à usage unique
|
|
|
|
A reçoit la demande, vérifie :
|
|
1. Que `source_key` correspond bien à une source qu'il détient
|
|
2. Que le couplage décrit respecte ses **Autorisations d'Exploitation** (voir section dédiée)
|
|
|
|
Si les deux conditions sont remplies, A monte le fichier réel dans son Minio interne
|
|
et génère une pre-signed URL Minio avec :
|
|
|
|
- `max-download-count = 1` → révoquée après le premier GET
|
|
- TTL = durée du booking
|
|
|
|
Le path réel n'est jamais transmis à B — seule l'URL pre-signed est retournée.
|
|
Si une AE est violée → refus + événement `AE_VIOLATION` (voir section AE).
|
|
|
|
#### 3. `oc-monitord` crée un Kubernetes Secret éphémère
|
|
|
|
La pre-signed URL revient à B via la réponse NATS.
|
|
`oc-monitord` crée un Secret Kubernetes dans le namespace Argo **juste avant** la soumission
|
|
du workflow, avec une `ownerReference` sur le workflow (suppression automatique à la fin) :
|
|
|
|
```yaml
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: source-<executions_id>
|
|
ownerReferences:
|
|
- kind: Workflow
|
|
name: <workflow_name>
|
|
data:
|
|
url: <base64(presigned_url)>
|
|
```
|
|
|
|
L'URL n'apparaît **jamais** dans le spec du workflow (pas de trace dans `kubectl get workflow -o yaml`).
|
|
|
|
#### 4. Génération de la step Argo
|
|
|
|
Le builder injecte un wrapper script comme commande de la step processing :
|
|
|
|
```sh
|
|
curl -s "$PRESIGNED_URL" -o /dev/shm/.exec
|
|
chmod +x /dev/shm/.exec
|
|
/dev/shm/.exec "$@" &
|
|
PID=$!
|
|
rm -f /dev/shm/.exec
|
|
wait $PID
|
|
```
|
|
|
|
La variable `PRESIGNED_URL` est injectée via `env.valueFrom.secretKeyRef` → jamais en clair dans le spec.
|
|
|
|
Le volume éphémère est déclaré en mémoire :
|
|
|
|
```yaml
|
|
volumes:
|
|
- name: ephemeral-bin
|
|
emptyDir:
|
|
medium: Memory # tmpfs — rien sur disque, détruit avec le pod
|
|
```
|
|
|
|
---
|
|
|
|
### Protections et limites
|
|
|
|
| Vecteur d'attaque | Protection | Efficacité |
|
|
|---|---|---|
|
|
| Re-téléchargement depuis l'URL | Usage unique — URL révoquée après le 1er GET | Forte |
|
|
| Copie du fichier depuis le pod (`kubectl exec cp`) | `rm` immédiat après exec — fichier absent du FS | Forte (non-root) |
|
|
| Extraction depuis le disque du node | `medium: Memory` — rien écrit sur disque | Forte |
|
|
| Lecture de l'URL dans le spec Argo | Kubernetes Secret + `valueFrom.secretKeyRef` | Forte |
|
|
| Identité / path réel de la source | Clé opaque — B ne connaît que la clé, pas le path | Forte |
|
|
| `/proc/PID/exe` (root sur le node B) | **Aucune** — voir ci-dessous | Nulle |
|
|
| `/proc/PID/mem` + `ptrace` / `gdb` (root) | **Aucune** | Nulle |
|
|
| `criu dump` (snapshot mémoire container) | **Aucune** | Nulle |
|
|
|
|
### Angle mort : `/proc/PID/exe`
|
|
|
|
Le `rm` sur `/dev/shm/.exec` supprime l'entrée dans le répertoire mais **pas l'inode** — le kernel
|
|
maintient une référence ouverte tant que le process tourne. Un admin root sur le node de B peut
|
|
récupérer le binaire intact en une commande pendant l'exécution :
|
|
|
|
```sh
|
|
cp /proc/$(pgrep .exec)/exe /tmp/recovered_binary
|
|
```
|
|
|
|
De même, `/proc/PID/mem` combiné à `/proc/PID/maps` permet de dumper les segments text/data,
|
|
et `ptrace` / `gdb --pid` d'attacher un debugger. Ces vecteurs sont **incontournables** par
|
|
des moyens purement logiciels si B est root sur son propre cluster.
|
|
|
|
Les protections en place couvrent donc :
|
|
- Les utilisateurs non-root / autres pods
|
|
- La persistence accidentelle (disque, logs, artefacts)
|
|
- Le re-téléchargement après exécution
|
|
|
|
Elles **ne protègent pas** contre un opérateur de B activement malveillant avec accès root.
|
|
|
|
---
|
|
|
|
### Options si le niveau de confiance en B est faible
|
|
|
|
#### ~~Option 1 — Exécution chez A~~ *(écarté)*
|
|
|
|
Dispatcher la step Argo sur l'infrastructure de A via Admiralty / virtual kubelet.
|
|
**Écarté** : le service couple un datacenter — le couplage physique d'infrastructure est
|
|
hors scope dans l'architecture actuelle.
|
|
|
|
#### Option 2 — Confidential Computing (Intel TDX / SGX)
|
|
|
|
La mémoire du pod est chiffrée au niveau hardware. Inaccessible au kernel et à root.
|
|
Nécessite du hardware compatible côté B — non déployable universellement.
|
|
|
|
#### Option 3 — Binaire chiffré + clé en mémoire à usage unique
|
|
|
|
A transmet un binaire chiffré (AES-GCM). Un sidecar sécurisé injecte la clé de déchiffrement
|
|
en mémoire uniquement, via un channel éphémère (ex. socket Unix dans le pod). Le loader déchiffre
|
|
en RAM et exécute sans jamais écrire le binaire en clair.
|
|
|
|
- `/proc/PID/exe` → blob chiffré inutilisable ✅
|
|
- `/proc/PID/mem` → expose le binaire déchiffré à l'exécution ❌ (angle mort résiduel)
|
|
|
|
Complexe à implémenter, ralentit le démarrage, mais ferme le vecteur `exe`.
|
|
|
|
#### Option 4 — OCI Image Encryption (ocicrypt) pour les images de conteneur
|
|
|
|
Complémentaire de l'Option 3 pour les images Docker (pas les binaires bruts).
|
|
Les layers de l'image sont chiffrés dans le registry. La clé est délivrée par un KMS
|
|
uniquement au moment du pull, uniquement à un kubelet autorisé.
|
|
Ce que root voit sur le disque du node = des blobs chiffrés inutilisables.
|
|
Supporté par containerd avec plugin.
|
|
|
|
> **Pourquoi c'est nécessaire :** quand containerd pull une image, les layers sont stockés
|
|
> dans `/var/lib/containerd/` sur le node. Un root peut les exporter intégralement :
|
|
> `ctr images export image.tar registry/image:tag`. Les credentials éphémères limitent
|
|
> le re-pull mais pas la copie de ce qui est déjà sur le node.
|
|
|
|
---
|
|
|
|
### Synthèse — choix selon le niveau de confiance en B
|
|
|
|
| Niveau de confiance en B | Solution recommandée |
|
|
|---|---|
|
|
| Peer de confiance (contrat, audit) | Protections best-effort actuelles + AE |
|
|
| Peer partiellement de confiance | Option 3 (binaire chiffré) + AE |
|
|
| Peer non de confiance | Option 2 (Confidential Computing) + AE |
|
|
|
|
### Architecture de protection par couches
|
|
|
|
```
|
|
Couche 1 — Protections techniques → limitent l'extraction physique (tmpfs, URL unique, Secret K8s)
|
|
Couche 2 — Chiffrement (Options 3/4) → limitent l'utilité d'une extraction réussie
|
|
Couche 3 — Autorisations d'Exploitation (AE) → limitent l'usage contextuel (couplage, peers autorisés, quota)
|
|
Couche 4 — Licences / Consentements → cadre légal et traçabilité opposable
|
|
```
|
|
|
|
---
|
|
|
|
## Autorisations d'Exploitation (AE)
|
|
|
|
### Concept
|
|
|
|
Au-delà de protéger l'accès à une ressource, A contrôle **dans quel contexte** sa ressource
|
|
peut être couplée dans un workflow. L'AE répond à la question :
|
|
|
|
> "Mon Processing P peut être utilisé chez B, **seulement** avec le Storage S de C
|
|
> et le Compute K de B. Toute autre combinaison est refusée."
|
|
|
|
On ne peut pas coupler dans un workflow une Data + un Storage d'un pair donné,
|
|
ou un Processing avec un Compute d'un pair donné, sans que le détenteur de ces ressources
|
|
ait explicitement accordé cette association.
|
|
|
|
### Modèle de données (oc-lib)
|
|
|
|
```go
|
|
type ExploitationAuthorization struct {
|
|
ID string
|
|
GrantorPeer string // A — celui qui autorise
|
|
GranteePeer string // B — celui qui reçoit le droit
|
|
Resource ResourceRef // la ressource concernée (Processing, Data, Storage...)
|
|
|
|
Conditions ExploitAuthConditions
|
|
License *ResourceLicense // lien avec le modèle licence
|
|
}
|
|
|
|
type ExploitAuthConditions struct {
|
|
AllowedComputePeers []string // nil = tout peer autorisé
|
|
AllowedStoragePeers []string
|
|
AllowedDataPeers []string
|
|
AllowedProcessingPeers []string
|
|
|
|
MaxExecutions int // -1 = illimité
|
|
ExpiresAt *time.Time
|
|
ConsentRequired bool
|
|
}
|
|
```
|
|
|
|
### Stockage des AE — oc-catalog (copie distribuée)
|
|
|
|
Les AE sont publiées dans **oc-catalog**, dont chaque peer détient une **copie locale synchronisée**.
|
|
|
|
**Rationale :** même si un peer B tente de tricher en construisant un workflow non autorisé,
|
|
A dispose de sa propre copie des règles et peut analyser le couplage reçu dans le payload
|
|
`PB_SOURCE_REQUEST` au moment du booking, puis **refuser** si une AE est violée.
|
|
|
|
Ce mécanisme est auto-défensif : A ne dépend pas de la bonne foi de B pour appliquer ses règles.
|
|
|
|
### Violation d'AE — acte critique et score de réputation
|
|
|
|
Tenter de contourner une AE est un **acte critique** qui est :
|
|
|
|
1. Détecté par A au moment du booking (vérification indépendante côté A)
|
|
2. Enregistré dans oc-catalog via un événement `AE_VIOLATION`
|
|
3. Sanctionné par une **dégradation sérieuse du score de réputation** de B
|
|
|
|
Un peer dont le score s'effondre perd progressivement la possibilité de booker
|
|
des ressources chez d'autres peers — c'est la dissuasion réseau.
|
|
|
|
```
|
|
Subject : AE_VIOLATION
|
|
Payload : {
|
|
violator_peer_id : string,
|
|
grantor_peer_id : string,
|
|
resource_id : string,
|
|
workflow_id : string,
|
|
timestamp : time,
|
|
attempted_coupling : { // couplage tenté vs. AE en vigueur
|
|
compute_peer : string,
|
|
storage_peers : []string,
|
|
data_peers : []string
|
|
}
|
|
}
|
|
```
|
|
|
|
### Vérification dans le workflow builder (côté B)
|
|
|
|
Au moment de la construction du template, avant soumission Argo, B vérifie
|
|
en local (sur sa copie des AE) la légitimité du couplage :
|
|
|
|
```go
|
|
for _, res := range workflow.Resources {
|
|
if res.PeerID != localPeerID {
|
|
ae, err := catalog.GetExploitationAuthorization(res.PeerID, localPeerID, res.ID)
|
|
if err != nil || !ae.AllowsCoupling(workflow.ComputePeer, workflow.StoragePeers, workflow.DataPeers) {
|
|
return nil, ErrUnauthorizedCoupling{Resource: res.ID, Peer: res.PeerID}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
La vérification côté B est une première barrière ; la vérification côté A au booking
|
|
est la barrière souveraine (celle que B ne peut pas contourner).
|
|
|
|
### Ce que les AE résolvent vs. les protections techniques
|
|
|
|
| Vecteur | Protection technique | AE |
|
|
|---|---|---|
|
|
| Copie du binaire | Partielle (loader chiffré, tmpfs) | ✗ hors scope |
|
|
| Ré-exécution dans un workflow non autorisé | ✗ aucune | ✅ couplage rejeté + score dégradé |
|
|
| Exfiltration vers un storage non autorisé | ✗ aucune | ✅ storage peer non listé → rejet |
|
|
| Usage du Processing avec une Data non autorisée | ✗ aucune | ✅ data peer non listé → rejet |
|
|
| Traçabilité légale | Partielle (logs) | ✅ violation enregistrée dans oc-catalog |
|
|
| Dissuasion | Faible | ✅ dégradation de score = conséquence réseau réelle |
|
|
|
|
---
|
|
|
|
## Licences et Consentements (oc-lib `Resource`)
|
|
|
|
Couche légale complémentaire aux AE. Chaque ressource porte ses conditions d'usage.
|
|
|
|
```go
|
|
type ResourceLicense struct {
|
|
SPDX string // ex. "Apache-2.0", "Proprietary"
|
|
ConsentURL string // lien vers les CGU / texte de licence complet
|
|
ConsentRequired bool // si true → workflow bloqué jusqu'à ACK explicite
|
|
MaxExecCount int // -1 = illimité, sinon quota d'exécutions
|
|
ExpiresAt *time.Time
|
|
}
|
|
```
|
|
|
|
Le workflow builder bloque la soumission si `ConsentRequired && !consent_recorded`.
|
|
Le consentement est enregistré dans oc-catalog (horodatage + identité du peer)
|
|
pour constituer une trace opposable.
|
|
|
|
Le `MaxExecCount` croise avec la pre-signed URL à usage unique (Cas 2) — les deux
|
|
limiteurs se renforcent mutuellement.
|
|
|
|
---
|
|
|
|
## Changements dans le workflow builder
|
|
|
|
```
|
|
if access.Container == nil && access.Source != "" {
|
|
if access.IsReachable {
|
|
// Ajoute une step curl avant la step courante
|
|
// Commande = <storage_mount>/<filename>
|
|
} else {
|
|
// 1. Vérifier les AE locales pour le couplage du workflow
|
|
// 2. waitForConsiders(PROCESSING_RESOURCE, peerA) via NATS
|
|
// → payload inclut le résumé de couplage (coupling{}) pour vérification AE côté A
|
|
// → reçoit presigned URL en réponse (NATS reply), ou erreur AE_VIOLATION
|
|
// 3. Crée K8s Secret éphémère avec ownerRef sur le workflow
|
|
// 4. Injecte volume emptyDir medium:Memory
|
|
// 5. Injecte wrapper script + env secretKeyRef dans la step
|
|
}
|
|
}
|
|
```
|
|
|
|
Le hook naturel est le mécanisme `waitForConsiders` / `PB_CONSIDERS` déjà en place
|
|
pour `STORAGE_RESOURCE` — à étendre avec un `PROCESSING_RESOURCE` source.
|
|
Le payload doit être enrichi du **résumé de couplage** (peers impliqués, ressources)
|
|
pour permettre la vérification AE souveraine côté A.
|
|
|
|
---
|
|
|
|
## Data — même système que Processing, même lacune à combler
|
|
|
|
### Constat actuel
|
|
|
|
Aujourd'hui, une **Data n'est jamais reliée à un Storage dans un workflow** — c'est un manque.
|
|
Elle devrait l'être, exactement comme un Processing est relié à un Compute.
|
|
|
|
### Ce que ça implique dans le builder
|
|
|
|
Une Data avec une `source` doit déclencher le même mécanisme que Processing,
|
|
à ceci près que la destination n'est pas `/dev/shm` (exécution en mémoire) mais
|
|
le **Storage lié à la Data dans le workflow**.
|
|
|
|
Avant toute step de processing qui consomme ce Storage, le builder doit vérifier
|
|
si la Data source a déjà été copiée dedans. Sinon, il injecte une step de transfert.
|
|
|
|
```
|
|
Cas isReachable = true (source publique) :
|
|
|
|
[step curl Data→Storage] → [step processing qui lit depuis Storage]
|
|
↓
|
|
Storage lié à la Data
|
|
|
|
Cas isReachable = false (source privée) :
|
|
|
|
[step wrapper NATS/Minio → Storage] → [step processing qui lit depuis Storage]
|
|
↓
|
|
même protocole que Processing :
|
|
PB_SOURCE_REQUEST → pre-signed URL → télécharge dans le Storage (pas en mémoire)
|
|
```
|
|
|
|
La différence clé avec Processing :
|
|
- **Processing** : la source est un exécutable → téléchargé en `/dev/shm`, exécuté, supprimé
|
|
- **Data** : la source est une donnée → téléchargée dans le Storage lié, persistée pour le processing
|
|
|
|
### Changements requis
|
|
|
|
**oc-lib** : `DataResourceAccess` (ou généraliser `ResourceAccess`) doit exposer les mêmes champs :
|
|
|
|
```go
|
|
type DataResourceAccess struct {
|
|
Source string // URL ou clé opaque de la source
|
|
IsReachable bool
|
|
Container *models.Container // nil si on passe par Source
|
|
// + lien vers le Storage cible dans le workflow (à définir)
|
|
}
|
|
```
|
|
|
|
**workflow builder** : même logique que Processing —
|
|
|
|
```
|
|
if data.access.Container == nil && data.access.Source != "" {
|
|
if data.access.IsReachable {
|
|
// Injecte step curl source → Storage lié
|
|
// avant toute step processing consommant ce Storage
|
|
} else {
|
|
// Même protocole NATS/Minio que Processing
|
|
// mais curl destination = Storage lié (S3 mount), pas /dev/shm
|
|
// pas de rm après (la donnée doit persister)
|
|
// pas de wrapper exec — juste le téléchargement
|
|
}
|
|
}
|
|
```
|
|
|
|
**Workflow** : le lien `Data → Storage` doit être modélisé explicitement
|
|
(aujourd'hui absent — c'est le prérequis de tout le reste).
|
|
|
|
Les AE s'appliquent de la même façon : A peut restreindre l'usage de sa Data
|
|
à certains peers de storage ou de compute.
|
|
|
|
---
|
|
|
|
## Validation d'intégrité du workflow — double barrière
|
|
|
|
### Principe
|
|
|
|
La validation de l'intégrité d'un workflow ne doit **jamais** reposer uniquement sur oc-front.
|
|
Le front peut être bypassé (appel API direct, client custom, bug). La règle est :
|
|
|
|
> **oc-front valide pour l'UX. oc-scheduler valide pour la sécurité.**
|
|
|
|
C'est le même principe que la double vérification des AE (B vérifie en local, A vérifie au booking).
|
|
|
|
### Liens obligatoires à valider
|
|
|
|
| Lien | Manquant → |
|
|
|---|---|
|
|
| `Processing → Compute` | Le processing ne peut pas s'exécuter |
|
|
| `Data → Storage` | La donnée n'a nulle part où atterrir |
|
|
| `Data.source → Storage lié` | La step de téléchargement ne peut pas être générée |
|
|
| `Processing.source → Storage lié` (si isReachable) | Idem |
|
|
|
|
### oc-front — enforcement UX
|
|
|
|
- Bloquer la soumission d'un workflow si un Processing n'a pas de Compute lié
|
|
- Bloquer si une Data avec source n'a pas de Storage lié
|
|
- Afficher les erreurs inline sur le graphe du workflow (arête manquante = erreur visuelle)
|
|
- Ces contrôles sont de la **prévention UX** — ils aident l'utilisateur, ils ne garantissent rien
|
|
|
|
### oc-scheduler — validation souveraine
|
|
|
|
Avant d'accepter un workflow pour scheduling, oc-scheduler effectue une **validation d'intégrité structurelle** :
|
|
|
|
```
|
|
1. Vérifier que tout Processing a un Compute lié
|
|
2. Vérifier que toute Data avec source a un Storage lié
|
|
3. Vérifier que les liens source → storage sont cohérents avec les accès déclarés
|
|
4. Vérifier les AE pour chaque ressource externe (copie locale oc-catalog)
|
|
5. Vérifier les consentements de licence requis
|
|
```
|
|
|
|
Si une règle est violée → **rejet immédiat** du workflow avec code d'erreur explicite.
|
|
Pas de tentative de correction silencieuse — le workflow est invalide tel quel.
|
|
|
|
Cette validation se fait **indépendamment de la source de la soumission** (front, API, CLI, autre service).
|
|
|
|
### Analogie avec la vérification AE
|
|
|
|
```
|
|
oc-front vérifie les liens ←→ B vérifie les AE en local
|
|
oc-scheduler valide en entrée ←→ A vérifie les AE au booking
|
|
|
|
Dans les deux cas : la barrière arrière est souveraine et ne fait pas confiance à la barrière avant.
|
|
```
|
|
|
|
---
|
|
|
|
## Évolutions oc-front
|
|
|
|
Dans la page/workflow, les Détails pour **Processing** et **Data** doivent afficher (readonly) :
|
|
- Si `Container` non nil → afficher le conteneur
|
|
- Sinon → afficher la source et son mode d'accès (`isReachable` ou privé)
|
|
- La clé source est rendue via un générateur de clé opaque : humainement lisible,
|
|
ressemblant à un path mais ne correspondant pas au path réel — dissociation intentionnelle,
|
|
à expliquer dans l'UI
|
|
- Les AE et licences associées à la ressource (résumé lisible)
|
|
- **Erreurs inline** sur le graphe si un lien obligatoire est manquant (Processing sans Compute, Data sans Storage)
|
|
|
|
---
|
|
|
|
## Intégration oc-catalog
|
|
|
|
- Lors du `POST` d'une ressource avec `source != ""` → création automatique de la clé opaque
|
|
et enregistrement dans la table privée de A (`source_key → real_path`)
|
|
- Publication des AE dans oc-catalog à la création/modification d'une ressource ;
|
|
chaque peer maintient une copie locale synchronisée
|
|
- Enregistrement des violations AE (`AE_VIOLATION`) et mise à jour du score de réputation
|
|
- Enregistrement des consentements de licence (horodatage + identité du peer)
|
|
|
|
---
|
|
|
|
## Évolution future — table privée de clés opaques
|
|
|
|
Plutôt que de transmettre l'URL pre-signed via NATS (canal réseau),
|
|
A maintient une table interne `source_key → real_path` et résout lui-même la clé
|
|
au moment de monter le fichier dans son Minio. B ne reçoit que la pre-signed URL,
|
|
sans aucune information sur ce qu'elle contient.
|