Enrich DNS and security logs with VerdictIQ domain verdicts and forward them to your SIEM.
174
VerdictIQ Connector enriches your DNS and security logs with VerdictIQ domain verdicts, inside your own environment. It receives your log stream, extracts the domain names, looks them up in batches with the VerdictIQ verdict API and writes each event, with its risk level and score, to your SIEM, Elasticsearch, Kafka, an HTTP endpoint or a file.
Only domain names leave your network. Raw log lines, client addresses and every other field stay with you.
verdictiq/connectorlinux/amd64, linux/arm64/data volumesource -> parser -> EPS limit -> queue -> verdict cache -> batch -> VerdictIQ API -> risk filter -> outputs
www.example.com, mail.example.com and cdn.example.com are one lookup of example.com. A domain already in the verdict cache (Redis, with a local copy in memory) is answered without API use.POST /verdict/bulk when the batch is full or its wait time passes.API use therefore follows the distinct registrable domains per hour, not the event rate.
linux/amd64 or linux/arm64.sk_...) is optional: without it, lookups use the account's plan limit.api.verdictiq.io, auth.verdictiq.io and portal.verdictiq.io (HTTPS_PROXY, HTTP_PROXY, NO_PROXY honored), and from your browser to the portal.docker run -d --name viq-connector \
--restart unless-stopped \
--stop-timeout 30 \
-p 127.0.0.1:5005:5005 \
-p 514:5514/udp -p 514:5514/tcp \
-p 8088:8088 \
-v viq-connector-data:/data \
verdictiq/connector:0.2.0
| Option | Why |
|---|---|
-p 127.0.0.1:5005:5005 | Web UI. Published on the host's loopback address only, so it is not reachable from the network. |
-p 514:5514/udp -p 514:5514/tcp | Syslog input: devices send to port 514 of the host, the connector listens on 5514 inside the container. Drop what you do not use. |
-p 8088:8088 | HTTP / Splunk HEC input. Drop it if you do not push logs over HTTP. |
-v viq-connector-data:/data | Keeps the configuration and state across restarts and upgrades. |
--stop-timeout 30 | Lets in-flight lookups and writes finish on stop. |
Check that it is up:
curl http://127.0.0.1:5005/health # {"status":"ok"}
docker logs viq-connector
On the first start the connector writes a default configuration to /data/config.yaml and logs default config created.
This single container has no Redis for the verdict cache: it logs verdict cache: redis not reachable, using the in-memory cache once and works normally from memory (the Overview shows an information note). The cache then starts empty after each restart. Docker Compose below adds Redis.
Recommended: the connector with Redis for the verdict cache.
services:
connector:
image: verdictiq/connector:0.2.0
container_name: viq-connector
restart: unless-stopped
stop_grace_period: 30s
ports:
- "127.0.0.1:5005:5005" # web UI, localhost only
- "514:5514/udp" # syslog UDP
- "514:5514/tcp" # syslog TCP
- "8088:8088" # HTTP / Splunk HEC input
volumes:
- viq-connector-data:/data
depends_on:
redis:
condition: service_healthy
# Verdict cache at redis:6379 (the connector default). No ports.
redis:
image: redis:7-alpine
container_name: viq-connector-redis
restart: unless-stopped
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru",
"--appendonly", "yes", "--save", ""]
volumes:
- viq-connector-redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
volumes:
viq-connector-data:
viq-connector-redis:
Redis is reachable only on the Compose network. maxmemory bounds it (about 300 bytes per verdict; least recently used go first); the append-only file keeps the cache across restarts. If Redis stops, lookups continue from memory and the connector switches back when Redis answers.
docker compose up -d
docker compose logs -f connector
http://localhost:5005 on the Docker host. For a remote server, open an SSH tunnel first (ssh -L 5005:127.0.0.1:5005 <server>) and open http://localhost:5005 on your workstation.:5514 with the parser preset that matches your device. The listen port is the container port: it must match the right side of your -p mapping. Paste a real log line into Test parser to check it.After a restart the saved configuration is used; the UI asks for a sign-in again, lookups continue without it.
Nothing is sent to VerdictIQ while no output works: events are counted as dropped instead, so no quota is used before results can be delivered.
| Page | What it shows |
|---|---|
| Overview | Status, warning bands, last 24 hours totals, event rate, cache hit rate and location, last 60 minutes chart. |
| Sources & Outputs | State and counters; add, edit, enable / disable, delete; Test parser, Send test event. |
| Live Events | Recent events, parse failures and drops, with the raw line and the reason. |
| Metrics | Last 24 hours totals, counters since start, last 60 minutes chart, latencies. |
| Settings | Account (Log out, Disconnect account), optional API key, batch / cache (Redis) / limit settings, Export config / Import config. |
| Usage & Quota | Opens the portal's usage page in a new tab. |
| Container port | Purpose |
|---|---|
5005/tcp | Web UI and its API (publish on 127.0.0.1 only) |
5514/udp, 5514/tcp | Syslog input (example; each source sets its own port) |
8088/tcp | HTTP / Splunk HEC input (example) |
Each source sets its own listen port; publish the ports your sources use.
The /data volume holds config.yaml (mode 0600, it contains secrets), its previous version config.yaml.bak, and state/: file source read positions, the last 24 hours metric totals and the account token (account.json, mode 0600).
| Variable | Default in the image | Meaning |
|---|---|---|
VIQ_CONNECTOR_CONFIG | /data/config.yaml | Config file path. The directory must be writable by UID 65532. |
VIQ_CONNECTOR_LISTEN | 0.0.0.0:5005 | UI address inside the container. |
VIQ_CONNECTOR_HEALTH_LISTEN | off | Separate listener serving only GET /health and GET /version, for example 0.0.0.0:5006 (Kubernetes probes while the UI stays on loopback). |
VIQ_CONNECTOR_ALLOWED_HOSTS | empty | Extra host names the UI answers to, comma separated (for a reverse proxy). By default only localhost, 127.0.0.1 and [::1]. |
docker exec viq-connector /connector --version prints the version.
The whole configuration is one YAML file; there is no database. You normally edit it in the UI: every save is validated, written atomically and applied at once without a restart. Invalid values are rejected next to the field and the file is not changed.
Editing the file by hand: it is read only at start (docker restart viq-connector after editing); omitted settings take their default; unknown keys and invalid values make the connector log invalid config field with the YAML path and exit with code 1. Durations: 500ms, 30s, 5m, 1h.
| Key | Default | Range | Meaning |
|---|---|---|---|
api.base_url | https://api.verdictiq.io/api/v2/ioc | URL | VerdictIQ API root. Change only if VerdictIQ tells you to. |
api.key | empty | Optional API key (sk_...). When set, every lookup uses it; empty means lookups use the connected account. | |
api.timeout | 30s | 1s - 5m | Timeout of one API request. |
api.portal_url | https://portal.verdictiq.io | URL | Portal root for the Usage & Quota link. |
auth.issuer | https://auth.verdictiq.io | URL | VerdictIQ sign-in server. Change only if VerdictIQ tells you to. |
batch.max_size | 1000 | 1 - 10000 | Domains per API request. |
batch.max_wait | 5s | 1s - 60s | Longest wait of a domain for its batch: the added delay for a domain not yet cached. |
batch.concurrency | 2 | 1 - 4 | Parallel API requests. |
cache.ttl | 1h | 5m - 24h | How long a verdict is reused. Higher means less quota use and older verdicts. |
cache.backend | redis | redis, memory | redis: verdicts kept in Redis across restarts, memory while Redis is down. memory: this process only. |
cache.redis.address | redis:6379 | host:port | Redis server; also cache.redis.password, db (0), key_prefix (viq:verdict:). |
cache.max_entries | 500000 | 1000 - 5000000 | In-memory cache capacity (local copy, or the whole cache without Redis), in registrable domains. |
limits.eps | 2000 | 1 - 2000 | Events per second accepted over all sources; the excess is dropped and counted. 2000 is the ceiling of this version. |
limits.queue_size | 50000 | 1000 - 500000 | Events waiting for lookup; when full, new events are dropped and counted. |
version: 1
sources:
- id: core-dns
name: Core DNS
type: syslog
enabled: true
syslog:
protocol: udp
listen: ":5514"
parser:
preset: bind
outputs:
- id: elastic
name: Elasticsearch
type: elasticsearch
enabled: true
format: json
elasticsearch:
urls:
- https://es1.example.com:9200
index: "verdictiq-%{+yyyy.MM.dd}"
api_key: "REPLACE_WITH_ENCODED_API_KEY"
- id: siem-cef
name: SIEM CEF
type: syslog
enabled: true
format: cef
min_risk_level: suspicious
syslog:
protocol: tcp
address: siem.example.com:514
Without api.key, lookups use the VerdictIQ account you signed in with. Note that enabled defaults to false: a source or output written by hand without enabled: true does not run.
Secrets (api.key, output passwords, tokens, API keys and HTTP header values) are stored in the file in plain text, hence mode 0600, and are always masked in the UI, the export and the logs. To keep a stored secret, leave the field empty when you save from the UI.
Source type | Use it when | Main settings |
|---|---|---|
syslog | Devices and DNS servers send syslog over UDP, TCP or TLS (newline or octet-counted framing, mutual TLS optional). | syslog.protocol, syslog.listen, syslog.tls.* |
http | A log shipper pushes over HTTP: JSON / NDJSON on /ingest, or the Splunk HEC endpoints (Splunk forwarder, Vector, Fluent Bit, Cribl). A token is always required. | http.listen, http.token, http.tls.* |
kafka | DNS logs are already on a Kafka topic (consumer group, TLS, SASL PLAIN / SCRAM). | kafka.brokers, kafka.topics, kafka.group_id, kafka.start_offset |
file | A DNS server writes a log file on the same host. Mount the log directory read-only; rotation is followed and read positions are kept in /data/state. | file.path (glob allowed), file.start_position, file.poll_interval |
Every source has a parser preset (parser.preset):
| Preset | For |
|---|---|
bind, infoblox | BIND (named) and Infoblox NIOS query logs (query: <name> IN <type>) |
dnsmasq | dnsmasq with log-queries (Pi-hole, routers) |
windows_dns | Windows Server DNS debug log |
cef, leef | Devices sending CEF or LEEF (standard host / DNS keys, or parser.kv_key) |
json | JSON logs: the dotted path in parser.json_field, for example dns.question.name |
kv | key=value logs: the key in parser.kv_key |
regex | Anything else: the named group (?P<domain>...) in parser.regex (RE2); other named groups become fields |
Presets also pick up the client address (client_ip), the query type (query_type) and, where present, the response code (response_code).
IP addresses, single-label names, internal suffixes (.local, .corp, .arpa, ...) and invalid names are never sent to VerdictIQ (domain_invalid).
Output type | Formats | Notes |
|---|---|---|
syslog | json, cef, leef | UDP or TCP, RFC 5424, facility local0, reconnects on its own. |
elasticsearch | json | Elasticsearch and OpenSearch Bulk API; date pattern in the index name (verdictiq-%{+yyyy.MM.dd}); Basic or API key auth; several node URLs. |
http | json, ndjson, hec, cef, leef | POST or PUT; bearer, Splunk or basic auth; extra headers; custom CA. |
kafka | json, cef, leef | Producer with optional domain key, compression, acks, TLS and SASL. |
file | json | JSON lines at an absolute path; follows external log rotation. |
Every output has min_risk_level (write only events at or above clean, low_risk, moderate, suspicious, high_risk or critical; empty writes everything) and include_raw (default true: include the original log line).
A JSON record:
{
"@timestamp": "2026-10-01T12:30:45.123Z",
"source": {"id": "core-dns", "name": "Core DNS", "address": "192.0.2.53:41234"},
"event": {
"raw": "<30>Oct 1 12:30:45 ns1 named[1234]: client 192.0.2.10#53211: query: www.example.com IN A",
"fields": {"client_ip": "192.0.2.10", "query_type": "A"}
},
"indicator": {"value": "www.example.com", "domain": "example.com", "normalized": "example.com"},
"verdict": {"score": 82, "risk_level": "high_risk", "evidence_saturation": "strong", "signal_count": 4, "cached": false, "status": "ok"}
}
| Field | Meaning |
|---|---|
indicator.value | The host name from the log line. |
indicator.domain | The registrable domain that was looked up; the verdict belongs to it. |
verdict.score | Risk score, 0-100. |
verdict.risk_level | clean, low_risk, moderate, suspicious, high_risk, critical, or empty (no verdict). |
verdict.cached | true when the verdict came from the connector's cache. |
verdict.status | ok: VerdictIQ knows the domain. not_found: VerdictIQ has no data for it. test: a record written by Send test event. |
Risk level to severity, for clean, low_risk, moderate, suspicious, high_risk, critical (empty as clean): CEF 0, 2, 4, 6, 8, 10; LEEF sev 1, 2, 4, 6, 8, 10; syslog 6, 6, 5, 4, 3, 2.
The test record (domain verdictiq-connector-test.invalid, verdict.status: test) bypasses min_risk_level so the connection check always works. Exclude it in searches with verdict.status != test (Kibana: NOT verdict.status:test).
Every event ends in exactly one place: an output, a filter or a drop counter. The counters are on the Metrics page.
| Counter | Meaning |
|---|---|
events_received | Events received from sources. |
parse_ok | Events whose domain was extracted. |
cache_hits / cache_misses | Events answered from the verdict cache, memory or Redis (no API use) / events that needed a lookup. |
cache_redis_errors | Redis failures that switched the cache to memory. |
inflight_joined | Events that joined a lookup already in progress (no extra API use). |
batches_sent | API requests sent. |
api_retries | API requests repeated after a temporary error. |
events_sent | Records written by outputs, summed over outputs. |
send_errors | Failed output write attempts (each retry counts). |
| Drop / filter counter | Meaning |
|---|---|
parse_failed | The parser did not recognize the line: check it with Test parser. |
parse_ignored | Recognized but not a query (for example dnsmasq reply lines). |
domain_invalid | Not a public domain name. |
receive_errors | Input rejected (oversize message, bad framing, wrong HTTP token). |
dropped_eps_limit | Over limits.eps. |
dropped_queue_full | Queue full: lookups slower than the input (raise batch.concurrency or limits.queue_size). |
dropped_no_output | No output is working, so nothing is looked up. |
dropped_api_failure | No verdict (API stopped, request failed after retries, domain rejected): see API status. |
filtered_below_risk | Below an output's min_risk_level (per output). |
dropped_output | An output could not write after retries: see its Failing message. |
dropped_rejected | The destination refused the record (for example an Elasticsearch mapping conflict). |
API errors are broken down by type: unauthorized, forbidden, quota_exceeded, rate_limited, server, network, item (one domain the API could not evaluate), other.
Last 24 hours totals (Overview cards and the Metrics page) are kept in hourly buckets and saved to /data/state/metrics-24h.json (about 1 KB) once a minute and at shutdown, so they survive a restart. Counters since start, the per-minute chart and Live Events start from zero after a restart; the verdict cache does too unless it is in Redis.
Distinct domain counts and risk distributions are left to your SIEM.
| Status | Meaning | Lookups | What to do |
|---|---|---|---|
ok | Working. | Running | |
not_configured | No account connected and no API key. | Stopped | Sign in, or enter an API key in Settings. |
sign_in_required | No API key and the account token expired or was revoked. | Stopped | Sign in again, or enter an API key. |
auth_error | The key or account token was rejected. | Stopped | Check the key in the portal, or sign in again. |
forbidden | Missing bulk verdict permission, or account suspended. | Stopped | Check the key's permission in the portal. |
quota_exceeded | The plan limit (account) or the key's quota is used up. | Stopped | Add an API key, extend the plan, or wait for the quota to renew. |
unreachable | Network, timeout or server error. | Retrying | Check outbound HTTPS, DNS, firewall, proxy. |
While stopped, cached domains are still delivered and other events count as dropped_api_failure; the credential is rechecked every 60 seconds without quota use and lookups resume on their own.
Quota:
not_found) uses one unit too.cache.ttl (default 1 hour), under any host name. Concurrent events for the same domain share one lookup.GET /health returns {"status":"ok"}; GET /version returns the version. Both are on the UI port, and on VIQ_CONNECTOR_HEALTH_LISTEN when set.docker logs viq-connector); config loaded shows the effective settings with the key masked. Repeated warnings are limited to about one line per minute.Update: the configuration lives on the volume, so pull the new image and recreate the container with the same volume.
docker pull verdictiq/connector:<new version>
docker stop -t 30 viq-connector && docker rm viq-connector
# run the same "docker run" command with the new tag
With Compose: change the tag, then docker compose pull && docker compose up -d.
Backup: the whole state is the /data volume.
docker run --rm -v viq-connector-data:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/viq-connector-data.tgz -C /data .
Export config is not a full backup: secrets are masked in it.
Remove:
docker rm -f viq-connector viq-connector-redis
docker volume rm viq-connector-data viq-connector-redis # configuration, cache
Mount the log directory (not a single file, so rotation is seen) and PEM certificates read-only at the paths used in the configuration, for example -v /var/log/named:/var/log/named:ro. Files must be readable by UID 65532.
Run one replica with a PVC on /data and the Recreate strategy. Set VIQ_CONNECTOR_LISTEN=127.0.0.1:5005 and open the UI with kubectl port-forward; set VIQ_CONNECTOR_HEALTH_LISTEN=0.0.0.0:5006 for the liveness and readiness probes (GET /health). Expose only the log input ports with a Service. Run Redis as a sidecar bound to 127.0.0.1 and map the name redis to 127.0.0.1 with hostAliases, so the default redis:6379 reaches it.
localhost, 127.0.0.1 and [::1] (plus VIQ_CONNECTOR_ALLOWED_HOSTS) and rejects cross-site requests. Sessions end after 24 hours or on restart.-p 127.0.0.1:5005:5005, SSH tunnel, kubectl port-forward): anyone who reaches it and signs in can change where outputs send data. The start-up warning about 0.0.0.0 inside the container is harmless with this mapping.X-Forwarded-Proto: https) and VIQ_CONNECTOR_ALLOWED_HOSTS./data is writable.| Symptom | Check |
|---|---|
http://localhost:5005 does not open | Container running (docker ps)? Port published with -p 127.0.0.1:5005:5005? SSH tunnel open on a remote host? |
The UI answers host_not_allowed | Opened under another host name: use localhost or set VIQ_CONNECTOR_ALLOWED_HOSTS. |
| The container exits at start | docker logs viq-connector: an invalid config field line names the setting. |
events_received stays at 0 | Source not enabled; listen port not the published container port; host firewall; UDP vs TCP mismatch. |
Many parse_failed | Wrong preset: test lines from Live Events with Test parser. |
Everything dropped_api_failure | API status on the Overview: not signed in, key rejected, quota used up, network blocked. |
Everything dropped_no_output | No output enabled, or the output failed to start: see its status. |
| File source reads nothing | Directory mounted at the configured path and readable by UID 65532? |
| Overview note "Verdict cache: in memory" | Redis not reachable at the address shown: is the redis container running on the same network? Settings > Verdict cache > Test connection. Without Redis on purpose, choose Memory only. |
For support, include the version (/version), the relevant log lines and an Export config (secrets masked).
License: TBD.
Support: TBD.
Content type
Image
Digest
sha256:327a64df3…
Size
5.5 MB
Last updated
30 days ago
docker pull verdictiq/connector