AnubisWatchDOCUMENTATION

Search the hall

Search guides, configuration fields and command examples.
↑↓ select · Enter openEsc close
GitHub
Documentation
Documentation/Start here
THE FIELD GUIDE

Configuration

The full JSON and YAML reference for your server, storage, authentication and monitors.

Project source · da59007Updated 2026-10-17

Complete guide to anubis.yaml configuration options


Quick Start

BASH
# Generate default configuration
anubis init

# Edit configuration
vim anubis.yaml

# Start with custom config
anubis serve --config /path/to/anubis.yaml

Minimum Valid Configuration

YAML
# anubis.yaml - Minimal configuration
server:
  port: 8080

storage:
  path: /var/lib/anubis/data

Server Configuration

Root server Block

FieldTypeDefaultRequiredDescription
hoststring"0.0.0.0"NoBind address for HTTP server
portinteger8080 from anubis initNoHTTP server port; use 443 or another HTTPS port when TLS is enabled
grpc_portinteger0NoManagement gRPC port. 0 disables gRPC; a positive value explicitly enables it
tlsobject-NoTLS configuration
allowed_originsstring[][]NoOrigins permitted to open WebSocket/CORS connections. Empty = none allowed
trusted_proxiesstring[][]NoProxy IPs whose X-Forwarded-For is trusted for the real client IP (REST + WebSocket rate limiting). Empty = trust none (use RemoteAddr)
grpc_reflectionbooleanfalseNoEnable gRPC server reflection (leave off in production)
metrics_authbooleanfalseNoRequire a bearer token on /metrics. Default is open for Prometheus scraping; enable in multi-tenant or internet-exposed deployments, since /metrics exposes monitor names/latencies across all workspaces

Note on environment: setting the top-level environment: "production" rejects any config with TLS disabled. When HTTP TLS is terminated upstream (ingress/reverse proxy), use environment: "production-proxied" so the instance may serve plain HTTP behind the proxy.

Management gRPC security: grpc_port: 0 is the secure default. In production and production-proxied, enabling gRPC requires process TLS because the API carries bearer tokens, unless server.host is an explicit loopback IP (127.0.0.1 or ::1) for a local-only tunnel. An HTTP ingress does not automatically protect the separate gRPC listener. gRPC binds to the same configured server.host, and unreadable/invalid TLS certificate files abort startup rather than falling back to plaintext.

server.tls Block

FieldTypeDefaultRequiredDescription
enabledbooleantrueNoEnable HTTPS
certstring-ConditionalPath to TLS certificate (PEM)
keystring-ConditionalPath to TLS private key (PEM)

Examples

Basic HTTP (No TLS)

YAML
server:
  host: "0.0.0.0"
  port: 8080
  tls:
    enabled: false

Bring Your Own Certificates

YAML
server:
  host: "0.0.0.0"
  port: 8443
  tls:
    enabled: true
    cert: "/etc/ssl/certs/anubis.crt"
    key: "/etc/ssl/private/anubis.key"
    min_version: 3

server.tls Block

FieldTypeDefaultDescription
enabledboolfalseTerminate TLS on the HTTP and gRPC listeners
certstring-PEM certificate path. Required when enabled: true
keystring-PEM private key path. Required when enabled: true
min_versionint0Minimum TLS version: 0 (unset — TLS 1.2), 3 (TLS 1.2), 4 (TLS 1.3)

min_version applies to both the REST and gRPC listeners. 1 (TLS 1.0) and 2 (TLS 1.1) are rejected at config load rather than honoured — both protocols are deprecated, and accepting them would lower the server below its TLS 1.2 floor. Any other value is also rejected, so a typo fails fast instead of silently falling back.

Removed: server.tls.prefer_server is accepted for backward compatibility but does nothing. It mapped onto tls.Config.PreferServerCipherSuites, which Go has ignored since 1.17 — the runtime chooses cipher preference. Delete it from your config.

Storage Configuration

Root storage Block

FieldTypeDefaultRequiredDescription
pathstring-YesPath to CobaltDB data directory
encryptionobject-NoDisk encryption settings
timeseriesobject-NoTime-series compaction rules
retentionobject-NoData retention policies

storage.encryption Block

