Operations

Scheduled scans

Run a scan every week (or on any schedule) with a systemd timer on the deployment host and a service token, see every failure in the journal and on the health card, and renew the token before it expires.

SectionOperations
UpdatedOctober 27, 2026
EditionSelf-hosted Enterprise, public beta

Gateway 0.2.0-beta.48 and later schedule scans in the console (Recurring scans): use that, and this recipe only on an earlier gateway. When you move to the console's schedules, disable these timers and revoke their token; running both scans the target twice. On an earlier gateway, a systemd timer on the deployment host submits a scan on a schedule you choose, through the gateway, with a service token. Each run is an ordinary scan: it takes a licensed slot like a scan someone starts, and its results appear under Scans. A run that fails says so in three places: the host's journal, a failed systemd unit, and the console's health card; each reported run is also an audit row, scheduled_scan.run.

Service tokens last at most 90 days, so the token needs renewing. The script warns two weeks before it expires, and so does the health card (TOKEN_WARN_DAYS changes both); Renew the token below takes two minutes.

The kit ships the parts: self-hosted/scheduled-scan.sh, the unit templates self-hosted/systemd/shakerscan-enterprise-scan@.service and ...@.timer, and an example schedule, self-hosted/scheduled-scan.env.example. The commands below use the installer's paths (/opt/shakerscan/kit, /srv/shakerscan-enterprise).

Before you start

  • Target authorization. With the default setting (Target authorization: automatic), the gateway authorizes the target for active testing when the timer's active scan starts, in the service account's name; the audit log names that account. Two cases need a person first: a deployment set to manual authorization, and a scan that signs in with a stored credential profile. There, authorize the target once in Targets before the first run. A passive scan needs no authorization.
  • Pick a name for the schedule: lower-case letters, digits and dashes, for example weekly-app. It names the files, the timer and the health card line.

1. Create the service token

In the console, open API access and create a token:

  • Service account: a new one for schedules, for example scheduled-scans.
  • Role: Operator (a viewer token cannot start scans).
  • Expires after: up to 90 days.
  • Acts for: Nobody (an automation token).

Choose Show the token, and copy it. It is shown once.

2. Write the schedule

On the deployment host, as root. The token goes into its file through standard input, so it never lands in your shell history or on a command line:

sudo install -d -m 700 /etc/shakerscan/scans
sudo sh -c 'umask 077; cat > /etc/shakerscan/scans/weekly-app.token'
# paste the token, press Enter, then Ctrl-D

The scan to run is the JSON body the API's POST /scans takes. This one runs the standard active checks against one application ("active_testing": true without a preset means standard active; leave policy out for a passive scan):

sudo tee /etc/shakerscan/scans/weekly-app.json > /dev/null <<'EOF'
{"target": "https://app.example.com", "budget_profile": "balanced", "policy": {"active_testing": true}}
EOF

The schedule itself is KEY=VALUE lines, read as data and never run. Only the gateway's address is usually needed; it defaults to the deployment's public address:

sudo tee /etc/shakerscan/scans/weekly-app.env > /dev/null <<'EOF'
GATEWAY_URL=https://scanner.example.com
EVERY_DAYS=7
EOF

All settings, with their defaults, are in scheduled-scan.env.example: the token and request files, DEPLOYMENT (where to report for the health card), CA_FILE for a certificate from your own CA, WAIT (1: wait for the scan and fail unless it completed; 0: submit only), MAX_HOURS (8, at most 48), BUSY_MINUTES (60, at most 1440), TOKEN_WARN_DAYS (14: the journal and the health card warn this many days before the token expires) and POLL_SECONDS (60). The unit has no timeout of its own: these limits end every run, and the run reports why.

Check it before installing the timer. --check reads the schedule, asks the gateway about the token and submits nothing:

sudo /opt/shakerscan/kit/self-hosted/scheduled-scan.sh --check weekly-app
# scheduled-scan weekly-app: token accepted: role operator, expires 2027-01-20 UTC (88 days); licence active

If the host cannot reach its own public name, add that name to /etc/hosts as 127.0.0.1: the gateway's proxy serves it on this host too.

3. Install the timer

Once per host, install the two templates with ExecStart pointing at the kit:

for unit in shakerscan-enterprise-scan@.service shakerscan-enterprise-scan@.timer; do
  sed 's|^ExecStart=/[^ ]*/self-hosted/|ExecStart=/opt/shakerscan/kit/self-hosted/|' \
    /opt/shakerscan/kit/self-hosted/systemd/$unit | sudo tee /etc/systemd/system/$unit > /dev/null
done
sudo systemctl daemon-reload

Then enable one timer per schedule, and run it once now to see it work:

sudo systemctl enable --now shakerscan-enterprise-scan@weekly-app.timer
sudo systemctl start shakerscan-enterprise-scan@weekly-app.service
sudo journalctl -u shakerscan-enterprise-scan@weekly-app -n 20

The timer runs weekly, on Monday at 03:00 host time within half an hour, and at the next boot if the host was off then. For another schedule run sudo systemctl edit shakerscan-enterprise-scan@weekly-app.timer and add:

[Timer]
OnCalendar=
OnCalendar=*-*-* 02:00:00

and set EVERY_DAYS in the schedule to match (here 1), so the health card knows when a run is late. systemctl list-timers 'shakerscan-enterprise-scan@*' shows the next runs.

