Sign inSign up

verdictiq/connector

By verdictiq

•Updated 30 days ago

Enrich DNS and security logs with VerdictIQ domain verdicts and forward them to your SIEM.

Image
0

174

verdictiq/connector repository overview

⁠VerdictIQ Connector

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.

  • Image: verdictiq/connector
  • Platforms: linux/amd64, linux/arm64
  • Base: distroless static, runs as non-root (UID 65532), no shell
  • Setup: web UI with a setup wizard; the whole configuration is one YAML file on the /data volume

⁠How it works

source -> parser -> EPS limit -> queue -> verdict cache -> batch -> VerdictIQ API -> risk filter -> outputs
  1. A source (syslog, HTTP / Splunk HEC, Kafka, log file) receives a line. Its parser extracts the domain.
  2. The EPS limit and a bounded queue protect the connector from bursts. Excess events are dropped and counted, never buffered without limit.
  3. The connector looks up the registrable domain: 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.
  4. Other domains are collected into a batch and sent to POST /verdict/bulk when the batch is full or its wait time passes.
  5. Each output writes the records at or above its own minimum risk level, as JSON, CEF or LEEF.

API use therefore follows the distinct registrable domains per hour, not the event rate.

⁠Requirements

  • Docker Engine 20.10 or later (or Docker Desktop) on linux/amd64 or linux/arm64.
  • A VerdictIQ account. Create one in the VerdictIQ portal before the first sign-in. An API key (sk_...) is optional: without it, lookups use the account's plan limit.
  • Outbound HTTPS to api.verdictiq.io, auth.verdictiq.io and portal.verdictiq.io (HTTPS_PROXY, HTTP_PROXY, NO_PROXY honored), and from your browser to the portal.
  • Network reachability from your log senders to the input ports you publish, and from the connector to your outputs.

⁠Quick start

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
OptionWhy
-p 127.0.0.1:5005:5005Web UI. Published on the host's loopback address only, so it is not reachable from the network.
-p 514:5514/udp -p 514:5514/tcpSyslog 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:8088HTTP / Splunk HEC input. Drop it if you do not push logs over HTTP.
-v viq-connector-data:/dataKeeps the configuration and state across restarts and upgrades.
--stop-timeout 30Lets 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.

⁠Docker Compose

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

⁠First setup

  1. Open 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.
  2. Click Sign in with VerdictIQ account, sign in on the portal tab that opens and choose Allow access; the connector continues in the first tab.
  3. The setup wizard asks for:
    • First source, for example UDP syslog on :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.
    • First output, where enriched events go. Send test event checks the connection.
  4. Point your devices at port 514 of the Docker host (or the port you published).
  5. Watch Live Events and Metrics.

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.

⁠The UI
PageWhat it shows
OverviewStatus, warning bands, last 24 hours totals, event rate, cache hit rate and location, last 60 minutes chart.
Sources & OutputsState and counters; add, edit, enable / disable, delete; Test parser, Send test event.
Live EventsRecent events, parse failures and drops, with the raw line and the reason.
MetricsLast 24 hours totals, counters since start, last 60 minutes chart, latencies.
SettingsAccount (Log out, Disconnect account), optional API key, batch / cache (Redis) / limit settings, Export config / Import config.
Usage & QuotaOpens the portal's usage page in a new tab.

⁠Ports, volume and environment variables

Container portPurpose
5005/tcpWeb UI and its API (publish on 127.0.0.1 only)
5514/udp, 5514/tcpSyslog input (example; each source sets its own port)
8088/tcpHTTP / 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).

VariableDefault in the imageMeaning
VIQ_CONNECTOR_CONFIG/data/config.yamlConfig file path. The directory must be writable by UID 65532.
VIQ_CONNECTOR_LISTEN0.0.0.0:5005UI address inside the container.
VIQ_CONNECTOR_HEALTH_LISTENoffSeparate 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_HOSTSemptyExtra 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.

⁠Configuration

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.

  • Settings > Export config downloads the file with secrets masked; Import config keeps the stored secret when the imported value is masked or empty.
