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>:
| Component | Default config path | Config type |
|---|---|---|
kartis-scan (serve, daemon, scan) | /etc/kartis/kartis-scan.yaml | Config (cmd/kartis-scan/config.go) |
kartis-discovery | /etc/kartis/kartis-discovery.yaml | DiscoveryDaemonConfig |
kartis-recon | /etc/kartis/kartis-recon.yaml | ReconDaemonConfig |
kartis-scheduler | /etc/kartis/kartis-scheduler.yaml | config (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: 20listendefaults to:8080andscan_profiletopoliteif omitted.profilestunes 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 sameprofilevalue that can be overridden per-run via--profileor the API’sprofilefield, orthogonal to scan depth (max_level).-sT(polite/normal) is a plain TCP connect scan;-sS(aggressive) is a raw-socket SYN scan and needsNET_RAWcapability (the dev compose file grants it viacap_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| Key | Meaning |
|---|---|
enabled | Mount /portal/... at all. Default false |
trusted_proxies | Addresses 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 |
insecure | Drops 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_from | Delivery 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 tickinterval 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.3Each 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.4kartis-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.
| Variable | Purpose |
|---|---|
POSTGRES_PASSWORD / POSTGRES_USER / POSTGRES_DB | Postgres credentials — no default in prod compose |
REDIS_PASSWORD | Redis auth; empty disables auth (dev / single-host) |
KARTIS_TAG | image tag docker-compose.prod.yml pulls (default latest) |
KARTIS_PORT | host-side port mapped to the API’s internal 8080 |
KARTIS_SCAN_LISTEN | listen address baked into kartis-scan.yaml by setup.sh |
KARTIS_SCAN_PROFILE | default scan profile: polite | normal | aggressive |
KARTIS_SCAN_CONTACT | contact URL/mailto: appended to the scanner’s User-Agent and SSH ident comment |
KARTIS_SCAN_EHLO | hostname announced in SMTP EHLO/HELO (default scanner.eon-kartis) |
ANTHROPIC_API_KEY | enables the AI-assisted subdomain source and the run --judge review pass |
SHODAN_API_KEY | enables the Shodan host/port enrichment source |
SECURITYTRAILS_API_KEY | optional discovery enrichment source |
WHOISXML_API_KEY | enables 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.