Skip to content
Configuration

Configuration

Every Kartis component is configured through a YAML file plus a small number of Docker Compose / .env variables. All three long-running commands take -c/--config <path>:

ComponentDefault config pathConfig type
kartis-scan (serve, daemon, scan)/etc/kartis/kartis-scan.yamlConfig (cmd/kartis-scan/config.go)
kartis-discovery/etc/kartis/kartis-discovery.yamlDiscoveryDaemonConfig
kartis-recon/etc/kartis/kartis-recon.yamlReconDaemonConfig
kartis-scheduler/etc/kartis/kartis-scheduler.yamlconfig (cmd/kartis-scheduler/main.go)

For local development, docker-compose.yml mounts a .docker.yaml variant of each file into the container at those same paths. For kartis-scan.yaml the only difference is the Postgres/Redis hostnames (postgres/redis instead of a host IP or localhost). For kartis-discovery.yaml and kartis-recon.yaml the docker variant additionally: env-expands db.password/redis.password (${POSTGRES_PASSWORD}/${REDIS_PASSWORD}) instead of a literal value, adds shodan_api_key/securitytrails_api_key entries (discovery only), and sets a different ai.model (claude-sonnet-4-6 in the docker config vs claude-sonnet-4-20250514 on the host). Diff configs/*.yaml against configs/*.docker.yaml for the exact deltas rather than assuming hostnames are the only change.

kartis-scan.yaml

name: kartis-scan
listen: ":8080"
database: "postgres://postgres:secret@<host>:5432/kartis?sslmode=disable"

redis:
  host: localhost
  port: 6379
  channel: scan
  password: ""

scan_profile: polite   # polite | normal | aggressive — which entry in `profiles` is active

profiles:
  polite:
    nmap_timing: T2
    nmap_scan_type: "-sT"
    max_rate_pps: 50
    inter_probe_delay_ms: 200
    concurrent_services_per_ip: 1
    concurrent_runs: 5
  normal:
    nmap_timing: T3
    nmap_scan_type: "-sT"
    max_rate_pps: 200
    inter_probe_delay_ms: 50
    concurrent_services_per_ip: 2
    concurrent_runs: 10
  aggressive:
    nmap_timing: T4
    nmap_scan_type: "-sS"
    max_rate_pps: 1000
    inter_probe_delay_ms: 0
    concurrent_services_per_ip: 4
    concurrent_runs: 20
  • listen defaults to :8080 and scan_profile to polite if omitted.
  • profiles tunes nmap pacing (timing template, scan type, max packets/sec, inter-probe delay) and concurrency (services scanned per IP at once, concurrent_runs = the daemon’s worker-pool size) — this is the same profile value that can be overridden per-run via --profile or the API’s profile field, orthogonal to scan depth (max_level).
  • -sT (polite/normal) is a plain TCP connect scan; -sS (aggressive) is a raw-socket SYN scan and needs NET_RAW capability (the dev compose file grants it via cap_add: ["NET_RAW"]).

Portal

The customer-facing portal is served by kartis-scan serve and is disabled by default, so an existing deployment that only serves Eon Center keeps the surface it has. See Portal for what it exposes.

portal:
  enabled: true
  trusted_proxies: "127.0.0.1"   # who may set X-Forwarded-For / X-Forwarded-Proto
  insecure: false                # drop the __Host- cookie prefix and Secure flag
  smtp_host: localhost           # delivers the login codes
  smtp_port: 25
  mail_from: kartis@example.com
KeyMeaning
enabledMount /portal/... at all. Default false
trusted_proxiesAddresses or CIDRs whose X-Forwarded-For and X-Forwarded-Proto are believed. Empty means believe nobody — right when nothing fronts the process, wrong the moment nginx does
insecureDrops the __Host- cookie prefix and the Secure flag, for a test over plain HTTP. The portal is otherwise served behind TLS and the browser would reject the cookie
smtp_host / smtp_port / mail_fromDelivery of the one-time login codes. Without a host configured the portal falls back to a capturing mailer that writes the code to the log — for a test deployment, and it says so loudly at startup

kartis-scheduler.yaml

The scheduler needs the database and nothing else: it does not scan, does not talk to Redis, and serves no HTTP.

database: "postgres://postgres:secret@<host>:5432/kartis"
interval: "1m"   # how often to look for due sessions
batch: 50        # most runs placed per tick

interval only decides how long after a slot a run is placed, since cadences are monthly or biweekly. batch caps a backlog so it is worked through over several ticks rather than arriving at the customer all at once.

kartis-discovery.yaml

name: kartis-discovery

redis:
  host: localhost
  port: 6379
  channel: discovery
  password: ""

db:
  host: localhost
  port: 5432
  database: kartis
  username: postgres
  password: secret

rate_limit:
  whois_per_minute: 10
  ctlogs_per_minute: 30
  reverse_ip_per_minute: 20
  asn_per_minute: 30
  dns_recon_per_minute: 60
  tech_fp_per_minute: 20
  git_recon_per_minute: 30
  ai_calls_per_minute: 10

sources:
  enable_whois: true
  enable_ctlogs: true
  enable_reverse_ip: true
  enable_asn: true
  enable_dns_recon: true
  enable_tech_fingerprint: true
  enable_git_recon: false
  enable_ai_correlation: true

  enable_reverse_whois: false        # opt-in, needs whoisxml_api_key (paid)
  whoisxml_api_key: ${WHOISXML_API_KEY}

  enable_brand_permutation: false    # opt-in — permute brand label across common TLDs
  enable_ai_org_expansion: false     # opt-in — LLM-derived reverse-WHOIS pivot terms, needs ai.api_key

ai:
  provider: anthropic               # anthropic | openai | ollama (local)
  api_key: ${ANTHROPIC_API_KEY}
  model: claude-sonnet-4-20250514
  # base_url: http://localhost:11434   # ollama only
  max_tokens: 4096
  temperature: 0.3

Each enable_* flag under sources turns a discovery source on or off independently; the run <domain> recursive-expansion parameters (--depth, --min-confidence, --max-related-per-job, --max-jobs) are CLI flags, not config keys (see Scanning).

kartis-recon.yaml

name: kartis-recon

redis:
  host: localhost
  port: 6379
  channel: enum      # MUST be the enum channel — discovery publishes enum jobs here
  password: ""

db:
  host: localhost
  port: 5432
  database: kartis
  username: postgres
  password: secret

ai:
  provider: anthropic
  api_key: ${ANTHROPIC_API_KEY}
  model: claude-sonnet-4-20250514
  max_tokens: 2048
  temperature: 0.4

kartis-recon warns at startup if redis.channel isn’t enum, since it will otherwise never receive the jobs kartis-discovery publishes.

Environment variables (.env / Docker Compose)

.env (copy from .env.example, or generate it with ./deploy/setup.sh) feeds variable substitution into docker-compose.yml and deploy/docker-compose.prod.yml, and is what the ${VAR} references inside the YAML files above expand from at container startup.

VariablePurpose
POSTGRES_PASSWORD / POSTGRES_USER / POSTGRES_DBPostgres credentials — no default in prod compose
REDIS_PASSWORDRedis auth; empty disables auth (dev / single-host)
KARTIS_TAGimage tag docker-compose.prod.yml pulls (default latest)
KARTIS_PORThost-side port mapped to the API’s internal 8080
KARTIS_SCAN_LISTENlisten address baked into kartis-scan.yaml by setup.sh
KARTIS_SCAN_PROFILEdefault scan profile: polite | normal | aggressive
KARTIS_SCAN_CONTACTcontact URL/mailto: appended to the scanner’s User-Agent and SSH ident comment
KARTIS_SCAN_EHLOhostname announced in SMTP EHLO/HELO (default scanner.eon-kartis)
ANTHROPIC_API_KEYenables the AI-assisted subdomain source and the run --judge review pass
SHODAN_API_KEYenables the Shodan host/port enrichment source
SECURITYTRAILS_API_KEYoptional discovery enrichment source
WHOISXML_API_KEYenables reverse-WHOIS (enable_reverse_whois)

An empty API key simply disables the corresponding optional source — none are required for the core discovery/scan pipeline to run.

Bearer tokens

kartis-scan serve authenticates every /v1/... route except /healthz and /readyz with a DB-backed bearer token (hashes stored in the auth_tokens table):

kartis-scan token create --name operator   # prints the plaintext token once
kartis-scan token list                     # prefixes + metadata, no plaintext
kartis-scan token revoke <prefix-or-uuid>

Store the printed token as a secret in whatever consumes the API (Eon Center, Eon Aethis, Eon Insights, an operator’s ~/.kartis-token, CI); it cannot be retrieved again after creation.

Deployment lifecycle

For a Linux x86 target, make package TAG=v1.0.0 builds a deploy tarball (deploy/build-package.sh); on the target, ./setup.sh interactively writes .env and the runtime YAMLs, ./install.sh brings the stack up via Docker Compose, and ./upgrade.sh applies an upgrade package on top of an existing install (backing up compose, .env, and YAML under .upgrade-backup-<timestamp>/). Locally, make dev / make dev-down bring the same stack up and down against the repo’s own docker-compose.yml.