Deployment configuration
config.yaml contains deployment settings. Create monitoring jobs and manage
users, destinations, scanner profiles, baselines, and public status in the web
console. See config.example.yaml for the complete
validated schema.
| Configuration | Managed in | Purpose |
|---|---|---|
database, retention |
YAML | SQLite location and history retention. |
timezone |
YAML | Optional IANA timezone for log, CLI, notification, and console times, and the default for new jobs. |
web.listen, web.allowed_hosts, web.trusted_proxies, web.forwarded_header |
YAML | Loopback listener, approved proxy hostnames, trusted proxy networks, and the single forwarding header used for client IPs. |
web.ipv6_rate_limit_prefix |
YAML | Prefix length by which the rate limits group IPv6 client addresses. |
web.auth_key_file |
YAML/secrets | Optional separate key for the encrypted TOTP seeds. |
web.source_url |
YAML | Source of a modified or forked build, the target of the console’s Source code link. |
web.max_live_streams, web.max_live_streams_per_unit |
YAML | Live-update streams the deployment keeps open, and the most that one business unit may hold. |
log.level |
YAML | Log verbosity: debug, info, warn, or error. |
scheduler.* |
YAML | Concurrent scans and probe budgets. |
scanner.target_exclusions |
YAML | Addresses that may never be scanned. |
scanner.sandbox |
YAML | Whether Nmap and Naabu run as an unprivileged identity; see the scanner sandbox. |
scanner.landlock |
YAML | Whether Landlock restricts the files Nmap and Naabu can open; see Landlock. |
scanner.max_job_hosts |
YAML | The most addresses the targets of one job may expand to when it scans, for every unit. |
enrichment.rdap.enabled |
YAML | Enable or disable on-demand public network-registration lookups. |
updates.enabled |
YAML | Enable or disable the three-hour stable-release check. |
notifications.encryption_key_file |
YAML/secrets | Optional separate key for the encrypted notification destinations. |
notifications.sandbox |
YAML | Whether the notification process runs as an unprivileged identity, restricted with Landlock; see the notification sandbox. |
notifications.urls, urls_file |
YAML/secrets | Deprecated. Imported once as web-managed destinations; see Notifications. |
backup.directory, backup.schedule, backup.keep |
YAML | Optional verified, rotated backups that the daemon takes on a schedule; see Scheduled backups. |
| Jobs, users, profiles, notification destinations | Web console | Runtime administration stored in SQLite. |
The YAML jobs section from older deployments is not imported into the scheduler. Such jobs remain inactive and EdgeWatch shows a startup warning so they can be recreated and reviewed explicitly in the console.
Defaults and limits
Section titled “Defaults and limits”| Setting | Default | Allowed values |
|---|---|---|
database |
/var/lib/edgewatch/edgewatch.db |
A file path. |
retention |
90d |
At least 24h. Durations use Go syntax such as 36h, plus a d suffix for days. |
log.level |
info |
debug, info, warn, or error. |
scheduler.max_concurrent_scans |
1 |
1 to 64. |
scheduler.max_probe_count |
5000000 |
1 to 100000000; 0 is rejected. |
scheduler.max_naabu_probe_count |
20000000 |
1 to 100000000; 0 is rejected. |
scanner.sandbox |
auto |
auto, required, or off. |
scanner.landlock |
auto |
auto, required, or off; required needs scanner.sandbox other than off. |
scanner.max_job_hosts |
65536 |
1 to 1000000. |
web.ipv6_rate_limit_prefix |
64 |
32 to 128; 128 counts each IPv6 address on its own. |
web.auth_key_file |
auth.key next to the database |
A regular file of 32 raw bytes or 64 hexadecimal characters, without group or other permissions. |
notifications.encryption_key_file |
notification.key next to the database |
A regular file of 32 raw bytes or 64 hexadecimal characters with mode 0400 or 0600. |
notifications.sandbox |
auto |
auto, required, or off. |
web.source_url |
The exact Git tag of an official build | An absolute HTTPS URL without credentials, a query, or a fragment, at most 2048 bytes. |
web.max_live_streams |
256 |
1 to 4096; 0 is rejected. |
web.max_live_streams_per_unit |
64, or web.max_live_streams when that is lower |
1 to web.max_live_streams; 0 is rejected. |
backup.directory |
Not set: no scheduled backups | The absolute path of an existing directory, other than /. |
backup.schedule |
0 3 * * * (daily at 03:00) |
A five-field cron expression that fires, in timezone when it is set and UTC otherwise, without a TZ= or CRON_TZ= prefix. Requires backup.directory. |
backup.keep |
7 |
1 to 1000 scheduled backups. Requires backup.directory. |
Jobs are configured in the console, which enforces these limits:
| Job setting | Default | Allowed values |
|---|---|---|
| Timeout | 1h |
1s to 30d. |
| Resume window | 8d |
1h to 30d. |
| Timing profile | Balanced | Conservative, balanced, or fast. |
| Maximum expanded hosts | 256 | 1 to 1000000; scans also stop at scanner.max_job_hosts. |
| Baseline samples | 2 in the console; 1 when omitted through the API | 1 to 100. |
| Change confirmations | 1 | 1 to 100. |
Important defaults
Section titled “Important defaults”timezoneis omitted by default: the daemon and CLI keep the process timezone (UTC in the container image), and each signed-in console shows its browser’s timezone. Set it to an IANA name such asEurope/Amsterdamto use one timezone everywhere. The public status page keeps the visitor’s browser timezone and never receives the configured value. Invalid names stop startup; host recovery commands ignore them.- The web listener defaults to
127.0.0.1:8080; non-loopback listeners are rejected. - Requests using a proxy or tunnel host must match
web.allowed_hosts; foreignHostheaders are rejected before authentication. Keep this list limited to names you control. - Forwarding headers are ignored unless the connecting proxy addresses are
explicitly listed in
web.trusted_proxies. By default, EdgeWatch reads onlyweb.forwarded_header: x-forwarded-for; set it toforwardedonly when your trusted proxy controls that header, ornoneto ignore forwarded client IPs. EdgeWatch never combines the two conventions, so configure the header that your proxy sanitizes or constructs for the trusted proxy chain. - Session cookies use the same trusted-proxy boundary for forwarded HTTPS
protocol headers. A trusted TLS-terminating proxy must send
X-Forwarded-Proto: httpsorForwarded: ...;proto=httpswhen it forwards a loopbackHost; otherwise EdgeWatch keeps the direct-loopback HTTP behavior. - If a tunnel or reverse proxy is not listed in
web.trusted_proxies, every client may appear as the same loopback peer. After five failed login or TOTP attempts within five minutes, all logins through that shared peer receive a short two-second cooldown instead of a five-minute lockout. A successful sign-in through the peer, with any account, does not reset the count; each failure expires five minutes after it happened. Applying the same cooldown to known and unknown usernames avoids revealing account existence. The first-run setup, the platform setup, and account activation through that peer get the same cooldown after five wrong tokens, so wrong tokens cannot block them for five minutes. Password and TOTP confirmations through that peer are limited per account: an account that fails five confirmations within five minutes is refused for five minutes, and other accounts, in any unit or on the platform, are not affected. Configure the proxy network and forwarding header when you need per-client rate limits and audit identities. EdgeWatch logs a startup warning when approved proxy hosts lack trusted client-IP forwarding. - A client identified by its own address may fail five sign-ins within five
minutes. Every failed sign-in counts the same: an unknown username, a
disabled account, an account whose unit is not active, and a wrong
password, one-time code, or recovery code. After that, every sign-in from
that client, with any username, receives the same
429 rate_limitedanswer for five minutes, so neither the answer nor the number of attempts left reveals which accounts exist. A successful sign-in does not reset the count; each failure expires five minutes after it happened. Clients that share one address, such as the clients of an untrusted proxy on another host, share this budget, and a hundred wrong setup or activation tokens from that address block setup and activation for all of them for five minutes. When requests come through a proxy that EdgeWatch does not trust, EdgeWatch logs a warning at most once an hour and shows the proxy’s address on the dashboard of a single unit’s administrators and on the platform status page. Such a proxy is a peer that is not listed inweb.trusted_proxiesand sendsX-Forwarded-FororForwarded, such as an unlisted proxy on the host, or, behind the listed proxies, the first unlisted address in the forwarding chain when the chain names another client before it, such as an unlisted proxy on another host in front of the proxy on the host. A client can send these headers itself and have its own address shown, so add the address toweb.trusted_proxiesonly when it is a proxy that you run. - The rate limits count an IPv6 client by its network of
web.ipv6_rate_limit_prefixbits, a /64 by default, so all addresses of one /64 share the sign-in budget and the other per-client limits, including the anonymous limits of the public pages. IPv4 and loopback addresses are counted as they are, and audit records keep the full address. Setweb.ipv6_rate_limit_prefix: 128when unrelated clients share one /64, or a shorter prefix, down to 32, when one client holds a larger network. - Each account with TOTP may fail ten one-time or recovery codes within 24
hours, whichever clients send them; only a sign-in with the right password
or a TOTP confirmation of a signed-in account counts. After that, the
account’s codes are not checked for 15 minutes: every sign-in with the
right password gets the answer of a wrong code, whatever code it carries,
and a confirmation is refused without spending its code. While the ten
latest wrong codes are less than 24 hours old, each further wrong code
starts another lockout, twice as long as the one before, up to four hours.
Each lockout is recorded once as
auth.second_factor_locked. Unless the account’s owner sent the codes, someone else holds the account’s password, so change it. The counts are kept in memory, so restarting the daemon starts them over. - Each open console holds one live-update stream, and one account at most
four. With more than one active business unit, each unit may hold an equal
share of
web.max_live_streams, at least four and at mostweb.max_live_streams_per_unit, so busy units cannot lock the others out while the active units number at most a quarter ofweb.max_live_streams. A stream over a limit is told to retry later and the console keeps reconnecting. Raiseweb.max_live_streamsfor more than 64 active units or many consoles per unit. - Sessions end after 24 hours without activity and 30 days after sign-in; the daemon removes ended sessions at startup and once a day. An account keeps at most 20 sessions: a new sign-in beyond that ends the account’s least recently used session. A TOTP code or recovery code counts as used only when its sign-in creates a session.
- By default,
scanner.target_exclusionscovers the loopback and link-local ranges127.0.0.0/8,::1/128,169.254.0.0/16, andfe80::/10. The IPv4 link-local range includes the169.254.169.254cloud metadata endpoint. Other metadata endpoints are not excluded by default; add the ones your provider uses, such asfd00:ec2::254/128on AWS with the IPv6 instance metadata endpoint enabled or100.100.100.200/32on Alibaba Cloud. An explicit list replaces the defaults, so keep the default ranges when you add entries. Changescanner.target_exclusionsonly when you understand the host-network exposure. The same list keeps a unit’s notification destinations away from these addresses, and EdgeWatch refuses a unit’s destination on an unspecified, loopback, or link-local address whatever the list holds. An explicitly empty list,[], allows every address for both; see destination addresses. scanner.sandbox: autostarts Nmap and Naabu as UID 65532 with only their raw-packet capabilities when the container grantsSETUID,SETGIDandKILL, as the bundledcompose.yamldoes. Otherwise they run as UID 0 and EdgeWatch warns. Setrequiredto refuse to scan without the sandbox.offalso turns off Landlock.scanner.landlock: autoalso restricts Nmap and Naabu with Landlock to the system files a scan reads and the files EdgeWatch passes to them, and lets only Naabu create files, below/tmp, when the kernel provides it. It installs a seccomp filter that refuses the system calls no scanner needs and makes the kernel’s out-of-memory killer stop a scanner before the daemon. Setrequiredto refuse to scan without Landlock, oroff, which turns off the filter and the limits too, if a scanner needs files outside those paths.- EdgeWatch keeps the files it passes to scanners, Nmap’s XML output and
Naabu’s target list, in
tmp/scannerbeside the database, which only the daemon can open, whateverTMPDIRnames; see scanner files. scanner.max_job_hosts, 65,536 by default, a /16, caps the addresses the targets of one job expand to when it scans, whatever the job’s ownmax_expanded_hostsallows: a scan keeps a scope and a result for every address in the daemon’s memory, which every business unit shares. A job whose targets expand to more fails to scan withexpanded targets exceed scanner.max_job_hosts=65536; split its targets into several jobs. The daemon logs such jobs at startup,edgewatch healthlists them inwarnings, and the job editor warns while you enter them. Raise the setting only with matching container memory: a resumable scan of a /16 peaked at about 344 MB in the 512 MiB limit of the bundledcompose.yaml, and a /14 did not fit. Onlyconfig.yamlsets it, never a unit or the API. A resumable cycle that a release before v0.36.0 planned keeps its pinned addresses and is not checked again; discard it to replan a job whose targets now exceed the setting.notifications.sandbox: autodelivers notifications from a process that runs as UID 65531 without capabilities, restricted with Landlock, when the container grantsSETUID,SETGIDandKILLand that process can read the certificate authoritiesSSL_CERT_FILEandSSL_CERT_DIRname. Setrequiredto refuse to start without it, oroffto deliver from an unconfined process.- RDAP is enabled by default and is requested only when an authenticated user
opens a public host. Private and special-use addresses are never queried.
Set
enrichment.rdap.enabled: falsefor isolated or privacy-sensitive deployments. RDAP lookups need direct HTTPS egress to IANA and the regional registries: they ignoreHTTPS_PROXYand the other proxy variables, which update checks and notifications use, because a proxy would resolve and connect to the registry itself, past the checks that keep lookups away from private addresses. Where only a proxy reaches the internet, host pages show RDAP as unavailable; setenrichment.rdap.enabled: falsethere. The daemon logs a warning at startup when RDAP is enabled and a proxy variable is set. - Scheduled backups are off by default. Setting
backup.directoryturns them on: the daemon writes a backup there onbackup.schedule, checks it as thebackupcommand does before it publishes it, and keeps the newestbackup.keepof them. It removes only its own files, namededgewatch-scheduled-YYYYMMDDTHHMMSSZ.db, and never another file in the directory.edgewatch healthand the console report the newest good backup and any failure. - Update checks are enabled by default, run at startup and every three hours,
and consider stable GitHub releases only. Set
updates.enabled: falsefor offline deployments. Checks reveal the host’s public IP and EdgeWatch user agent to GitHub; EdgeWatch reports updates but never upgrades itself.
Validate changes
Section titled “Validate changes”Validate the edited file in a fresh container before you restart the service.
A running container can still see the previous file when an editor replaces
the bind-mounted file instead of rewriting it, so docker compose exec could
validate the old configuration:
docker compose run --rm --no-deps edgewatch config validate \ --config /etc/edgewatch/config.yamlWhen the result is "valid": true, recreate the container so the daemon reads
the new file:
docker compose up -d --force-recreate edgewatch