⁠Main settings
KeyDefaultRangeMeaning
api.base_urlhttps://api.verdictiq.io/api/v2/iocURLVerdictIQ API root. Change only if VerdictIQ tells you to.
api.keyemptyOptional API key (sk_...). When set, every lookup uses it; empty means lookups use the connected account.
api.timeout30s1s - 5mTimeout of one API request.
api.portal_urlhttps://portal.verdictiq.ioURLPortal root for the Usage & Quota link.
auth.issuerhttps://auth.verdictiq.ioURLVerdictIQ sign-in server. Change only if VerdictIQ tells you to.
batch.max_size10001 - 10000Domains per API request.
batch.max_wait5s1s - 60sLongest wait of a domain for its batch: the added delay for a domain not yet cached.
batch.concurrency21 - 4Parallel API requests.
cache.ttl1h5m - 24hHow long a verdict is reused. Higher means less quota use and older verdicts.
cache.backendredisredis, memoryredis: verdicts kept in Redis across restarts, memory while Redis is down. memory: this process only.
cache.redis.addressredis:6379host:portRedis server; also cache.redis.password, db (0), key_prefix (viq:verdict:).
cache.max_entries5000001000 - 5000000In-memory cache capacity (local copy, or the whole cache without Redis), in registrable domains.
limits.eps20001 - 2000Events per second accepted over all sources; the excess is dropped and counted. 2000 is the ceiling of this version.
limits.queue_size500001000 - 500000Events waiting for lookup; when full, new events are dropped and counted.
⁠Example: syslog in, Elasticsearch and SIEM out
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.

⁠Inputs and parsers

Source typeUse it whenMain settings
syslogDevices and DNS servers send syslog over UDP, TCP or TLS (newline or octet-counted framing, mutual TLS optional).syslog.protocol, syslog.listen, syslog.tls.*
httpA 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.*
kafkaDNS logs are already on a Kafka topic (consumer group, TLS, SASL PLAIN / SCRAM).kafka.brokers, kafka.topics, kafka.group_id, kafka.start_offset
fileA 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):

PresetFor
bind, infobloxBIND (named) and Infoblox NIOS query logs (query: <name> IN <type>)
dnsmasqdnsmasq with log-queries (Pi-hole, routers)
windows_dnsWindows Server DNS debug log
cef, leefDevices sending CEF or LEEF (standard host / DNS keys, or parser.kv_key)
jsonJSON logs: the dotted path in parser.json_field, for example dns.question.name
kvkey=value logs: the key in parser.kv_key
regexAnything 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).

⁠Outputs and the enriched record

Output typeFormatsNotes
syslogjson, cef, leefUDP or TCP, RFC 5424, facility local0, reconnects on its own.
elasticsearchjsonElasticsearch and OpenSearch Bulk API; date pattern in the index name (verdictiq-%{+yyyy.MM.dd}); Basic or API key auth; several node URLs.
httpjson, ndjson, hec, cef, leefPOST or PUT; bearer, Splunk or basic auth; extra headers; custom CA.
kafkajson, cef, leefProducer with optional domain key, compression, acks, TLS and SASL.
filejsonJSON 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"}
}
FieldMeaning
indicator.valueThe host name from the log line.
indicator.domainThe registrable domain that was looked up; the verdict belongs to it.
verdict.scoreRisk score, 0-100.
verdict.risk_levelclean, low_risk, moderate, suspicious, high_risk, critical, or empty (no verdict).
verdict.cachedtrue when the verdict came from the connector's cache.
verdict.statusok: 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).

⁠Metrics

Every event ends in exactly one place: an output, a filter or a drop counter. The counters are on the Metrics page.

