Skip to content

Agent

tis-agent (tis/tis-agent) runs on the machines that use the TIS lists – firewalls, DNS resolvers – as one long-running service with one configuration file. It replaces the client scripts tis-pull.sh, tis-push.sh, tis-hits.sh and tis-cron.sh:

  • Pull jobs download an export on a schedule and deploy it to a local target – BIND, Unbound and dnsmasq configuration, a plain file, an ipfw or pf table, an nftables or ipset set – checking it first, reloading the service and rolling back when the reload fails, like tis-pull.sh.
  • Push jobs send local content – a file, a URL or a command's output – to an import-only source, like tis-push.sh.
  • Hit counting reports how often the lists blocked something, for the hit statistics in the web GUI: packets ipfw diverts to the agent, the blocks the BIND RPZ log and the ipfw log record, like tis-hits.sh, and on Linux the element counters of nftables sets and the packets nftables rules log to an NFLOG group.
  • Check-in reports the agent's state to the platform every minute, so the machine shows up on the Devices page with its jobs, and the platform notices when it goes offline or a job keeps failing (v0.5.0+).
  • fail2ban: the IPs fail2ban bans go to an import-only source as indicators and count as hits when the lists have them (v0.8.0+).
  • Healthchecks ping a Healthchecks.io-style monitor around every run, like tis-cron.sh --healthcheck-url.

The configuration reference describes every setting; Migration from the scripts turns tis-cron.d steps into jobs.

Security model

  • The agent only connects out – to the platform over HTTPS, and to the URLs of push jobs and healthchecks (without the token). It listens on no port and runs nothing the platform sends.
  • Only the machine's configuration file defines the targets: which file or table is written and which commands check, reload and revert it. Commands run directly, never through a shell.
  • The token lives in the configuration file (owned by root, mode 600; the agent warns when others can read it) or in TIS_TOKEN, never on the command line.
  • Upgrades come only as packages; the agent never updates itself.

Requirements

  • FreeBSD 13 or later (amd64), or Linux – Debian/Ubuntu packages for amd64 and arm64 with systemd.
  • Root: the agent writes DNS server configuration and firewall tables and runs their reload commands.
  • A TIS API token (see Tokens & ACL) allowed to export the lists it pulls (export/group/<list>, export/private/<source>), to import into the sources it pushes to (import/<source>) and to report hits (hits), e.g.

    token:create --name fw-01 --role guest --acl '["export/group/customer-x","hits"]'
    
  • Hit counting from a divert socket: ipfw and the ipdivert kernel module (FreeBSD); from logs: read access to them.

Installation

The packages are published to the package registry of the agent's project as tis-agent, each with its .sha256.

FreeBSD – tis-agent-<version>.pkg:

pkg add ./tis-agent-<version>.pkg

It installs /usr/local/bin/tis-agent, the rc.d script /usr/local/etc/rc.d/tis_agent and /usr/local/etc/tis-agent.json.sample.

Debian/Ubuntu – tis-agent_<version>_amd64.deb or _arm64.deb:

apt install ./tis-agent_<version>_amd64.deb

It installs /usr/bin/tis-agent, the systemd unit tis-agent.service, /usr/share/doc/tis-agent/examples/config.json and the directory /etc/tis-agent (mode 750). The service is not started on the first install – it needs its configuration first; an upgrade restarts a running one. apt purge also removes the configuration and the state.

Quick start

  1. Copy the sample and keep it private – it holds the token:

    # FreeBSD
    cp /usr/local/etc/tis-agent.json.sample /usr/local/etc/tis-agent.json
    chmod 600 /usr/local/etc/tis-agent.json
    # Debian/Ubuntu
    cp /usr/share/doc/tis-agent/examples/config.json /etc/tis-agent/config.json
    chmod 600 /etc/tis-agent/config.json
    
  2. Fill in platform.token and adjust the targets and jobs, e.g. a BIND RPZ zone and an ipfw table from the list customer-x:

    {
      "platform": { "token": "<token>" },
      "targets": {
        "rpz":       { "type": "bind", "path": "/usr/local/etc/namedb/tis.rpz" },
        "blocklist": { "type": "ipfw-table", "table": "tis-blocklist", "rule": 1000 }
      },
      "jobs": [
        { "name": "rpz", "every": "5m",
          "pull": { "list": "customer-x", "format": "rpz", "type": "fqdn", "target": "rpz" } },
        { "name": "blocklist", "every": "10m",
          "pull": { "list": "customer-x", "type": "ipv4", "target": "blocklist" } }
      ]
    }
    
  3. Check it and see what the first run would change:

    tis-agent check
    tis-agent run -dry-run
    
  4. Start the service – FreeBSD:

    sysrc tis_agent_enable=YES
    service tis_agent start
    

    Debian/Ubuntu:

    systemctl enable --now tis-agent
    
  5. Watch the jobs: tis-agent status.

