← Blog

Automation

Auditchecklist voor LLM-stappen die klantgegevens raken

Een Gmail-trigger in n8n stuurde vorige maand stilletjes 612 klantrijen naar een externe HTTP-node. Dit is de audit die we nu draaien voor elke LLM-stap die PII raakt.

Jacob Molkenboer· Oprichter · A Brand New Company· 29 mei 2025· 9 min
Ivoren bureau met dichte crème envelop, groene waszegel, messing paperclip, carbonformulier, inktkussen, stempel.

Het is dinsdag, 14:00. Een operations lead bij een fintech van 40 mensen opent na de lunch haar n8n-dashboard en haalt het nachtlog erbij. De workflow 'enrich new leads' draaide 612 keer. Elke run duwde de volledige contactkaart in een HTTP-node die bleek te horen bij een freelance tool die de vorige ops manager twee jaar geleden had gebouwd. De credentials werkten nog. Het endpoint antwoordde nog steeds met 200 OK. De data zit nu in iemand anders zijn logs.

Dit is geen hypothese. We zien er om de paar maanden een variant van bij de audits die we draaien voor klanten onder de €15M, vlak voordat ze een LLM-stap koppelen aan een workflow die een klantrecord raakt. Het patroon is altijd hetzelfde. Een trigger met meer OAuth-scope dan de workflow nodig heeft. Een downstream HTTP-node die niemand zich herinnert te hebben goedgekeurd. Een LLM-stap die van 'vat deze lead samen' een 'stuur de ruwe rij naar een derde partij voor inferentie' maakt. De fix is saai. Het is een checklist.

Waarom de LLM-stap de slechtste plek is om dit te ontdekken

Je overleeft een Gmail-trigger met te veel scope zolang de data het werkgeheugen van de workflow nooit verlaat. Op het moment dat er een LLM-stap bijkomt, veranderen drie dingen.

Ten eerste wordt de rij geserialiseerd in een prompt en naar een modelprovider gestuurd. Zelfs met zero-retention-contracten op papier passeert die prompt transit-logs, queue-infrastructuur en abuse-monitoring pipelines voor een venster van dagen. Ten tweede behandelt de volgende node de reactie van het model als betrouwbare tekst. Prompt injection vanuit de berichttekst van de lead wordt zo een control-flow-kwetsbaarheid, geen content-probleem. Ten derde is de LLM-stap meestal het laatste wat erop wordt geschroefd in een workflow die al maanden draait, wat betekent dat niemand de upstream-nodes opnieuw audit als hij landt.

De Open Worldwide Application Security Project zet sensitive information disclosure op LLM02 en excessive agency op LLM06 in zijn 2025 LLM Top 10. Beide gelden voor no-code workflows, ook al worden de meeste n8n- en Make.com-estates intern nooit als 'AI-applicatie' geclassificeerd. Die ontbrekende classificatie is precies wat auditors missen.

Trigger-scopes nalopen

Begin bij de bron. Elke trigger in n8n, Make en Zapier draait op een OAuth-scope of een API-key met een permissieset. De meeste operations-teams accepteren de default-scope op het moment van connecten en kijken er nooit meer naar om.

Haal elke connected app uit het workflow-estate en schrijf de daadwerkelijk toegekende scope op. Niet de scope waarvan de docs beweren dat hij nodig is. Voor Gmail vraagt de trigger 'On new email matching search' om gmail.readonly op de hele mailbox, niet alleen op de matchende berichten. Voor HubSpot geeft de default OAuth-flow crm.objects.contacts.read op alle properties, inclusief de custom velden die in de CRM-UI als gevoelig zijn aangemerkt. Voor de Microsoft 365-modules van Make vraagt de verbinding om Mail.Read op alle folders, ook als het scenario maar uit één gedeelde mailbox leest.

Snij weg wat je niet nodig hebt. Als de workflow alleen mails leest die naar billing@ zijn gestuurd, route ze dan eerst naar dat label en gebruik gmail.metadata plus een label-filter. De Google-docs beschrijven de scope-hiërarchie in detail en de metadata-scope is bijna altijd genoeg voor routing.

De PII-inventarisatie

Voor elke workflow die een LLM-stap krijgt: maak een lijst van de velden die er doorheen stromen. Niet 'klantrecord'. De daadwerkelijke kolommen: e-mail, telefoon, volledig adres, BSN, IBAN, vrije-tekst notities die van alles kunnen bevatten.

Dit klinkt vervelend. Het kost twintig minuten per workflow. De uitkomst is een tabel met twee kolommen die je vastpint aan de workflow-beschrijving. Zonder is de volgende stap onmogelijk.