FieldTypeDefaultRequiredDescription
enabledbooleanfalseNoEnable AES-256 encryption
keystring-Conditional32-byte encryption key (use env var)

storage.timeseries.compaction Block

Every check result is folded into a 1-minute summary as it is saved. A background job then rolls summaries up into coarser ones every 5 minutes. A bucket is rolled up once its whole span is older than the threshold, and is built only once.

FieldTypeDefaultDescription
raw_to_minuteduration48hUnused: 1-minute summaries are written as results arrive
minute_to_fiveduration168h1-minute → 5-minute summaries
five_to_hourduration720h5-minute → hourly summaries
hour_to_dayduration720hHourly → daily summaries

storage.timeseries.retention Block

Data older than these periods is deleted once an hour, starting at server start.

FieldTypeDefaultDescription
rawduration2160hKeep raw check results (judgments)
minuteduration720hKeep 1-minute summaries
fiveduration2160hKeep 5-minute summaries
hourduration8760hKeep hourly summaries
daydurationunlimitedKeep daily summaries

Status pages (90 days of uptime bars), soul logs and dashboard queries read raw check results, so a raw period shorter than 90 days leaves gaps in them.

Keep each summary level longer than its compaction threshold plus one bucket of the next level (for example hour > hour_to_day + 24h). Otherwise its oldest buckets are deleted before they are rolled up, and the server logs a warning at startup.

Example

YAML
storage:
  path: "/var/lib/anubis/data"
  encryption:
    enabled: true
    key: "${ANUBIS_ENCRYPTION_KEY}"
  timeseries:
    compaction:
      raw_to_minute: 48h
      minute_to_five: 168h
      five_to_hour: 720h
      hour_to_day: 720h
    retention:
      raw: 2160h
      minute: 720h
      five: 2160h
      hour: 8760h
      day: unlimited

Cluster Configuration (Necropolis)

Root necropolis Block

FieldTypeDefaultRequiredDescription
enabledbooleanfalseNoEnable cluster mode
node_namestringAuto-generatedNoHuman-readable node name
regionstring"default"NoGeographic region for routing
bind_addrstring"0.0.0.0:7946"NoAddress to bind Raft/gossip
advertise_addrstringAuto-detectedNoAddress advertised to peers
cluster_secretstring-ConditionalShared HMAC secret. It authenticates gossip and every Raft RPC (votes, pre-votes, AppendEntries, heartbeats, snapshots, forwarded writes, joins); see Cluster traffic authentication below. Must be identical on every node. Without it cluster traffic is unauthenticated and the node logs a warning at startup. Leave empty in single-node mode (anubis serve --single); the Helm chart enforces presence when config.necropolis.enabled=true
discoveryobject-NoNode discovery configuration
raftobject-NoRaft consensus tuning
distributionobject-NoHow souls are shared between nodes

necropolis.distribution Block

In cluster mode every soul is owned by exactly one live node, which is the only node that probes it and evaluates its alert rules. Judgments and incidents replicate through Raft, so every node shows every soul's history. Ownership is recomputed when souls change or a node joins, leaves or stops answering the leader (after five election timeouts), without a restart.

Journeys are shared the same way: each journey (configured or created through the API) runs on exactly one live node, chosen by the same strategy over the journey ID, and its runs replicate so every node lists them. A standalone node runs its configured journeys as before.

When a soul moves to a new owner, alerting continues where the old owner left off: the record of when each rule last notified about the soul (its cooldown) is replicated, and the new owner seeds the consecutive_failures streak and the status_for clock from the soul's replicated judgments, so a failover does not re-send an alert inside the rule's cooldown. Per-channel rate-limit counters remain per node.

FieldTypeDefaultDescription
strategystring"hash"hash (rendezvous hashing: a membership change moves only the affected node's souls), round-robin (exactly even counts, but adding or removing a soul can move others), region-aware (prefer nodes in the soul's region, balance by count), load-based (same as round-robin today). Unrecognised values fall back to hash with a warning. A soul's regions restriction is always honoured.

redundancy and rebalance_interval are parsed but not used.

Writes may be sent to any node: a follower forwards them to the leader and answers once it has applied them itself. When no leader is reachable a write is refused with 503 Service Unavailable and a Retry-After header.

Nodes can be added and removed at runtime. Each change is a replicated joint-consensus configuration change, one at a time.