CounterMeaning
events_receivedEvents received from sources.
parse_okEvents whose domain was extracted.
cache_hits / cache_missesEvents answered from the verdict cache, memory or Redis (no API use) / events that needed a lookup.
cache_redis_errorsRedis failures that switched the cache to memory.
inflight_joinedEvents that joined a lookup already in progress (no extra API use).
batches_sentAPI requests sent.
api_retriesAPI requests repeated after a temporary error.
events_sentRecords written by outputs, summed over outputs.
send_errorsFailed output write attempts (each retry counts).
Drop / filter counterMeaning
parse_failedThe parser did not recognize the line: check it with Test parser.
parse_ignoredRecognized but not a query (for example dnsmasq reply lines).
domain_invalidNot a public domain name.
receive_errorsInput rejected (oversize message, bad framing, wrong HTTP token).
dropped_eps_limitOver limits.eps.
dropped_queue_fullQueue full: lookups slower than the input (raise batch.concurrency or limits.queue_size).
dropped_no_outputNo output is working, so nothing is looked up.
dropped_api_failureNo verdict (API stopped, request failed after retries, domain rejected): see API status.
filtered_below_riskBelow an output's min_risk_level (per output).
dropped_outputAn output could not write after retries: see its Failing message.
dropped_rejectedThe 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.

⁠API status and quota

StatusMeaningLookupsWhat to do
okWorking.Running
not_configuredNo account connected and no API key.StoppedSign in, or enter an API key in Settings.
sign_in_requiredNo API key and the account token expired or was revoked.StoppedSign in again, or enter an API key.
auth_errorThe key or account token was rejected.StoppedCheck the key in the portal, or sign in again.
forbiddenMissing bulk verdict permission, or account suspended.StoppedCheck the key's permission in the portal.
quota_exceededThe plan limit (account) or the key's quota is used up.StoppedAdd an API key, extend the plan, or wait for the quota to renew.
unreachableNetwork, timeout or server error.RetryingCheck 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:

  • Each domain sent to the API uses one unit of quota; a domain VerdictIQ has no data for (not_found) uses one unit too.
  • A registrable domain is looked up at most once per cache.ttl (default 1 hour), under any host name. Concurrent events for the same domain share one lookup.
  • Test connection uses no quota. Without Redis the cache is empty after a restart, so the first hour uses more.
  • The portal's Usage & Quota page is the authoritative usage figure.
⁠Account and API key
  • The first sign-in connects the connector to that account's organization; later sign-ins must use the same organization.
  • Without an API key, lookups use the account's plan limit (Settings shows the plan). While a key is set, every lookup uses it; Remove API key switches back.
  • Log out ends this browser's UI session; lookups continue. Disconnect account revokes the account token and logs every browser out; without an API key, lookups stop until someone signs in again.

⁠Health checks and logs

  • GET /health returns {"status":"ok"}; GET /version returns the version. Both are on the UI port, and on VIQ_CONNECTOR_HEALTH_LISTEN when set.
  • Logs are JSON lines on stdout (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, backup and removal

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
⁠Log files from the host and certificates

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.

⁠Kubernetes

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.

⁠Security

  • Only domain names are sent to VerdictIQ, over HTTPS.
  • The UI requires a VerdictIQ sign-in of the connected organization, answers only to 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.
  • Keep the UI on localhost anyway (-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.
  • Another UI host name needs an HTTPS reverse proxy (X-Forwarded-Proto: https) and VIQ_CONNECTOR_ALLOWED_HOSTS.
  • Input ports are meant for your network. The HTTP input always requires a token; use TLS where possible.
  • Runs as non-root (UID 65532) on a distroless image; a read-only root file system works as long as /data is writable.

⁠Troubleshooting

SymptomCheck
http://localhost:5005 does not openContainer 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_allowedOpened under another host name: use localhost or set VIQ_CONNECTOR_ALLOWED_HOSTS.
The container exits at startdocker logs viq-connector: an invalid config field line names the setting.
events_received stays at 0Source not enabled; listen port not the published container port; host firewall; UDP vs TCP mismatch.
Many parse_failedWrong preset: test lines from Live Events with Test parser.
Everything dropped_api_failureAPI status on the Overview: not signed in, key rejected, quota used up, network blocked.
Everything dropped_no_outputNo output enabled, or the output failed to start: see its status.
File source reads nothingDirectory 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 and support

License: TBD.

Support: TBD.

Tag summary

Content type

Image

Digest

sha256:327a64df3…

Size

5.5 MB

Last updated

30 days ago

docker pull verdictiq/connector