Commands

tis-agent [-config <file>] [-log-timestamps=false] [command]

  daemon            run the jobs on their schedules and count hits (default)
  run [job ...]     run jobs once – all without names – and exit
                      -dry-run  show what would change, change nothing
                      -force    download or push even when unchanged
  hits              count the hits in the logs once and report them
                      -dry-run  print the counts, send and save nothing
  check             check the configuration
  status            show the last result of every job
  version           print the version

The configuration is /usr/local/etc/tis-agent.json on FreeBSD and /etc/tis-agent/config.json elsewhere, unless -config names another.

run exits with the codes of tis-pull.sh – the highest of the jobs run:

Code Meaning
0 Success, or nothing to change
1 Download, upload or other error
2 Deployment failed (reload or table load); previous list kept
3 The check failed; nothing deployed

A job runs at most once at a time: run fails while the daemon runs the same job.

A deployment logs how many entries it added and removed against the list deployed before, e.g. [blocklist] deployed 20125 entries to nftables set inet filter tis_v4 (+14 -14) (since tis-agent 0.4.0); the generation time and the SOA serial don't count.

status lists every job with its type, schedule, last run and result (deployed, pushed, unchanged, not modified, empty or error with the message), the number of entries and the last change:

JOB        TYPE  EVERY   LAST RUN             RESULT        ENTRIES  LAST CHANGE          ERROR
rpz        pull  5m0s    2026-10-22 14:05:12  not modified  48213    2026-10-22 09:35:07  -
blocklist  pull  10m0s   2026-10-22 14:01:40  deployed      1310     2026-10-22 14:01:40  -
local      push  15m0s   2026-10-22 14:00:03  unchanged     12       2026-10-22 06:00:02  -

Running

The daemon starts each scheduled job after a random delay of up to a tenth of its interval (at most a minute), so many machines don't hit the platform at once, and then runs it every interval; a run never overlaps the previous one. Jobs without every run only by tis-agent run.

After failed runs in a row the wait grows, so a platform that is down isn't asked again and again: the first failure keeps the interval, then 2×, 4×, … up to an hour (or the interval when it is longer); a success brings the interval back. The log says when the next run comes, e.g. [rpz] 3 failures in a row; the next run in 20m0s. A run that finds the job busy (a tis-agent run of it) doesn't count as a failure. The passes over the hit logs back off the same way.

  • SIGHUP re-reads the configuration; an invalid one is logged and the running configuration kept.
  • SIGTERM/SIGINT stop it: a deployment that has started finishes (its commands have a 5-minute limit), downloads are dropped, the remaining hits are sent.

FreeBSD – the rc.d script:

service tis_agent start
service tis_agent reload    # after editing the configuration
service tis_agent stop

start and reload check the configuration first. The script passes tis_agent_config (default /usr/local/etc/tis-agent.json) and tis_agent_flags, loads ipdivert (a failure only stops the divert hit counting) and logs to syslog with the tag tis_agent – by default to the security facility (/var/log/security) with priority notice, without the agent's own timestamps; tis_agent_syslog_facility and tis_agent_syslog_priority change it.

Debian/Ubuntu – the systemd unit:

systemctl reload tis-agent    # after editing the configuration
journalctl -u tis-agent

The unit checks the configuration before starting and reloading, restarts the agent when it fails, keeps the state in /var/lib/tis-agent and logs to the journal.

Building, testing and releasing are described in the agent's repository.