Joining by address. Start the new node with one or more existing members' Raft addresses and no peers:

BASH
anubis serve --join 10.0.1.10:7946          # or necropolis.raft.join: ["10.0.1.10:7946"]

A node with raft.join, no raft.peers and bootstrap: false never elects itself; it owns no souls or journeys and asks the join addresses to add it under its own raft.node_id. When it is added it learns every member's real node ID and address from the answer and becomes an ordinary voter (the membership is persisted, so later restarts do not need --join).

  • With a cluster_secret, the join request is authenticated and the new node is admitted automatically.
  • Without a secret, joining is not automatic (anyone who can reach the Raft port could otherwise add themselves). Add the waiting node from any member with anubis summon <new-node-raft-addr>: the server asks the node for its node ID and adds it under that ID. --name <node-id> is still accepted to name it explicitly.

anubis banish <node-id> removes a member. The REST equivalents are POST /api/v1/cluster/nodes (node_id optional: omitted, it is learned from the node at address) and DELETE /api/v1/cluster/nodes/{id}. Statically configured raft.peers keep working as before.

Cluster traffic authentication

With cluster_secret set, every frame on the Raft transport carries an HMAC-SHA256 signature over the RPC method, the sender's node ID, a timestamp, a per-process epoch and sequence number, and the payload; responses are signed and bound to their request. A receiver rejects a frame that is unsigned, signed with another secret, altered, timestamped more than 60 seconds from its own clock, replayed, or sent by a node that is not a current member (a removed node holding the secret can no longer vote or replicate). Replay protection is a fixed-size sliding window per sender, so it adds no per-message allocation to the heartbeat path. Keep node clocks within 60 seconds of each other (NTP). Every node must use the same secret: a node with a different or missing secret cannot talk to the cluster. TLS (raft.tls) can be layered on top for confidentiality.

necropolis.discovery Block

FieldTypeDefaultRequiredDescription
modestring"gossip"Nomdns, gossip, or manual
seedsarray-ConditionalSeed nodes for gossip/manual

necropolis.raft Block

FieldTypeDefaultDescription
election_timeoutduration1000msFollower → candidate timeout
heartbeat_timeoutduration300msLeader heartbeat interval
joinarray-Raft addresses of existing members to ask for admission (see Joining by address); used only while the node has no peers and is not bootstrapping. anubis serve --join <addr> appends to it

Example: Single Node

YAML
necropolis:
  enabled: false

Example: 3-Node Cluster with Gossip

YAML
necropolis:
  enabled: true
  node_name: "jackal-eu-01"
  region: "eu-west"
  bind_addr: "0.0.0.0:7946"
  advertise_addr: "203.0.113.10:7946"
  cluster_secret: "${ANUBIS_CLUSTER_SECRET}"
  discovery:
    mode: "gossip"
    seeds:
      - "jackal-us-01.example.com:7946"
      - "jackal-apac-01.example.com:7946"
  raft:
    election_timeout: 1000ms
    heartbeat_timeout: 300ms

Example: Manual Discovery

YAML
necropolis:
  enabled: true
  node_name: "jackal-us-01"
  region: "us-east"
  cluster_secret: "${ANUBIS_CLUSTER_SECRET}"
  discovery:
    mode: "manual"
    seeds:
      - "10.0.1.10:7946"
      - "10.0.1.11:7946"

Authentication

Root auth Block

FieldTypeDefaultRequiredDescription
typestring"local"Nolocal, oidc, or ldap
localobject-ConditionalLocal auth settings
oidcobject-ConditionalOIDC provider settings
ldapobject-ConditionalLDAP settings

auth.local Block

FieldTypeDefaultRequiredDescription
admin_emailstring-ConditionalAdmin user email
admin_passwordstring-ConditionalAdmin password (use env var)

The configured admin_password seeds the account on first start. A password changed later (dashboard Settings → Change Password or the API) is stored hashed in <storage.path>/sessions.json and takes precedence over the config file after a restart. To recover a lost password, stop the server and run:

BASH
anubis admin reset-password            # prompts; or --password-stdin

This revokes all sessions. Password reset tokens from POST /api/v1/auth/reset-password are not delivered anywhere yet.

auth.oidc Block

