Policy as code
Security policy that lives only in a web UI has no history, no review, and no
explanation of why any given rule is there. apply and export put it in a file.
# .vulnetix/ai-firewall.yaml
apiVersion: vulnetix.com/v1
kind: AiFirewallPolicy
spec:
settings:
logsEnabled: false
providers:
- slug: openai
action: allow
- slug: deepseek
action: deny
models:
- slug: gpt-4o-mini
provider: openai
action: allow
guardrails:
- name: no-private-keys
ruleType: blocked_pattern
action: block
pattern: '-----BEGIN [A-Z ]*PRIVATE KEY-----'
priority: 1
enabled: true
- name: redact-pii
ruleType: pii_redact
action: redact
priority: 20
enabled: true
- name: cap-conversation
ruleType: max_messages
action: block
pattern: '200'
priority: 90
enabled: true
The workflow
vulnetix ai-firewall export # capture what is live today
# review, edit, commit
vulnetix ai-firewall apply --dry-run # see exactly what would change
vulnetix ai-firewall apply # commit it
export writes your current live policy to the file. Start there rather than from a
blank page — you get a true picture of what your organisation actually has.
--dry-run prints the plan and changes nothing:
AI Firewall policy plan (dry run — nothing was changed)
Policy file .vulnetix/ai-firewall.yaml
Changes
Op Kind Target Detail
create guardrail no-private-keys blocked_pattern · block · priority 1
update guardrail redact-pii action: flag → redact
update provider deepseek action: → deny
Keys are never in the file
A provider key never appears in the policy file. The schema has no field that would hold one. You reference where the key comes from, and the CLI reads it at apply time:
providers:
- slug: openai
action: allow
key:
fromEnv: OPENAI_API_KEY # or: fromFile: /run/secrets/openai
This is deliberate. A policy file is meant to be committed, and a schema that could carry a secret is a schema that eventually will.
Neither does export — it will never write a key into the file, because it cannot
read one back out of the vault. See BYOK.
Drift is reported, not destroyed
If someone changed policy in the dashboard and it is not in your file, apply tells
you — and leaves it alone:
Drift (left alone)
guardrail emergency-block-supplier-x not present in the policy file
It does not delete it. Someone added that rule for a reason, quite possibly at 2am
during an incident, and a routine apply from CI silently reverting it would be the
worst possible behaviour.
To make the file authoritative and remove anything not in it, you must ask explicitly:
vulnetix ai-firewall apply --prune
Know what you are doing when you use --prune in CI.
Guardrails reconcile by name
The name is the identity. Change the pattern under an existing name and you have
updated that rule; change the name and you have created a new one and (with
--prune) removed the old.
Names must therefore be unique, and they should be stable and descriptive —
no-private-keys, not rule-3. The name is also what appears in the blocked_by
field of every 403, so a developer who trips it sees the name you chose. Make it one
they can act on.
Apply order
Applied in this order, and the order matters:
guardrails → models → providers → keys → settings
Guardrails go on first, so a provider you are about to allow is never briefly allowed without the content rules that were supposed to accompany it.
In CI
The pattern that works:
# .github/workflows/ai-firewall-policy.yml
name: AI Firewall policy
on:
pull_request:
paths: ['.vulnetix/ai-firewall.yaml']
push:
branches: [main]
paths: ['.vulnetix/ai-firewall.yaml']
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install the Vulnetix CLI
run: curl -fsSL https://get.vulnetix.com | sh
- name: Plan (on pull requests)
if: github.event_name == 'pull_request'
env:
VULNETIX_API_KEY: ${{ secrets.VULNETIX_API_KEY }}
VULNETIX_ORG_ID: ${{ secrets.VULNETIX_ORG_ID }}
run: vulnetix ai-firewall apply --dry-run
- name: Apply (on merge)
if: github.event_name == 'push'
env:
VULNETIX_API_KEY: ${{ secrets.VULNETIX_API_KEY }}
VULNETIX_ORG_ID: ${{ secrets.VULNETIX_ORG_ID }}
run: vulnetix ai-firewall apply
Now a change to your organisation’s AI policy is a pull request. It gets reviewed. It has an author, a date, and a reason in the commit message. That is the entire point.
Adopting the baseline
The recommended guardrail set can be pulled into your file rather than hand-copied:
spec:
baseline:
enabled: true
ref: recommended
exclude:
- some-baseline-rule-you-do-not-want
See prompt injection for what the baseline contains.
Validation happens before anything is applied
apply compiles every regex and parses every max_messages integer before it
changes anything. A file with one bad pattern is rejected whole.
This is the strongest reason to manage policy this way: it is the only path where a typo in a regex cannot silently disable a control. Applied any other way, an uncompilable pattern is skipped in silence and the rule enforces nothing — see the regex reference.
Related
- Rollout — the flag → redact → block ladder, run as pull requests.
- Regex reference — the silent-skip failure this avoids.
- Providers & models — the other half of what the file controls.