What a run does

  1. Checks the token with the gateway (/_enterprise/me): it must be an operator token and the licence active. The token's expiry goes to the health card, with a warning in the journal when it is TOKEN_WARN_DAYS (14) days away or less.
  2. Submits the scan under one Idempotency-Key for the whole run. When every licensed slot is busy it waits and tries again, for up to BUSY_MINUTES. When an answer is lost (a timeout, a gateway restart), or the engine says it is still processing an earlier try, it repeats the same submission up to three times: the gateway and the engine treat the repeat as the same scan, so it cannot start a second one. The key is new for each run, so a new run always submits a new scan.
  3. Waits for the scan to end, reading its state every POLL_SECONDS, for up to MAX_HOURS.
  4. Reports the run to the health card and prints the scan's link.

Exit codes: 0 done (the scan completed), 1 failed, 2 the schedule or its files are wrong, 3 the token was refused, 143 stopped. Stopping the unit (systemctl stop, a shutdown) stops only the waiting: the run is reported as interrupted on the health card and in the journal, the scan goes on, and Scans in the console cancels it (see A run that ended without a result).

When something goes wrong

The health card (Overview) has a Scheduled scans line once a schedule has reported. It is bad when a schedule's last run failed or was interrupted, or its token was refused or has expired, and warns when a token expires within TOKEN_WARN_DAYS or a run is more than a day later than EVERY_DAYS. Its text names the schedule and what to do. A support bundle numbers the schedules instead of naming them, since a name can name a target. On the host:

systemctl list-units --failed 'shakerscan-enterprise-scan@*'
sudo journalctl -u shakerscan-enterprise-scan@weekly-app -n 50
The journal saysWhat to do
the service token was refusedRenew the token.
every licensed slot stayed busyOther scans held every slot for BUSY_MINUTES. Move the timer to a quieter hour, or raise BUSY_MINUTES.
the gateway refused the scan (HTTP 403)Usually the target is not authorized yet; authorize it in Targets. The rest of the line is the gateway's reason.
the submission's outcome is unknownThe gateway or engine did not answer, or was still processing the submission. See A run that ended without a result.
stopped by SIGTERMSomeone or something stopped the unit (systemctl stop, a shutdown). The rest of the line says whether a scan was started. If none was, start the run again. If one was or may have been, see A run that ended without a result.
scan ... is still running afterThe scan took longer than MAX_HOURS; it goes on. Raise MAX_HOURS, and see A run that ended without a result.
scan ... ended failedThe engine's own record of the scan (the link) says why.
health card not updatedThe run itself is fine, but the gateway did not take the report: the stack was down or DEPLOYMENT is wrong.

Nothing sends an e-mail yet. To be told without opening the console, point your monitoring at the unit's state (systemctl is-failed shakerscan-enterprise-scan@weekly-app.service) or at the journal.

A run that ended without a result

Some runs end without knowing how their scan ended: the unit was stopped while the scan ran or while it was being submitted, an answer was lost, the token was refused while the scan ran, or the scan ran longer than MAX_HOURS. The scan may have started and may still be running. Do not just start the unit again: a new run submits a new scan and takes a second licensed slot. The health card says so, and names the scan when the run knew it (Open on the card goes to it, and the card gives the full command below).

  1. Look under Scans in the console for this schedule's scan (its target, at the time of the run). If the token was refused, renew it first.

  2. If the scan is there, let the script wait for it instead of submitting another. Put the scan's id in place of SCAN_ID:

    sudo /opt/shakerscan/kit/self-hosted/scheduled-scan.sh --follow SCAN_ID weekly-app
    

    --follow submits nothing. It waits for that scan to end and records its result as the schedule's run, so the health card clears when the scan completed. An id the gateway does not know changes nothing on the card.

  3. If there is no such scan, nothing ran: start the run again with sudo systemctl start shakerscan-enterprise-scan@weekly-app.service.

Renew the token

Do this when the journal or the health card says the token expires soon, or after it was refused:

  1. In API access, create a new token for the same service account (Operator, up to 90 days).
  2. Replace the token file, through standard input as before: sudo sh -c 'umask 077; cat > /etc/shakerscan/scans/weekly-app.token'
  3. Check it: sudo /opt/shakerscan/kit/self-hosted/scheduled-scan.sh --check weekly-app. The health card's token warning clears at once; a failed last run stays on the card until the next run succeeds. Start one now with sudo systemctl start shakerscan-enterprise-scan@weekly-app.service, unless the card says a scan may still be running: then follow that scan as in A run that ended without a result.
  4. Revoke the old token in API access.

One token can serve several schedules; renew it once and replace each schedule's token file.

Remove a schedule

sudo systemctl disable --now shakerscan-enterprise-scan@weekly-app.timer
sudo rm /etc/shakerscan/scans/weekly-app.env /etc/shakerscan/scans/weekly-app.json \
  /etc/shakerscan/scans/weekly-app.token
sudo docker compose -f /srv/shakerscan-enterprise/compose.json exec -T enterprise \
  python -m shakerscan_saas.enterprise.cli scheduled-scan-forget weekly-app

The last command removes the schedule from the health card. Revoke its token if nothing else uses it.

Limits

  • The timer runs on the deployment host; that is where it can report to the health card. On another machine, set DEPLOYMENT= (empty) and rely on the journal and the unit's state.
  • One scan per schedule. Scheduled scans share the licensed slots with everyone's scans.
  • A restore revokes every service token: create a new one and replace each schedule's token file afterwards.