Security — Secrets, API Key Rotation, Audit Logs, Guardrails
Keep secrets in a centralized vault (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) and out of config files, env files in VCS, and spreadsheets. Use IAM roles over static keys, and OIDC for CI/CD. The 2026 answer is the AI-gateway pattern: apps → gateway → model provider, with the gateway pulling credentials from the vault at runtime. Rotate in the vault and the apps pick up the new key within minutes, with no redeploys and no Slack threads asking who has the new key. Rotate on a ≤90 day policy and scan with TruffleHog / GitGuardian / Gitleaks on each commit. Zero-trust means MFA, SSO, RBAC/ABAC, short-lived tokens, and device posture. PII scrubbing uses entity recognition to mask PHI/PII before forwarding, and consistent tokenization (Mesh approach) maps sensitive values to stable placeholders so the LLM keeps code and relationship semantics. For network egress, put LLM services in a dedicated VPC/VNet subnet that allows only `api.openai.com`, `api.anthropic.com` and similar, and block other outbound traffic. The incident behind all this: a 2026 Vercel supply-chain attack used compromised CI/CD credentials to exfiltrate env vars across thousands of customer deployments.
Learning Objectives
- Enumerate the four secret-management anti-patterns (config files in VCS, hardcoded env, spreadsheets, static keys) and name their replacements.
- Explain the AI-gateway-pulls-from-vault pattern as 2026 production standard.
- Implement a PII scrubber with consistent tokenization (same value → same placeholder) so semantics survive.
- Name the 2026 Vercel supply-chain incident and what it taught about CI/CD credential hygiene.
The Problem
An intern commits .env with API keys and deletes it quickly. The keys are already in git history. A GitGuardian scan catches it, and your rotation process is "Slack the team, update 40 config files, redeploy all services." 8 hours later, half your services are live and half are waiting for deploy windows.
Separately, a user prompt includes "My SSN is 123-45-6789." The prompt goes to OpenAI. You have a BAA, but your internal policy says to mask PII before forwarding, and nothing did.
Separately again, your EKS cluster's LLM pod can reach any internet host. Someone exfiltrates data through DNS lookups to a domain they control, and nothing blocks it.
Security for LLM services has to cover all three vectors: vault-backed credentials, PII scrubbing, and network egress filtering. Audit logs tie them together by recording what happened.
The Concept
Centralized vault + IAM-role pull
Vault: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager. One source of truth.
IAM role: the app or gateway authenticates with its IAM identity instead of a static key. The vault returns the secret for the lifetime of the token.
The AI-gateway pattern: the gateway pulls OPENAI_API_KEY from the vault at request time. Rotate in the vault and the next request gets the new key. No redeploys.
Rotation policy ≤ 90 days
This covers API keys, vault root tokens, and CI/CD credentials. Automate rotation where you can. When rotation is manual, log it and track it.
Secret scanning
- TruffleHog: regex plus entropy checks on commits.
- GitGuardian: commercial, high accuracy.
- Gitleaks: OSS, runs in CI.
Run them on each commit, and block the PR when a new secret is found.
Zero-trust posture
- MFA on all accounts.
- SSO via SAML/OIDC.
- RBAC (role-based) or ABAC (attribute-based) for fine-grained access.
- Short-lived tokens (hours, not days).
- Device posture: corp devices with disk encryption only.
PII / PHI scrubbing
Before the prompt leaves your infra:
- Entity recognition (spaCy NER, Presidio, commercial).
- Mask matched entities:
"My SSN is 123-45-6789"→"My SSN is [SSN_TOKEN_A3F]". - Consistent tokenization (Mesh approach): the same value maps to the same placeholder, so the LLM can still follow relationships between entities.
- Optionally, a reverse mapping for the LLM response.
Static regex filters catch the basic patterns and NER catches more. Use both.
Input + output guardrails
Input: block known jailbreaks and forbidden topics, and rate-limit per user.
Output: regex-scrub for leaked secrets (API key patterns, email patterns in refusal contexts) and run a classifier for policy violations.
Network egress whitelist
Put LLM services in a dedicated subnet:
- Whitelist:
api.openai.com,api.anthropic.com, vector DB endpoints, vault endpoints. - Drop the rest.
- Resolve DNS through an allowlist-only resolver to avoid DNS-tunneling exfil.
Audit log
An immutable log entry for each LLM call, with:
- Timestamp.
- User / tenant.
- Prompt hash (not the raw prompt, for privacy).
- Model + version.
- Token counts.
- Cost.
- Response hash.
- Any guardrail trips.
Retain per regulatory requirement (SOC 2 1 year, HIPAA 6 years).
The 2026 Vercel incident
A supply-chain attack: compromised CI/CD credentials exfiltrated env vars across thousands of customer deployments. CI/CD credentials are prod-equivalent, so store them in the vault, scope them narrowly, and rotate them aggressively.
Numbers you should remember
- Rotation policy: ≤ 90 days.
- Scan on each commit: TruffleHog / GitGuardian / Gitleaks.
- Vercel 2026: CI/CD creds compromised, thousands of customer env vars leaked.
- Audit log retention: SOC 2 = 1 year, HIPAA = 6 years.
Use It
code/main.py implements a toy PII scrubber with consistent tokenization, plus an audit-log entry builder that prints one JSON line per call.
Ship It
This lesson produces outputs/skill-llm-security-plan.md. Given your regulatory scope and current state, it plans the vault migration, scrubber, egress policy, and audit log.
I'd run main.py and read the token table before anything else. Prompts 1 and 2 share an SSN and an email, and both come out with the same [SSN_001] and [EMAIL_001]. That works because Scrubber.tokens is keyed on the raw value, so the second sighting hits the dict instead of bumping the counter.
Treat it as a sketch of the idea, not a scrubber. It is three regexes applied in order (SSN, then email, then phone) with no NER, so a name in the prompt passes straight through. The placeholders are counters like [SSN_001], not the hash-suffixed form shown in the body, and the table lives in memory on one Scrubber instance. A real gateway needs that mapping to survive restarts and stay consistent across replicas.
The audit side is thinner still. audit_log_call builds a JSON string and main prints it. Nothing appends to storage, so the append-only property from the docstring is something you would have to build. What it does get right is hashing the scrubbed prompt rather than the raw one, which keeps the SSN out of the log. Cost is a hardcoded 0.0012, and the token table print shows the first three characters of each value. I'd drop both before pointing this at real data. The phone regex also leaves a stray paren on "(202) 555-0150", which prints as "([PHONE_002]".
Exercises
- Run
code/main.py. Send two prompts referencing the same SSN. Confirm both get the same placeholder. - Design the network egress policy for a vLLM-on-EKS deployment calling OpenAI + Anthropic + Weaviate.
- You discover a key in git history (2 years old). What's the correct response: rotate the key, scrub history, or both? Justify.
- Your audit log grows 10 GB/day. Design retention tiers (hot 30d, warm 12mo, cold 6yr).
- Argue whether reverse-tokenization (substituting real values back into LLM response) is worth the complexity versus keeping placeholders visible.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Vault | "secrets store" | Centralized credential management service |
| IAM role | "identity-based auth" | Role assumed by app; returns short-lived creds |
| OIDC for CI/CD | "cloud-issued tokens" | No static keys in CI — identity via OIDC |
| TruffleHog / GitGuardian / Gitleaks | "secret scanners" | Commit-time secret detection |
| RBAC / ABAC | "access control" | Role-based vs attribute-based |
| PII scrubbing | "data masking" | Remove or tokenize sensitive entities |
| Consistent tokenization | "stable placeholders" | Same value → same token each time |
| Mesh approach | "Mesh tokenization" | Semantic-preserving tokenization pattern |
| Egress whitelist | "outbound allowlist" | Only permitted domains reachable |
| Audit log | "immutable history" | Append-only record for compliance |
Further Reading
Code
- main.py (python, 121 lines)