Monitord Acces Change

This commit is contained in:
mr
2026-05-27 16:09:45 +02:00
parent a9284314ef
commit 7c91a8b032
19 changed files with 2496 additions and 332 deletions
+542
View File
@@ -0,0 +1,542 @@
# 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.