NetStacksNetStacks

Credential Sanitization

Mandatory redaction before any text reaches an LLM: 19 built-in patterns, optional network redaction, custom regex, and a case-insensitive allowlist.

Overview

Every byte of text that NetStacks sends to an external LLM — chat messages, selected terminal output, system prompts, and agent tool results — passes through a sanitization layer first. The layer scrubs credentials and secrets so that passwords, SNMP communities, API keys, routing secrets, VPN keys, and private keys never leave your machine in plaintext.

This is not advisory. Safety Rule 4, hardcoded into every AI interaction, states:

Non-negotiable safety rule

"Always sanitize credentials — passwords, SNMP communities, API keys, routing secrets, VPN keys, and private keys must be redacted before reaching any LLM provider."

The redaction itself is enforced in code, not by the model. It is applied by a SanitizingProvider decorator that wraps every AI provider, so no current or future provider can bypass it.

The sanitizer applies three tiers of patterns, in this exact order:

  1. Mandatory patterns — 19 built-in credential patterns that are always active and cannot be disabled.
  2. Optional network redaction — five toggles for IPv4, IPv6, MAC, hostnames, and usernames (off by default).
  3. Custom patterns — your own named regular expressions with your own replacement tokens.

An allowlist sits across all three: any match containing an allowlisted substring (case-insensitive) is left untouched.

Where to configure it

Open Settings → AI and scroll to the AI Data Security section. In Personal Mode the configuration is stored locally under the ai.sanitization_config setting; with a Controller it is managed centrally (see Controller SanitizingProvider).

How Sanitization Works

The SanitizingProvider is a transparent decorator. When the app or an agent needs an LLM, it builds the real provider (Anthropic, OpenAI, OpenRouter, or a custom OpenAI-compatible endpoint) and immediately wraps it. Every entry point — interactive chat, the streaming agent loop, and delegated sub-agents — goes through the same wrapper.

For each request the decorator sanitizes, before forwarding to the real provider:

  • every chat message's content;
  • the agent system prompt (it can carry device IPs and pasted terminal output);
  • agent message blocks — both plain Text blocks and ToolResult blocks (the raw command output a tool produced);
  • context fields: selected text, recent terminal output, and session-context entries (issue, root cause, resolution, commands).

Two categories are deliberately passed through unmodified:

  • Tool schema definitions — these are static JSON descriptions of available tools, not user data.
  • ToolUse blocks — the model's own generated tool calls (id, name, input). These originate from the AI, not from your devices, so they are not re-sanitized.

Inside sanitize(), patterns are applied sequentially — mandatory, then optional, then custom — each one rewriting the running text. Most patterns use a capture group so the label stays and only the secret is replaced, e.g. enable secret 5 <hash> becomes enable secret 5 [REDACTED]. The result also reports a redaction_count and the list of pattern_names that fired, which the test panel surfaces.

Cached and auto-invalidated

The compiled sanitizer is cached in memory for speed. Whenever you save the ai.sanitization_config setting, the cache is invalidated so the next request rebuilds with your new patterns — no restart required.

Mandatory Patterns (Always On)

Nineteen credential patterns ship built in and are always active. They cannot be toggled off — even with an empty configuration, every one of these runs. They are vendor-aware and cover the credential forms you find in real network configs:

#PatternMatchesReplacement
1cisco_enable_secretenable secret 5 … / enable password 7 …[REDACTED] (keeps label)
2cisco_password_7password 7 …[REDACTED]
3cisco_password_0password 0 …[REDACTED]
4snmp_communitysnmp-server community …[REDACTED]
5snmp_v3_authSNMPv3 auth/priv md5|sha|des|aes… keys[REDACTED]
6tacacs_keytacacs-server … key …[REDACTED]
7radius_keyradius-server … key …[REDACTED]
8juniper_secretJuniper secret "$9$…"[REDACTED]
9juniper_encryptedJuniper encrypted-password "…"[REDACTED]
10arista_secretArista secret sha512|0|5|7 …[REDACTED]
11paloalto_passwordPalo Alto <password>…</password>[REDACTED] (keeps tags)
12paloalto_keyPalo Alto <key>…</key>[REDACTED] (keeps tags)
13private_key_block-----BEGIN … PRIVATE KEY----- blocks[PRIVATE-KEY-REDACTED]
14certificate_block-----BEGIN CERTIFICATE----- blocks[CERTIFICATE-REDACTED]
15api_key_genericapi_key / api_token / bearer / access_token / auth_token = 20+ chars[REDACTED]
16aws_access_keyAWS access key IDs (AKIA…)[AWS-KEY-REDACTED]
17aws_secret_keyaws_secret_access_key = …[REDACTED]
18generic_passwordpassword / passwd / pass / pwd : …[REDACTED]
19generic_secretsecret / shared_key / pre_shared_key : …[REDACTED]

In the UI these are listed read-only under the collapsible Mandatory Patterns (19 always active) header. There is no switch to disable them. Given a snippet of a Cisco config:

! before — what you paste / what a tool returns
enable secret 5 $1$mERr$aBcDeFgHiJkLmNoPqR.
snmp-server community public RO
username netadmin password 0 S3cr3tP@ss

! after — what the LLM actually receives
enable secret 5 [REDACTED]
snmp-server community [REDACTED]
username netadmin password 0 [REDACTED]
Labels are preserved on purpose

Most patterns keep the keyword (the captured prefix) and replace only the secret value. This keeps the output useful to the model — it can still reason about "there is an enable secret configured" — without seeing the secret itself.

Optional Network Redaction

Five additional patterns redact network identifiers. These are off by default — identifiers like IPs and hostnames are often exactly what you want the AI to reason about. Enable them under Optional Redaction when you need stricter privacy (for example, when sharing output with a third-party model under a strict data-handling policy).

Toggle (config key)LabelExample matchReplacement
redact_ip_addressesIPv4 Addresses10.0.0.1, 192.168.1.0/24[IP-REDACTED]
redact_ipv6_addressesIPv6 Addressesfe80::1, 2001:db8::1[IPv6-REDACTED]
redact_mac_addressesMAC Addresses00:1a:2b:3c:4d:5e, 001a.2b3c.4d5e[MAC-REDACTED]
redact_hostnamesHostnames/FQDNsrouter1.corp.example.com[HOST-REDACTED]
redact_usernamesUsernamesusername admin[USER-REDACTED] (keeps the username keyword)
Hostname and IP redaction is aggressive

The FQDN pattern matches any dotted name with three or more labels, and the IPv4 pattern matches any valid dotted-quad. Turning these on can redact identifiers you wanted the AI to see — and may reduce the quality of troubleshooting answers. Enable them deliberately, then use the test panel to confirm the output still reads the way you expect. Use the allowlist to keep specific values, like a lab subnet, visible.

Custom Regex Patterns

Add your own patterns for site-specific secrets the built-ins don't know about — an internal token format, a corporate asset tag, an employee ID, or anything else you never want sent to an LLM. Each custom pattern has three fields:

name
A label for the pattern (shown in the UI and in test results).
regex
A Rust regex-crate pattern. The field also accepts the alias pattern for compatibility with the Controller.
replacement
The token to substitute. You can reference capture groups with ${1}, ${2}, etc. — the same syntax the built-in patterns use to preserve their labels.

Custom patterns run after the mandatory and optional patterns. Add them in the UI under Custom Patterns, or write them straight into the config JSON:

ai.sanitization_config (excerpt)json
{
  "custom_patterns": [
    {
      "name": "Internal API Token",
      "regex": "INT-[A-Z0-9]{32}",
      "replacement": "[INT-TOKEN-REDACTED]"
    },
    {
      "name": "Employee ID",
      "regex": "(?i)(emp[-_]?id\\s*[=:]\\s*)\\S+",
      "replacement": "${1}[EMP-ID-REDACTED]"
    }
  ]
}
Invalid regex is skipped, not fatal