FieldTypeDefaultRequiredDescription
issuerstring-YesOIDC issuer URL
client_idstring-YesOAuth client ID
client_secretstring-YesOAuth client secret
redirect_uristringAuto-generatedNoCallback URL
scopesarray["openid", "email", "profile"]NoOAuth scopes

auth.ldap Block

FieldTypeDefaultRequiredDescription
hoststring-YesLDAP server hostname
portinteger389NoLDAP port (389 or 636 for LDAPS)
use_tlsbooleanfalseNoUse LDAPS
base_dnstring-YesBase DN for searches
bind_dnstring-ConditionalService account DN
bind_passwordstring-ConditionalService account password
user_filterstring(uid=%s)NoLDAP filter for user lookup
group_dnstring-NoRequired group DN

Example: Local Auth

YAML
auth:
  type: "local"
  local:
    admin_email: "admin@example.com"
    admin_password: "${ANUBIS_ADMIN_PASSWORD}"

Example: OIDC (Google Workspace)

YAML
auth:
  type: "oidc"
  oidc:
    issuer: "https://accounts.google.com"
    client_id: "${GOOGLE_OAUTH_CLIENT_ID}"
    client_secret: "${GOOGLE_OAUTH_CLIENT_SECRET}"
    redirect_uri: "https://anubis.example.com/oauth/callback"
    scopes:
      - "openid"
      - "email"
      - "profile"

Example: LDAP (Active Directory)

YAML
auth:
  type: "ldap"
  ldap:
    host: "dc01.example.com"
    port: 636
    use_tls: true
    base_dn: "DC=example,DC=com"
    bind_dn: "CN=anubis,CN=Users,DC=example,DC=com"
    bind_password: "${LDAP_BIND_PASSWORD}"
    user_filter: "(sAMAccountName=%s)"
    group_dn: "CN=AnubisAdmins,CN=Users,DC=example,DC=com"

Dashboard

Root dashboard Block

FieldTypeDefaultRequiredDescription
enabledbooleantrueNoEnable embedded dashboard
brandingobject-NoCustom branding options

dashboard.branding Block

FieldTypeDefaultRequiredDescription
titlestring"AnubisWatch"NoBrowser title
logostring-NoCustom logo URL
themestring"auto"Noauto, dark, or light

Example

YAML
dashboard:
  enabled: true
  branding:
    title: "My Company Monitoring"
    logo: "/assets/logo.png"
    theme: "dark"

Souls (Monitored Targets)

Root souls Array

Each soul represents a monitored target.

FieldTypeDefaultRequiredDescription
namestring-YesHuman-readable name
typestring-YesCheck type (see below)
targetstring-YesTarget URL/host
weightduration60sNoCheck interval
timeoutduration10sNoCheck timeout
enabledbooleantrueNoEnable/disable check
tagsarray-NoTags for filtering/alerting
regionsarray-NoRegions to run checks from
<type>object-ConditionalProtocol-specific config

Supported Types

TypeProtocolConfig Block
httpHTTP/HTTPShttp
tcpTCPtcp
udpUDPudp
dnsDNSdns
smtpSMTPsmtp
imapIMAPimap
icmpICMP Pingicmp
grpcgRPCgrpc
websocketWebSocketwebsocket
tlsTLS Certtls

http Block

FieldTypeDefaultDescription
methodstringGETHTTP method
headersobject-Custom headers
bodystring-Request body
valid_statusarray[200-299]Accepted status codes
body_containsstring-Required body substring
body_regexstring-Body regex pattern
json_pathobject-JSON path assertions
json_schemastring-JSON Schema (draft 2020-12)
featherduration-Max response time budget
follow_redirectsbooleantrueFollow redirects
max_redirectsinteger5Max redirect count

HTTP Example

YAML
souls:
  - name: "Production API"
    type: http
    target: "https://api.example.com/health"
    weight: 30s
    timeout: 10s
    tags: ["production", "api", "critical"]
    http:
      method: GET
      headers:
        Authorization: "Bearer ${API_TOKEN}"
      valid_status: [200, 201]
      body_contains: '"status":"ok"'
      json_path:
        "$.status": "ok"
        "$.services.db": "connected"
      feather: 500ms

tcp Block

FieldTypeDefaultDescription
banner_matchstring-Expected banner regex
sendstring-Data to send (hex or text)
expectstring-Expected response

