Files
oc-monitord/source_minio.md
T
2026-05-27 16:09:45 +02:00

20 KiB

Source tierce dans ProcessingAccess

Contexte

ProcessingResourceAccess (oc-lib) expose trois champs :

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) :

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 :

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 :

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 :

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)

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 :

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.

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 :

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.