Each pattern is compiled independently. If a custom pattern fails to compile, it is logged as a warning and skipped — the rest of the sanitizer (including all 19 mandatory patterns) keeps working. Always test a new pattern before relying on it: if it never appears in the test result's fired-pattern list, it either didn't match or didn't compile.

Escaping in JSON

In JSON every backslash must be doubled. A regex like \d+ is written "\\d+" in the config. The UI form handles a single level of escaping for you; only double-escape when editing the raw JSON.

Allowlist

The allowlist names strings that should never be redacted, even if a pattern matches them. Matching is a case-insensitive substring check: if a would-be redaction contains any allowlisted entry (in any case), it is left in the output verbatim.

This is the escape hatch for the aggressive optional patterns. For example, with hostname redaction on, allowlist lab.example.com to keep your lab gear visible to the model while still redacting production FQDNs. Or allowlist a well-known placeholder like public so the literal SNMP community string public passes through.

ai.sanitization_config (excerpt)json
{
  "redact_hostnames": true,
  "redact_ip_addresses": true,
  "allowlist": [
    "lab.example.com",
    "10.255.0.0",
    "public"
  ]
}
Substring match cuts both ways

Because the check is a substring, allowlisting example.com also exempts prod-fw.example.com and secret.example.com. Keep allowlist entries as specific as possible, and verify with the test panel that you are only exempting what you intend.

The allowlist cannot un-redact real credentials

The allowlist applies to matched text, so in principle it can exempt a credential if that credential string contains an allowlisted substring. Do not allowlist short or common fragments (single words, short numbers) that could appear inside a password or key — you could accidentally leak a secret. Prefer long, unambiguous values.

Configuration Reference

The complete configuration is a single JSON object stored under the ai.sanitization_config setting. All fields are optional; anything omitted falls back to its default (which is "off" for the optional toggles, and empty for custom patterns and the allowlist). The 19 mandatory patterns are not represented here because they are always on.

ai.sanitization_config (defaults)json
{
  "redact_ip_addresses": false,
  "redact_ipv6_addresses": false,
  "redact_mac_addresses": false,
  "redact_hostnames": false,
  "redact_usernames": false,
  "custom_patterns": [],
  "allowlist": []
}

A fully populated, stricter example:

ai.sanitization_config (strict)json
{
  "redact_ip_addresses": true,
  "redact_ipv6_addresses": true,
  "redact_mac_addresses": true,
  "redact_hostnames": true,
  "redact_usernames": true,
  "custom_patterns": [
    {
      "name": "Corp Asset Tag",
      "regex": "ASSET-[0-9]{6,8}",
      "replacement": "[ASSET-REDACTED]"
    }
  ],
  "allowlist": [
    "lab.example.com",
    "10.255.0.0/16"
  ]
}

In Personal Mode you can also set it over the Local Agent's loopback API. The agent stores it wrapped in a value field as a JSON string; saving it invalidates the in-memory sanitizer cache automatically:

curl -s -X PUT http://127.0.0.1:PORT/api/settings/ai.sanitization_config \
  -H 'Content-Type: application/json' \
  -d '{"value":"{\"redact_ip_addresses\":true,\"allowlist\":[\"lab.example.com\"]}"}'
Agent port is dynamic

The Local Agent binds to 127.0.0.1 (loopback only) on a port chosen at launch. Substitute the real port for PORT. Normally you never touch this endpoint directly — use the AI Data Security settings UI, which calls it for you.

Testing Sanitization

The settings UI includes a Test Sanitization panel: paste any text, click Test, and it shows the sanitized output, a redaction count, and the list of patterns that fired. The test path always loads a fresh config (bypassing the cache) so you see the effect of unsaved-then-saved changes immediately.