TCP Example

YAML
souls:
  - name: "PostgreSQL Primary"
    type: tcp
    target: "db-primary.example.com:5432"
    weight: 15s
    timeout: 5s
    tags: ["database", "critical"]
    tcp:
      banner_match: "PostgreSQL"

dns Block

FieldTypeDefaultDescription
record_typestringADNS record type
expectedarray-Expected values
nameserversarraySystem DNSNameservers to query
propagation_checkbooleanfalseCheck propagation
propagation_thresholdinteger100Required agreement %
dnssec_validatebooleanfalseValidate DNSSEC

DNS Example

YAML
souls:
  - name: "Primary DNS"
    type: dns
    target: "example.com"
    weight: 60s
    timeout: 10s
    tags: ["dns", "critical"]
    dns:
      record_type: A
      expected: ["93.184.216.34"]
      nameservers:
        - "8.8.8.8:53"
        - "1.1.1.1:53"
      propagation_check: true
      propagation_threshold: 100

tls Block

FieldTypeDefaultDescription
expiry_warn_daysinteger30Days until warning
expiry_critical_daysinteger7Days until critical
min_protocolstringTLS1.2Minimum TLS version
expected_issuerstring-Expected CA issuer
check_ocspbooleanfalseCheck OCSP stapling
min_key_bitsinteger2048Minimum key size

TLS Example

YAML
souls:
  - name: "API Certificate"
    type: tls
    target: "api.example.com:443"
    weight: 3600s
    timeout: 10s
    tags: ["security", "certificate"]
    tls:
      expiry_warn_days: 30
      expiry_critical_days: 7
      min_protocol: "TLS1.2"
      expected_issuer: "Let's Encrypt"
      check_ocsp: true
      min_key_bits: 2048

grpc Block

FieldTypeDefaultDescription
servicestring-gRPC service name
tlsbooleanfalseUse TLS
metadataobject-Request metadata
featherduration-Response time budget

gRPC Example

YAML
souls:
  - name: "Payment Service gRPC"
    type: grpc
    target: "grpc.example.com:9090"
    weight: 30s
    timeout: 5s
    tags: ["production", "grpc", "payments"]
    grpc:
      service: "payment.PaymentService"
      tls: true
      metadata:
        x-api-key: "${GRPC_API_KEY}"
      feather: 200ms

websocket Block

FieldTypeDefaultDescription
headersobject-Upgrade headers
subprotocolsarray-WebSocket subprotocols
sendstring-Message to send
expect_containsstring-Expected response substring
ping_checkbooleanfalseValidate ping/pong

WebSocket Example

YAML
souls:
  - name: "Realtime Feed"
    type: websocket
    target: "wss://ws.example.com/feed"
    weight: 60s
    timeout: 10s
    tags: ["websocket", "realtime"]
    websocket:
      headers:
        Authorization: "Bearer ${WS_TOKEN}"
      subprotocols: ["graphql-ws"]
      send: '{"type":"connection_init"}'
      expect_contains: "connection_ack"
      ping_check: true

icmp Block

FieldTypeDefaultDescription
countinteger4Number of pings
intervalduration200msBetween pings
max_loss_percentinteger0Max packet loss %
featherduration-Latency budget
privilegedbooleantrueUse raw sockets

ICMP Example

YAML
souls:
  - name: "Gateway Router"
    type: icmp
    target: "10.0.0.1"
    weight: 15s
    timeout: 5s
    tags: ["network", "infrastructure"]
    icmp:
      count: 5
      interval: 200ms
      max_loss_percent: 20
      feather: 100ms
      privileged: true

Alert Channels

Root channels Array

FieldTypeDefaultRequiredDescription
namestring-YesChannel name
typestring-YesChannel type
<type>object-YesType-specific config

Supported Channel Types

TypeDescriptionConfig Block
slackSlack webhookslack
discordDiscord webhookdiscord
telegramTelegram bottelegram
emailSMTP emailemail
pagerdutyPagerDutypagerduty
opsgenieAtlassian Opsgenieopsgenie
smsTwilio SMSsms
ntfyNtfy.sh pushntfy
webhookGeneric webhookwebhook

slack Block

