kisenon
Agent-Safe Change Control

Forks masqués

Donnez à un agent (ou à un humain) la forme de la prod et rien de ses PII — politiques de masquage, la bibliothèque de fonctions intégrées, et comment les branches et sandboxes masqués restent scellés jusqu'à ce que le masque soit validé.

MASK est la piste Agent-Safe Change Control pour les données sensibles : elle remet à un agent un fork ayant la forme de la production et rien de ses PII. Elle fonctionne de deux façons — branches masquées (pour les humains : dev, CI, prestataires) et sandboxes masqués (pour les agents). Les deux sont couverts ci-dessous.

Le masquage de données anonymise les colonnes sensibles au moment où une branche est créée. Vous attachez une politique de masquage à l'appel de création de branche, et les colonnes correspondantes de la nouvelle branche sont irréversiblement réécrites — hachées, mises à null, tronquées ou remplacées — avant que la branche ne soit joignable. La branche parente n'est jamais modifiée.

Vous y recourriez pour remettre des données ayant la forme de la production au développement, à la CI ou à un prestataire sans livrer les PII qu'elles contiennent : formes de tables réelles, nombres de lignes réels, e-mails factices.

Comment ça marche

Une politique de masquage est un ensemble de règles nommé et à portée de projet. Chaque règle associe des colonnes par motif et nomme une fonction de masquage intégrée :

ChampSignification
schema_patternMotif de nom de schéma (% ou * = tout, _ = un caractère). Par défaut *.
table_patternMotif de nom de table.
column_patternMotif de nom de colonne.
masking_fnUne des fonctions intégrées ci-dessous.
fn_argsArguments de fonction (seul mask_constant en prend un : value).

Au moment de la création de branche, lorsqu'un masking_policy_id est fourni :

  1. La branche fork de son parent comme d'habitude (copy-on-write — instantané).
  2. La branche entre dans l'état masking. Aucun point de terminaison n'est provisionné et le proxy refuse les connexions vers elle : il n'y a aucune fenêtre dans laquelle les données pré-masquées peuvent être lues.
  3. Un worker de masquage se connecte au calcul de la branche en tant que rôle à moindre privilège, découvre le schéma, associe vos règles à celui-ci, et exécute chaque réécriture dans une seule transaction.
  4. La branche atterrit en ready et ses points de terminaison montent — ne servant désormais que des données masquées. En cas d'échec, la branche atterrit en failed et reste scellée ; supprimez-la et réessayez.

Les entrées des règles sont traitées comme des données, jamais comme du SQL : les identifiants sont mis entre guillemets et les valeurs d'arguments sont liées comme paramètres à l'exécution, de sorte qu'un nom de colonne ou une constante hostile ne peut pas s'échapper de la réécriture.

Fonctions intégrées

FonctionEffet
mask_emailmd5(value)@masked.invalid
mask_nameName_ + un préfixe de hachage de 8 caractères
mask_nullNULL (la colonne doit être nullable)
mask_constantUne valeur fixe que vous fournissez (fn_args.value)
mask_ssn_partial***-**-1234 — conserve les 4 derniers chiffres
mask_credit_card****-****-****-1234 — conserve les 4 derniers chiffres
mask_ip0.0.0.0
mask_date_yearConserve l'année, tronque au 1er janvier
mask_hashmd5(value)
mask_shuffleBrouillage basé sur le hachage (le mélange complet des caractères est prévu)
mask_phoneUn numéro +1-555-XXXX synthétique
mask_uuidUn nouvel UUID aléatoire

Le catalogue est aussi servi par l'API :

curl -s https://api.kisenon.com/v1/masking-functions \
  -H "Authorization: Bearer $KISENON_API_KEY"

Gérer les politiques

Dans la console, ouvrez Project settings → Data masking pour créer une politique : nommez-la, ajoutez des règles (colonnes par motif + une liste déroulante de fonction), et enregistrez. La même surface existe sur l'API :