workflow: enrich-new-leads
trigger: gmail.message.new
fields_in_motion:
  - sender_email          # PII
  - sender_name           # PII
  - message_body          # may contain PII
  - thread_id             # internal
  - hubspot_contact_id    # internal
llm_step:
  receives: [sender_first_name, message_body_redacted]
  forbidden: [sender_email, thread_id, hubspot_contact_id]

De LLM-grens

Bepaal wat de LLM-stap eigenlijk mag zien. De default in de OpenAI-node van n8n en in de AI-modules van Make is om de complete upstream JSON in de prompt te pompen. Dat is de verkeerde default voor elke workflow die PII raakt.

Zet een redactie-node direct vóór de LLM-stap. In n8n is dat een Function-node. In Make is het een Tools > Set Multiple Variables-module. De redactie krimpt de payload tot het minimum dat het model nodig heeft om zijn werk te doen, en vervangt alles wat de downstream-nodes later moeten opzoeken door een correlatie-token.

// n8n Function node, placed immediately before the OpenAI node
const item = $input.item.json;
const crypto = require('crypto');

// Hash anything we need to correlate later but the model should not see
const correlationId = crypto
  .createHash('sha256')
  .update(item.sender_email + process.env.WORKFLOW_SALT)
  .digest('hex')
  .slice(0, 12);

// Redact free text with a regex sweep, then forward only the minimum
const redactedBody = (item.message_body || '')
  .replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, '[EMAIL]')
  .replace(/\+?\d[\d\s().-]{7,}/g, '[PHONE]')
  .replace(/\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b/g, '[IBAN]');

return {
  json: {
    correlation_id: correlationId,
    sender_first_name: (item.sender_name || '').split(' ')[0],
    body_for_model: redactedBody.slice(0, 2000),
  },
};

Het correlation_id reist heen en weer door de LLM en laat de downstream-node het echte record opzoeken in je eigen database. Het model ziet het e-mailadres nooit. Als er een prompt injection landt, kan die hooguit de samenvatting corrumperen, niet de rij exfiltreren.

De HTTP-node allowlist

Van elke HTTP Request-node in het estate schrijf je de bestemmings-hostnaam op. Leg die lijst naast het beoogde doel van de workflow. Alles dat naar een domein wijst dat niemand kan uitleggen schakel je uit (niet weghalen) terwijl je de geschiedenis natrekt.

Het lek van 612 rijen waarmee we openden, was een HTTP-node die wees naar een Heroku-app uit 2023, opgeleverd door een contractor die al lang weg is. Het endpoint accepteerde wat het ook binnenkreeg en gaf nooit een fout terug. Niemand had het door, want de success-metric van de workflow was '200 OK'. Een simpele denylist op de n8n-instance, plus een driemaandelijkse review van HTTP-bestemmingen, had het in dezelfde week aan het licht gebracht waarin de contractor vertrok.

Waarschuwing

Een 200 OK van een HTTP-node is geen bewijs van correctheid. Het is bewijs dat iets antwoord gaf. Als de workflow alleen de statuscode logt, heb je geen audit trail wanneer een endpoint stilletjes van eigenaar wisselt.

Credential-hygiëne

Open de credentials store in n8n of de connections-view in Make. Sorteer op 'laatst gebruikt'. Alles ouder dan 90 dagen dat nog actieve credentials heeft, draai je om of haal je weg. Alles dat hangt aan het Google-account van een ex-medewerker, trek je in bij de IDP, niet op workflow-niveau. Intrekken op workflow-niveau laat het token leven op alle andere plekken waar het ooit is gekopieerd.

Voor LLM-provider keys specifiek: gebruik een aparte key per workflow met een spend cap. De dashboards van Anthropic, OpenAI en Google's AI Studio ondersteunen allemaal per-key budget limits. Als er om drie uur 's nachts een runaway loop begint te itereren over je contacttabel, stopt de cap het bloeden voordat de factuur dat doet. Het recente verhaal van een AI-agent die zijn operator failliet liet gaan tijdens een DN42-scan laat netjes zien waarom een ongelimiteerde credential op een langlopende workflow een eigen risicocategorie is, los van datalekken.

Logging waarmee je kunt replayen

De execution log moet voor elke PII-rakende stap drie dingen vastleggen: het correlation_id, de geredacteerde payload die naar de LLM gaat, en de ruwe response. Het ongeredacteerde record komt nooit in de log.