FieldTypeDefaultDescription
webhook_urlstring-Slack incoming webhook
channelstring-Override channel
usernamestringAnubisWatchBot username
icon_emojistring:anubis:Bot emoji icon
mention_on_criticalarray-Users/roles to mention

Slack Example

YAML
channels:
  - name: "ops-slack"
    type: slack
    slack:
      webhook_url: "${SLACK_WEBHOOK_URL}"
      channel: "#ops-alerts"
      username: "AnubisWatch"
      icon_emoji: ":anubis:"
      mention_on_critical: ["@oncall-team"]

discord Block

FieldTypeDefaultDescription
webhook_urlstring-Discord webhook URL
usernamestringAnubisWatchBot username
avatar_urlstring-Bot avatar URL

Discord Example

YAML
channels:
  - name: "dev-discord"
    type: discord
    discord:
      webhook_url: "${DISCORD_WEBHOOK_URL}"
      username: "AnubisWatch"

email Block

FieldTypeDefaultDescription
smtp_hoststring-SMTP server
smtp_portinteger587SMTP port
starttlsbooleantrueUse STARTTLS
usernamestring-SMTP username
passwordstring-SMTP password
fromstring-From address
toarray-Recipients
subject_templatestring-Go template for subject

Email Example

YAML
channels:
  - name: "ops-email"
    type: email
    email:
      smtp_host: "smtp.example.com"
      smtp_port: 587
      starttls: true
      username: "${SMTP_USER}"
      password: "${SMTP_PASS}"
      from: "anubis@example.com"
      to:
        - "ops@example.com"
        - "cto@example.com"
      subject_template: "[{{ .Severity }}] {{ .Soul.Name }} — {{ .Judgment.Status }}"

pagerduty Block

FieldTypeDefaultDescription
integration_keystring-PagerDuty integration key
severity_mapobject-Custom severity mapping
auto_resolvebooleantrueAuto-resolve on recovery

PagerDuty Example

YAML
channels:
  - name: "pagerduty-oncall"
    type: pagerduty
    pagerduty:
      integration_key: "${PAGERDUTY_INTEGRATION_KEY}"
      severity_map:
        critical: "critical"
        warning: "warning"
        info: "info"
      auto_resolve: true

ntfy Block

FieldTypeDefaultDescription
serverstringhttps://ntfy.shNtfy server URL
topicstring-Topic name
priority_mapobject-Priority mapping

Ntfy Example

YAML
channels:
  - name: "ntfy-push"
    type: ntfy
    ntfy:
      server: "https://ntfy.sh"
      topic: "anubis-alerts"
      priority_map:
        critical: "urgent"
        warning: "high"
        info: "default"

webhook Block

FieldTypeDefaultDescription
urlstring-Webhook URL
methodstringPOSTHTTP method
headersobject-Custom headers
body_templatestring-Go template for body
timeoutduration10sRequest timeout

Generic Webhook Example

YAML
channels:
  - name: "custom-webhook"
    type: webhook
    webhook:
      url: "https://hooks.example.com/alerts"
      method: POST
      headers:
        Authorization: "Bearer ${WEBHOOK_TOKEN}"
        Content-Type: "application/json"
      body_template: |
        {
          "severity": "{{ .Severity }}",
          "soul": "{{ .Soul.Name }}",
          "status": "{{ .Judgment.Status }}",
          "timestamp": "{{ .Judgment.Timestamp }}"
        }

Alert Rules (Verdicts)

Root verdicts Block

FieldTypeDefaultRequiredDescription
rulesarray-NoAlert rules list
escalationarray-NoEscalation policies

verdicts.rules[] Block

FieldTypeDefaultRequiredDescription
namestring-YesRule name
scopestring"all"NoTarget scope
conditionobject-YesTrigger condition
severitystring"warning"NoAlert severity
channelsarray-NoChannels to notify
cooldownduration5mNoMin time between alerts

Scope Formats

FormatDescription
allAll souls
tag:<tag>Souls with specific tag
type:<type>Souls of specific type
soul:<name>Specific soul by name
region:<region>Souls in specific region

Condition Types

consecutive_failures

FieldTypeDefaultDescription
typestring-consecutive_failures
thresholdinteger-Number of failures
YAML
condition:
  type: consecutive_failures
  threshold: 3

threshold

