Protect data with DLP profiles
Configure MAQPNA's data loss prevention (DLP) profiles to deny or redact card numbers, IBANs, national IDs and secrets in tool arguments, tool results, prompts and completions.
flowchart LR A[Tool call or model call] --> B[Gateway picks a DLP profile<br/>server, model route, namespace, default] B --> C[Scan request<br/>arguments or prompt] C -- deny --> D[Call denied<br/>reason dlp:detector] C -- redact --> E[Replace match with<br/>REDACTED:detector] C -- clean --> F[Policy, approval, budget] E --> F F --> G[MCP server or model] G --> H[Scan response<br/>result or completion] H --> I[Agent] D --> J[Audit ledger:<br/>detector ids and counts only] H --> J
Goal#
Stop sensitive data from leaving through an agent's tool and model calls. You will define two profiles, one that denies a request carrying an IBAN and one that redacts a card number and an email address, bind them to traffic, and read what the audit ledger records.
Data loss prevention (DLP) in MAQPNA runs inside the gateway. Its detectors are deterministic and work offline: matches are validated with checksums, structure or context keywords, so false positives stay low and no data goes to an outside classifier.
Prerequisites#
- A local MAQPNA (
maqpna dev up), or a cluster with MAQPNA installed. jq, to read the configuration and the audit ledger.
What DLP scans#
| Direction | Scanned |
|---|---|
| Request | Tool-call arguments (string values, object keys, long numbers) and model request bodies, before policy is evaluated and before anything is forwarded |
| Response | Every MCP response (result, error and params, JSON or each server-sent event), exec output and files, and model completions, including streamed deltas |
Detectors#
A profile with no detectors list uses the default set: every detector marked "default" below.
| Detector | Class | Default | Validation |
|---|---|---|---|
iban |
financial | yes | Country length and ISO 7064 mod-97 |
pan |
financial | yes | 13–19 digits, card grouping, issuer prefix and Luhn |
email |
personal data | no | Dot-atom local part, dotted domain, alphabetic TLD |
phone_e164 |
personal data | no | + and 8–15 digits (E.164) |
de_tax_id |
personal data | yes | 11 digits, ISO 7064 MOD 11,10 and repeat rule |
fr_nir |
personal data | yes | 15 characters, 97 − (n mod 97) key |
nl_bsn |
personal data | yes | 9 digits, 11-proof, context keyword required |
it_codice_fiscale |
personal data | yes | 16-character structure and check character |
es_dni |
personal data | yes | DNI/NIE, mod-23 control letter |
uk_nino |
personal data | yes | HMRC prefix and suffix rules |
us_ssn |
personal data | yes | SSA area, group and serial rules |
ipv4_private |
infrastructure | no | RFC 1918 dotted quad |
aws_access_key |
secret | yes | AKIA/ASIA/ABIA/ACCA and 16 base32 characters |
gcp_sa_key |
secret | yes | "private_key_id" with 40 hex characters |
gcp_api_key |
secret | yes | AIza and 35 characters |
github_token |
secret | yes | gh[pousr]_ and 36 characters, github_pat_ and 82 characters |
slack_token |
secret | yes | xox[abposre]-, xapp-, Slack webhook URLs |
openai_api_key |
secret | yes | sk-proj-, sk-svcacct-, sk-admin- or the legacy marker |
anthropic_api_key |
secret | yes | sk-ant- and 80 or more characters |
jwt |
secret | yes | Three base64url segments, JSON header with alg |
pem_private_key |
secret | yes | A -----BEGIN ... PRIVATE KEY----- block |
high_entropy_secret |
secret | no (opt-in) | Shannon entropy over tokens of 32 or more characters |
Add your own with custom: {id, regex, secret}. The id must match [a-z0-9_]{1,40} and must not
collide with a built-in detector.
Profile settings#
| Key | Values | Default |
|---|---|---|
detectors |
Detector IDs | The default set |
custom |
[{id, regex, secret}] |
— |
requestAction |
off, audit, redact, deny |
redact |
responseAction |
off, audit, redact, deny |
redact |
onSecret |
An action for secret-class detectors in both directions, for example deny while personal data is only redacted |
— |
maxScanBytes |
Bytes scanned per message | 1 MiB |
onOversize |
audit or deny for a message over maxScanBytes |
deny where the direction redacts or denies, else audit |
streamCarryBytes |
Hold-back window for matches split across streamed chunks (-1 disables) |
256 |
entropyMinLength, entropyThreshold |
Tuning for high_entropy_secret |
32, 4.3 |
audit records the hit and forwards the data unchanged. redact replaces each match with
[REDACTED:<detector>] and keeps the JSON structure. deny blocks the message.
Steps#
1. Define profiles and bind them#
The gateway reads two configuration keys: dlpProfiles (named profiles) and dlp (which profile applies
to which traffic).
{
"dlpProfiles": {
"payments": {
"detectors": ["iban", "pan", "github_token", "aws_access_key"],
"requestAction": "deny",
"responseAction": "redact"
},
"support": {
"detectors": ["email", "iban", "pan"],
"requestAction": "redact",
"responseAction": "redact",
"onSecret": "deny"
}
},
"dlp": {
"defaultProfile": "payments",
"namespaces": {"support": "support"}
}
}
Bindings, from most to least specific:
- For an MCP server:
dlp.servers[<server>]. For a model route: the route's owndlpProfile(Agent.spec.model.dlpProfilein a cluster), thendlp.models[<route>]. dlp.namespaces[<namespace>].dlp.defaultProfile.
The profile name none turns DLP off for that binding. Profile names match [a-z0-9][a-z0-9-]{0,62}.
2a. In a cluster: set Helm values#
The chart maps the same keys under dlp:
dlp:
profiles:
eu-strict:
detectors: [iban, pan, email, de_tax_id, fr_nir, nl_bsn, pem_private_key, github_token, jwt]
requestAction: redact
responseAction: redact
onSecret: deny
defaultProfile: eu-strict
namespaces: {}
servers: {}
models: {}
Per MCP server, set spec.dlpProfile on the MCPServer; per agent model route, set
spec.model.dlpProfile on the Agent. Apply the values with maqpna upgrade -f values.yaml (see
Upgrade and rollback).
2b. On a local MAQPNA: edit the gateway configuration#
maqpna dev up has no DLP flag. To try DLP locally, add the two keys to .maqpna/gateway.json, check the
file, and restart the gateway with the same configuration and admin token:
maqpna-gateway -check-config -config .maqpna/gateway.json
Config OK: .maqpna/gateway.json
pkill -f "maqpna-gateway -config $PWD/.maqpna"
(set -a; . .maqpna/dev.env; nohup maqpna-gateway -config "$PWD/.maqpna/gateway.json" \
> .maqpna/logs/maqpna-gateway.log 2>&1 &)
3. Send data the profile denies#
maqpna dev run -- maqpna call --server echo --tool echo \
--arg text="Pay invoice 4411 to DE89370400440532013000"
Expected output (exit status 3):
maqpna dev: session dev-aa2e3c7f (agent coder, namespace dev, on behalf of you@localhost)
error -32001: denied by DLP profile payments: dlp:iban
data: {"domain":"maqpna.com","policy":"dlp","reason":"dlp:iban","rule":"payments"}
maqpna dev: dev-aa2e3c7f: 1 deny, cost $0.0000 (maqpna dev timeline --last)
The reason is dlp:<detector>; the rule is the profile name. A denied model call answers HTTP 403 with
the error type maqpna_dlp_blocked.
4. Send data the profile redacts#
Sessions in namespace support use the support profile, which redacts:
maqpna dev run --namespace support -- maqpna call --server echo --tool echo \
--arg text="Refund card 4111 1111 1111 1111 for alice@example.com"
{
"content": [
{
"text": "Refund card [REDACTED:pan] for [REDACTED:email]",
"type": "text"
}
],
"isError": false
}
The MCP server received the redacted text, so its echo shows the markers.
Verify#
The audit ledger records detector IDs and counts, never the matched values:
tail -1 .maqpna/audit.jsonl | jq '{decision, ext}'
{
"decision": "allow",
"ext": {
"argsRedactedSha256": "e6929dc2c9aaa2c5e02c4cf28f14414f8312cb2dd2eb88664a1b253ac15c6e9f",
"dlp": "email:1,pan:1",
"dlpAction": "redact",
"dlpProfile": "support"
}
}
argsSha256 stays the hash of the original arguments; argsRedactedSha256 is the hash of what was
forwarded. A denial shows in the timeline with its reason:
maqpna dev timeline --session dev-aa2e3c7f
TIME KIND SERVER/TOOL DECISION DETAIL
23:48:38 tool_call echo/echo deny dlp:iban
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
The gateway does not start: dlpProfiles.<name>: ... |
An unknown detector, an invalid action or a bad custom regex | Run maqpna-gateway -check-config -config FILE and fix the named key |
unknown DLP profile "x" |
A binding names a profile that is not defined | Define it under dlpProfiles (Helm dlp.profiles) |
| A namespace binding has no effect | A server binding wins over a namespace binding | Check dlp.servers, or set the server's binding to the profile you want |
| Large requests are denied | The message is over maxScanBytes and the direction enforces, so onOversize is deny |
Raise maxScanBytes, or set onOversize: audit knowingly |
| A national ID is not found | The detector is not in the profile, or it needs a context keyword (nl_bsn, unformatted us_ssn) |
List it in detectors; include the keyword in test data |
| Locally, the change has no effect | The old gateway is still running | pgrep -fl maqpna-gateway, stop the old one, start it again |
Next steps#
- Combine DLP with taint rules for content an agent reads: Taint and prompt-injection containment.
- Prove what happened to sensitive data: Audit ledger.
- Reason codes your agent can receive: Error and reason codes.
- Command reference:
maqpna call,maqpna dev timeline.