curl -s -X POST \
  https://api.kisenon.com/v1/projects/$PROJECT_ID/masking-policies \
  -H "Authorization: Bearer $KISENON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dev-safe",
    "rules": [
      {"table_pattern": "users", "column_pattern": "email", "masking_fn": "mask_email"},
      {"table_pattern": "users", "column_pattern": "phone", "masking_fn": "mask_null"},
      {"table_pattern": "%", "column_pattern": "%ssn%", "masking_fn": "mask_ssn_partial"}
    ]
  }'

Créez ensuite une branche masquée en ajoutant la politique à un appel de création de branche normal :

curl -s -X POST \
  https://api.kisenon.com/v1/projects/$PROJECT_ID/branches \
  -H "Authorization: Bearer $KISENON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "masked-dev", "masking_policy_id": "'$POLICY_ID'"}'

La branche rapporte state: "masking" pendant l'exécution de la réécriture ; regardez-la basculer vers ready dans la console (en direct, via le flux d'événements du projet) ou en interrogeant GET /v1/branches/{id}.

Sandboxes masqués (pour les agents)

Les mêmes politiques masquent le fork de sandbox d'un agent, de sorte qu' un agent travaille contre la forme de la production et ne voit jamais ses PII. Attachez une politique à la création :

keon sandbox create --project <id> --masking-policy <policy-id>

Le masque s'exécute avant que le sandbox ne s'active. Le sandbox rapporte masked: true et son masking_policy_id, et tant qu'il est encore creating, un objet de progression masking (state, phase, rules_total, rules_completed, rows_affected_so_far, error) montre la réécriture avancer.

Fixez un plancher par projet pour que chaque sandbox soit masqué par défaut :

keon projects update <id> --sandbox-masking-policy <policy-id>   # or: none

Un --masking-policy par requête outrepasse la valeur par défaut du projet, mais il ne peut jamais se désengager d'un mandat — une défense contre un agent qui parlementerait pour se sortir du masquage. Fixer le mandat est réservé à owner/admin ; les clés à capacité d'agent sont refusées (403).

Fail-closed par construction. Les points de terminaison d'un fork masqué naissent dans un état de masquage — le proxy refuse les connexions (SQLSTATE 57P05) jusqu'à ce que la réécriture soit validée. Si l'étape de masquage échoue ou n'est pas câblée, la création échoue fermée : aucune URL n'est jamais émise pour un fork non masqué. Le worker de masquage se connecte en tant que rôle à moindre privilège kisenon_masker (jamais un superutilisateur), ne détient aucun RBAC de cluster, et son mot de passe par exécution n'est jamais persisté ni journalisé.

Deux mises en garde à connaître :

  • Du DML contre des valeurs masquées ne correspondra pas sur le parent au rejeu. Une instruction comme UPDATE … WHERE email = 'a1b2@masked.invalid' cible une valeur masquée qui n'existe pas sur main. Le travail de couloir de schéma est indépendant des valeurs et promeut proprement ; le DML dépendant des valeurs n'est pas le rôle d'un fork masqué.
  • Les clés étrangères restent appliquées. Le masquage désactive les déclencheurs utilisateur (ALTER TABLE … DISABLE TRIGGER USER), pas les déclencheurs d'intégrité référentielle. Masquer une colonne au sein d'une relation de clé étrangère peut donc rencontrer une erreur RI et faire échouer le fork fermé plutôt que de casser silencieusement la référence.
  • v1 masque uniquement la base de données main.

Bon à savoir

  • Le masquage est unique, à la création. Modifier une politique ne touche jamais les branches déjà créées avec elle — elles conservent la forme de données avec laquelle elles sont nées. Recréez la branche pour appliquer les nouvelles règles.
  • Une politique en cours d'utilisation ne peut pas être supprimée. Supprimer une politique avec laquelle une branche vivante a été créée renvoie 409 policy_in_use ; supprimez d'abord ces branches.
  • Les règles non appariées sont ignorées, pas fatales. Si votre schéma a dérivé et qu'un motif ne correspond à rien, la branche se termine quand même — vérifiez les motifs de règle si une colonne que vous attendiez masquée a encore une forme de données réelle.
  • Les incompatibilités de type font échouer la branche. Une règle qui correspond à une colonne que sa fonction ne peut pas réécrire (disons mask_email sur un integer) avorte le masque entier — la branche atterrit en failed et n'est jamais exposée à moitié masquée.