FieldTypeDefaultDescription
typestring-threshold
metricstring-Metric name
operatorstring->, <, >=, <=, ==
valuestring-Threshold value
windowduration-Evaluation window
YAML
condition:
  type: threshold
  metric: latency_p95
  operator: ">"
  value: "500ms"
  window: 5m

percentage

FieldTypeDefaultDescription
typestring-percentage
metricstring-Metric name
operatorstring-Comparison operator
thresholdinteger-Percentage threshold
windowduration-Evaluation window
YAML
condition:
  type: percentage
  metric: failure_rate
  operator: ">"
  threshold: 10
  window: 5m

Available Metrics

MetricDescription
latency_p5050th percentile latency
latency_p9595th percentile latency
latency_p9999th percentile latency
latency_maxMaximum latency
failure_rateFailure percentage
uptime_percentUptime percentage
tls_days_until_expiryDays until cert expiry
propagation_percentDNS propagation %
packet_loss_percentICMP packet loss %

Example Rules

YAML
verdicts:
  rules:
    - name: "Service Down - Critical"
      scope: "all"
      condition:
        type: consecutive_failures
        threshold: 3
      severity: critical
      channels: ["ops-slack", "pagerduty-oncall"]
      cooldown: 5m

    - name: "High Latency"
      scope: "tag:production"
      condition:
        type: threshold
        metric: latency_p95
        operator: ">"
        value: "1s"
        window: 5m
      severity: warning
      channels: ["ops-slack"]
      cooldown: 15m

    - name: "Certificate Expiring Soon"
      scope: "tag:security"
      condition:
        type: threshold
        metric: tls_days_until_expiry
        operator: "<"
        value: 14
      severity: warning
      channels: ["ops-email"]
      cooldown: 24h

Escalation Policies

YAML
verdicts:
  escalation:
    - name: "production-escalation"
      stages:
        - wait: 0s
          channels: ["ops-slack", "ntfy-push"]
        - wait: 5m
          channels: ["pagerduty-oncall"]
          condition: "not_acknowledged"
        - wait: 15m
          channels: ["ops-email", "sms-oncall"]
          condition: "not_resolved"

Synthetic Monitoring (Journeys)

Root journeys Array

FieldTypeDefaultRequiredDescription
namestring-YesJourney name
weightduration300sNoRun interval
timeoutduration30sNoTotal timeout
continue_on_failurebooleanfalseNoContinue on step failure
variablesobject-NoJourney variables
stepsarray-YesJourney steps

steps[] Block

FieldTypeDefaultRequiredDescription
namestring-YesStep name
typestringhttpNoStep type
targetstring-YesTarget URL
httpobject-NoHTTP config
extractobject-NoVariable extraction

extract Block

FieldTypeDefaultDescription
<var_name>object-Extraction rule

Extraction Rule

FieldTypeDefaultDescription
fromstring-body, header, cookie
json_pathstring-JSON path (for body)
header_namestring-Header name
cookie_namestring-Cookie name
regexstring-Regex pattern

Journey Example

YAML
journeys:
  - name: "User Login Flow"
    weight: 300s
    timeout: 30s
    continue_on_failure: false
    variables:
      base_url: "https://app.example.com"
      test_email: "${TEST_EMAIL}"
      test_password: "${TEST_PASSWORD}"
    steps:
      - name: "Get CSRF Token"
        type: http
        target: "${base_url}/login"
        http:
          method: GET
          valid_status: [200]
        extract:
          csrf_token:
            from: body
            json_path: "$.csrf_token"

      - name: "Perform Login"
        type: http
        target: "${base_url}/api/auth/login"
        http:
          method: POST
          headers:
            Content-Type: "application/json"
            X-CSRF-Token: "${csrf_token}"
          body: |
            {"email": "${test_email}", "password": "${test_password}"}
          valid_status: [200]
        extract:
          auth_token:
            from: body
            json_path: "$.token"

      - name: "Fetch Dashboard"
        type: http
        target: "${base_url}/api/dashboard"
        http:
          method: GET
          headers:
            Authorization: "Bearer ${auth_token}"
          valid_status: [200]
          feather: 2s

Logging

Root logging Block

FieldTypeDefaultRequiredDescription
levelstring"info"NoLog level
formatstring"json"NoLog format
outputstring"stdout"NoLog output
filestring-ConditionalLog file path