Under the hood the panel calls POST /api/ai/sanitization/test on the Local Agent (or the Controller, in Enterprise mode). You can exercise it directly:

curl -s -X POST http://127.0.0.1:PORT/api/ai/sanitization/test \
  -H 'Content-Type: application/json' \
  -d '{"text":"enable secret 5 $1$mERr$aBcD\nsnmp-server community public RO"}'
{
  "sanitized": "enable secret 5 [REDACTED]\nsnmp-server community [REDACTED] RO",
  "redaction_count": 2,
  "pattern_names": ["cisco_enable_secret", "snmp_community"]
}

An empty text returns zero redactions and no patterns. If a pattern you expected does not appear in pattern_names, the secret wasn't matched (check spacing/format) or a custom regex failed to compile.

Always test optional and custom patterns

Network redaction and custom regexes are the two places where output quality can regress or a pattern can silently fail to compile. Paste a representative sample of a real config or show run into the test panel after every change — if the redaction count and fired patterns match your intent, you are good.

Controller SanitizingProvider

Tier: Team / Enterprise. When the Terminal is connected to a Controller, sanitization works identically — the same SanitizingProvider decorator, the same 19 mandatory patterns, the same optional toggles, custom patterns, and allowlist. The single difference is where the configuration lives.

  • Personal Mode: config is stored locally under ai.sanitization_config and read/written via /api/settings/ai.sanitization_config.
  • Enterprise Mode: config is managed centrally by the Controller and read/written via /admin/sanitization/config, where it is sent as structured JSON (the same field names as the local config). Admins set the policy once; every connected operator inherits it.

The Controller accepts custom patterns using either the regex or the pattern field name — the engine treats pattern as an alias for regex — so a policy authored against the Controller schema applies cleanly on the Terminal. The test endpoint POST /api/ai/sanitization/test behaves the same in both modes.

Same guarantee, central policy

The point of the Controller integration is governance: instead of every engineer configuring redaction on their own machine, a security admin defines one organization-wide policy. The redaction code path is unchanged, so the guarantee you get standalone is exactly the guarantee you get under the Controller.

Q&A

Can I turn off the credential redaction entirely?
No. The 19 mandatory patterns are always active and have no toggle. Only the five network-identifier patterns (IP, IPv6, MAC, hostname, username), custom patterns, and the allowlist are configurable. This enforces Safety Rule 4.
Does sanitization apply to local-only models too?
The SanitizingProvider wraps every provider the app builds, including a custom OpenAI-compatible endpoint. Whatever you point at, the same redaction runs before text leaves the agent.
Why do I still see the keyword, like "enable secret 5"?
By design. Most patterns capture the keyword prefix and replace only the secret with [REDACTED], so the model can reason about the configuration without seeing the value. Full blocks (private keys, certificates) are replaced wholesale with [PRIVATE-KEY-REDACTED] / [CERTIFICATE-REDACTED].
My custom pattern isn't redacting anything. Why?
Either it didn't match the text, or the regex failed to compile (invalid patterns are logged and skipped, never fatal). Paste a sample into the test panel; if the pattern name doesn't appear in the fired list, fix the regex. Remember to double backslashes in raw JSON.
What does the allowlist match against?
The matched text of a would-be redaction, as a case-insensitive substring. If the match contains any allowlisted entry, it is left untouched. Keep entries specific so you don't accidentally exempt more than intended.
Do agents and sub-agents get sanitized too?
Yes. The ReAct agent loop and delegated sub-agents build the provider through the same SanitizingProvider wrapper, so system prompts, messages, and tool results are all sanitized before any LLM call.
Is the config the same on the Controller?
The fields and behavior are identical; only the storage location differs. Enterprise mode reads and writes /admin/sanitization/config centrally, and accepts pattern as an alias for regex. See Controller SanitizingProvider.