In n8n betekent dat 'Save data on success' aanzetten met een payload size limit, en daarnaast een aparte audit-regel wegschrijven naar je eigen Postgres die het correlation_id terugkoppelt aan de klantrij. In Make.com gebruik je hiervoor de History-feature met de data-store-write module voor het audit-record. Hoe dan ook is het principe hetzelfde: de runtime store van de workflow houdt de geredacteerde versie vast, en een aparte, geaudite store houdt de mapping naar de echte rij.

De reden dat dit ertoe doet, is het inzageverzoek. Als een klant je schrijft met 'waarom heeft jullie systeem mij gebeld', moet je de beslissing van het model kunnen reconstrueren zonder de data opnieuw te lekken. Het correlation_id laat je replayen tegen je eigen kopie. Zonder dat laat je de klant zijn eigen ruwe rij zien, opgesloten in een vendor-log, óf je vertelt 'm dat je de vraag niet kunt beantwoorden. Beide zijn slecht.

De kill switch

Elke workflow die PII raakt, heeft een onvoorwaardelijk uitschakelpad nodig dat niet vereist dat je inlogt op de n8n-UI. Bij ons is dat een rij in een Postgres-tabel die de workflow bij elke run checkt. Als er disabled = true staat, stopt de workflow voordat er één node draait.

CREATE TABLE workflow_kill_switch (
  workflow_id  text PRIMARY KEY,
  disabled     boolean NOT NULL DEFAULT false,
  disabled_at  timestamptz,
  disabled_by  text,
  reason       text
);

-- Disable from any psql session, including one triggered by PagerDuty
UPDATE workflow_kill_switch
   SET disabled    = true,
       disabled_at = now(),
       disabled_by = 'pagerduty-incident-9412',
       reason      = 'PII leak suspected on HTTP node 7'
 WHERE workflow_id = 'enrich-new-leads';

De eerste node in elke workflow is een Postgres-query tegen deze tabel. De query kost 8 milliseconden. De gemoedsrust dat de on-call engineer vanaf zijn telefoon een lekkende workflow om 02:00 kan stoppen, zonder een SSO-wachtwoord te onthouden, is die latency waard.

Wat we deden voor een fintech-klant

Toen we eerder dit jaar de lead-enrichment AI-agent bouwden voor een Nederlandse fintech, liepen we tegen een oude Make.com-scenario aan die veertien maanden lang door niemand was aangeraakt en nog een Gmail OAuth-token vasthield met volledige readonly op de persoonlijke inbox van de oprichter. We hebben uiteindelijk de upstream-trigger herbouwd op een service-account met scope op één gedeeld label, geredacteerd aan de grens, en elke HTTP-bestemming achter een allowlist gezet die de operations-lead goedkeurt via een Slack-workflow. De audit kostte twee middagen. De gemoedsrust houdt aan.

De vijf-minuten-versie

Als je dit leest en vandaag één ding doet: open je n8n- of Make-estate, sorteer connected apps op datum, en schrijf de OAuth-scope op van elke trigger ouder dan 90 dagen. Streep door wat de workflow niet daadwerkelijk nodig heeft. Het volgende lek zit vrijwel zeker verstopt in die lijst.

Kern

Voordat een LLM-stap een klantrij raakt: controleer de trigger-scope, redacteer aan de grens, zet HTTP-bestemmingen op een allowlist en bouw een kill switch.

FAQ

Maakt een zero-retention-overeenkomst met de modelprovider redactie overbodig?

Nee. Zero-retention dekt training en langetermijnopslag af. Prompts passeren nog steeds transit- en abuse-monitoring-systemen voor dagen. Door aan de grens te redacteren, hou je PII volledig buiten die vensters.

Waarom redacteren vóór de LLM-stap in plaats van erna?

Zodra de prompt de workflow verlaat, bepaal jij niet meer waar hij blijft of wie hem leest. Post-hoc redactie helpt alleen je eigen logs. Het hele punt van redactie is dat je de rij überhaupt niet verstuurt.

Is deze checklist overkill voor een kleine Zapier-setup?

De trigger-scope review en de HTTP-bestemmingslijst kosten een uur voor een klein estate. Sla de Postgres kill switch en de audit-tabel over als je onder de vijf workflows zit. De redactie-node hou je hoe dan ook.

Wat telt als PII in een vrije-tekst e-mailbody?

Alles dat een persoon op zichzelf identificeert, of in combinatie met één ander veld. Namen, e-mailadressen, telefoonnummers, adressen, rekeningnummers, IBAN's, BSN's. Bij twijfel: behandel de hele body als PII en redacteer stevig.

automationai agentssecurityworkflowprocess automationoperations

Iets bouwen?

Start een project