level Options

  • debug - All logs
  • info - Informational and above
  • warn - Warnings and errors
  • error - Errors only

format Options

  • json - Structured JSON
  • text - Human-readable text

output Options

  • stdout - Standard output
  • file - File (requires file field)

Example

YAML
logging:
  level: "info"
  format: "json"
  output: "file"
  file: "/var/log/anubis/anubis.log"

Security

YAML
security:
  # Probes and alert channels refuse private/loopback targets by default
  # (SSRF protection). Enable to monitor services on your LAN.
  allow_private_targets: false
  # Narrower alternative: only these private ranges are reachable.
  allowed_networks:
    - "10.20.0.0/16"
  # Honour per-soul insecure_skip_verify (MITM risk; dev/test only).
  allow_insecure_skip_verify: false

allow_private_targets covers 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, fc00::/7 and ::1. Link-local and cloud metadata addresses (169.254.0.0/16, 100.100.100.200, …) stay blocked regardless. ANUBIS_SSRF_ALLOW_PRIVATE=1 has the same effect as allow_private_targets: true.

A soul whose target is blocked is recorded as dead with the reason in the judgment message, so it shows up in the dashboard and triggers alert rules.


Environment Variable Expansion

AnubisWatch supports environment variable expansion in configuration values:

Syntax

SyntaxDescription
${VAR}Replace with VAR value (empty if unset)
${VAR:-default}Use VAR or default if unset
${VAR:+alternate}Use alternate if VAR is set, empty otherwise

Examples

YAML
storage:
  encryption:
    key: "${ANUBIS_ENCRYPTION_KEY}"  # Required, fails if unset

necropolis:
  cluster_secret: "${ANUBIS_CLUSTER_SECRET}"  # Required when clustering is enabled

server:
  tls:
    cert: "${ANUBIS_CERT:-/etc/anubis/tls/cert.pem}"

Configuration Validation

Validate your configuration file:

BASH
# Validate config syntax
anubis config validate --config anubis.yaml

# Show the resolved config path
anubis config path --config anubis.yaml

Common Errors

ErrorCauseFix
missing required field 'path'Storage path not setAdd storage.path
invalid duration formatBad duration syntaxUse 1h, 30m, 60s, 500ms
unknown protocol 'htp'Typo in typeUse http, tcp, etc.
invalid JSON pathMalformed JSON pathUse $.key.subkey format
TLS cert not foundMissing certificate fileCheck server.tls.cert path

Example Configurations

Minimal Single-Node

YAML
# anubis.yaml - Minimal
server:
  port: 8080

storage:
  path: /var/lib/anubis/data

souls:
  - name: "Google DNS"
    type: dns
    target: "google.com"
    weight: 60s
    dns:
      record_type: A

Production-Ready Single Node

YAML
server:
  host: "0.0.0.0"
  port: 8443
  tls:
    enabled: true
    cert: "/etc/ssl/certs/anubis.crt"
    key: "/etc/ssl/private/anubis.key"

storage:
  path: /var/lib/anubis/data
  encryption:
    enabled: true
    key: "${ANUBIS_ENCRYPTION_KEY}"

auth:
  type: "local"
  local:
    admin_email: "admin@example.com"
    admin_password: "${ANUBIS_ADMIN_PASSWORD}"

souls:
  - name: "Production API"
    type: http
    target: "https://api.example.com/health"
    weight: 30s
    tags: ["production", "api", "critical"]
    http:
      valid_status: [200]
      body_contains: '"status":"ok"'

channels:
  - name: "ops-slack"
    type: slack
    slack:
      webhook_url: "${SLACK_WEBHOOK_URL}"

verdicts:
  rules:
    - name: "Service Down"
      scope: "tag:production"
      condition:
        type: consecutive_failures
        threshold: 3
      severity: critical
      channels: ["ops-slack"]
      cooldown: 5m

3-Node Cluster

See deployment/guide.md for cluster configuration examples.


See Also


⚖️ The Judgment Never Sleeps

Adapted from the AnubisWatch project. Examples use placeholder services and credentials.View project source Suggest a correction

AnubisWatch is built and maintained by

oxognet.

A small studio in Estonia building AI agents, developer tools and the infrastructure behind them.

Made in Estonia. Shipped worldwide.