Configuration
Philter Router reads a single YAML file, passed as the first argument:
java -jar philter-router.jar router.yaml
Configuration is validated on startup and is fail-closed: any problem (a missing default, an unknown
engine or classifier, an invalid port) prevents the router from starting rather than letting it run with
a gap. At least one entry point must be enabled: the HTTP API (server), folder watching
(watch.locations), or both.
Blocks
server
Enables the HTTP API.
server:
enabled: true
port: 8080
watch.locations
The directories to watch (see Folder Watching). Each location sets its own watch mechanism and drainage directories.
watch:
locations:
- path: "/data/intake"
mode: poll # poll (default; required on network shares) or notify (local only)
pollIntervalMs: 5000
stableForMs: 2000
recursive: true
output: "/data/redacted"
done: "/data/intake/.done"
error: "/data/intake/.error"
engines
Named Philter engines, referenced by routes and the default.
engines:
philter1: { url: "http://philter1:8080" }
philter2: { url: "http://philter2:8080", context: "batch", readTimeoutMs: 300000 }
apiKey and context are optional. Do not embed secrets in the policy; resolve an API key from the
environment where possible.
Timeouts for the forwarded request are per engine (all in milliseconds):
| Field | Default | Purpose |
|---|---|---|
connectTimeoutMs |
10000 | TCP connect. |
writeTimeoutMs |
60000 | Inactivity while uploading the file body. |
readTimeoutMs |
120000 | Inactivity while awaiting Philter's response. |
callTimeoutMs |
0 (off) | Hard end-to-end ceiling for the whole call. |
Philter sends nothing until redaction finishes, so readTimeoutMs must cover the full processing time of
the slowest document routed to that engine, not the average. Raise it for large files. callTimeoutMs
is an optional upper bound so a stuck engine cannot hold a router thread indefinitely; a value of 0
disables it. If a proxy sits in front of the router, raise its read timeout to match.
When a Philter engine serves HTTPS with a self-signed certificate, the forwarded request fails the default TLS check. Two per-engine options handle it:
| Field | Default | Purpose |
|---|---|---|
caCertPath |
(none) | Path to a PEM certificate (or its issuing CA) to trust. Verification stays on: the engine is still authenticated. |
insecureSkipVerify |
false |
Disable TLS verification entirely. |
engines:
philter1: { url: "https://philter1:8443", caCertPath: /etc/philter-router/philter1.crt }
Prefer caCertPath: it trusts the specific self-signed certificate while keeping the connection
authenticated and resistant to interception. insecureSkipVerify disables authentication and exposes the
forwarded document and API key to interception, so use it only on a trusted network or for testing; the
router logs a warning at startup when it is on. If both are set, insecureSkipVerify wins.
classifiers
Named local LLM classifiers. Each runs one prompt that returns a single label from labels, at most
once per file (cached). The {{text}} placeholder is replaced with an excerpt of the extracted text.
classifiers:
doc-type:
type: ollama
endpoint: "http://ollama:11434"
model: llama3.1
timeoutMs: 2000
labels: [medical, financial, legal, general]
prompt: |
Classify this document into exactly one of: medical, financial, legal, general.
Reply with only the label.
{{text}}
The endpoint must be local: a classifier reads un-redacted document text, which must not leave your boundary. On timeout, failure, or an unrecognized label, the router applies the default.
routes
An ordered list, evaluated first match wins. Each route has a match, an optional languages list, and
an outcome (engine + policy).
routes:
- name: office-docs
match: { extensions: [".xlsx", ".docx"] }
languages: [eng, spa]
engine: philter2
policy: office-default
- name: medical-records
match: { classification: { classifier: doc-type, label: medical } }
languages: [any]
engine: philter1
policy: hipaa
Match fields: contentTypes, extensions, directories, and classification
({ classifier: <name>, label: <label> }). Within a route, all specified fields must match (AND);
within a single field, a list is any-of (OR). For OR across fields, use two routes.
languages is a list of allowed ISO 639-3 codes, AND-ed with the match. When omitted it defaults to
[eng]. Use any to accept all languages. See Routing.
default
The mandatory catch-all outcome, applied when no route matches or when language detection or a classifier cannot decide. The router refuses to start without it. It takes one of two forms.
Redact with a policy (point it at a policy that redacts, so any unmatched file is still redacted):
default:
engine: philter1
policy: default
Or reject unmatched documents instead of redacting them with a generic policy:
default:
action: reject
With action: reject the router does not guess a policy for a file it has no route for. In the folder
watcher a rejected file is moved to the location's error directory and never written to output; over
the HTTP API the request returns 422 Unprocessable Entity and nothing is sent to Philter. A rejecting
default must not set engine or policy. Either form upholds the guarantee that an unmatched file is
never emitted unredacted: it is either redacted by the default or refused.
Examples
The examples/ directory has complete,
commented configurations:
minimal.yaml- one watched directory, one policy.folder-watching.yaml- multiple locations, two engines, a classifier, and routes.network-share.yaml-pollon an SMB/NFS share,notifyon a local disk.http-api.yaml- the HTTP API with classification routing.watch-and-api.yaml- the API and folder watching from one config.classifier-routing.yaml- routing by classification and by language.