This is a guide to installing, configuring and maintaining Arcivéo Security Monitor. The sections are grouped: overview, deploying the dashboard, connecting security tools, built-in modules and diagnostics. Commands can be copied with the button on the right.
Panel installation is covered on separate step-by-step pages. Choose a method:
Arcivéo Security Monitor is a server-security dashboard. It collects data from installed tools (Fail2ban, UFW, Lynis, ModSecurity, AIDE, ClamAV, Auditd, CrowdSec, Suricata, Falco, and more) and shows it in a single interface with a dashboard, an attack map, and detailed pages for each tool.
Monitor is not an active protection tool — it does not block attacks on its own. Its job is to aggregate information from already-running tools and present it in a convenient form.
The monitor runs locally only — it must be installed on the same server it monitors. There is no SSH or remote API.
The dashboard runs all commands (fail2ban-client, ufw status, ipset list, etc.) as the web-server user (usually www-data, or the site account on hosting panels) with a narrow set of sudo permissions — limited to specific utilities, without general root access. The results are parsed and shown in the browser.
Two rhythms. The dashboard pages query the tools when you open them — you always see the current state. On top of that, background jobs run on a schedule (cron):
The full list of jobs is in "All cron jobs".
The score starts at maximum and drops for each issue found:
PermitRootLogin yes) — −20PasswordAuthentication yes) — −10X11Forwarding enabled, MaxAuthTries above 3, or sshd_config readable by every account — −5TRACE method is enabled, or the Strict-Transport-Security or X-Frame-Options header is missing — −5Result: 80+ = Protected, 60–79 = Attention, <60 = At risk.
WebAuthn is a passwordless authentication standard using a hardware key. It supports YubiKey, Touch ID, Face ID, Windows Hello, Passkey.
After you log in with a password, the system requests confirmation via a registered key. Even if the password leaks, no one can log in without the physical key or biometrics.
To set it up, open WebAuthn keys in the side menu and click “Register key”. Register two keys at once: if your only key is lost or breaks, you won't be able to log in to the dashboard with it.
There are four channels: Telegram, Email, webhook and browser push. They are independent — switch on any of them, or all four. The daily security report, the load alerts and the findings of the security-event sensor all travel over these same channels, configured once here, under "Settings".
Telegram. You need a bot token and a chat id:
@BotFather → /newbot → get a token like 123456:ABC....@userinfobot, or open https://api.telegram.org/bot<TOKEN>/getUpdates and locate "chat":{"id":...}.Email. Two options to choose from under "Settings" → Email:
re_...) and a verified sender domain.Webhook. One URL that accepts a POST covers Slack, Discord, Mattermost, n8n and your own handler. "Settings" → Webhook: paste the URL and switch the channel on.
{"text": …}, Discord receives {"content": …} (Discord rejects messages longer than 2000 characters, so the text is trimmed);text, event, severity, source, count, host, ts — there is no need to parse the message text again.Push notifications. The message arrives straight in the browser, and if the panel is added to a phone home screen, as an ordinary app notification — with no bot and no mail server in between. "Settings" → Push notifications: switch it on and press "Connect this device" on every device that should receive notifications. The "Connected devices" counter shows how many are subscribed.
Report status: "ATTENTION" or "OK". The header turns to "ATTENTION" only for a real problem or a pending action: a ClamAV threat found, file changes in AIDE, critical Falco events (Emergency/Alert/Critical in the last 24 h), a failed service in Monit, a required reboot, an expiring SSL certificate (≤14 days), or pending security updates. Background noise — SSH brute-force bots, IP addresses banned by fail2ban, Suricata alerts, Lynis warnings, and already-blocked ModSecurity requests — does not raise the status, so such numbers in the report do not mean "ATTENTION" on their own.
Detailed monitoring modules (Lynis, UFW, ModSecurity, attack map, AIDE, ClamAV, and others) unlock with a valid license. Without one, the dashboard, settings, and account still work, but the modules show a "License required" card.
After purchasing in your account, you have an activation code in the form ARCIVEO-XXXX-XXXX-XXXX-XXXX. It needs to be "activated" for your panel's domain — this turns the code into a signed license file (the [license] block) that you paste into the panel.
How to activate (3 steps):
my.arciveo.com → the "Licenses" / "License activation" section — copy the ARCIVEO-… code.monitor.example.com). Click activate — the system generates a license file bound to that domain and shows it in a field with a "Copy" button.The panel verifies the key cryptographically: the signature, the domain binding, and the expiry date.
APP_URL constant in config.php and enter only the hostname — without https:// and without the www prefix. Activation is one-time: the code turns into a license for the entered domain and cannot be activated again — if the domain is wrong, the key won't fit your panel and the code will be spent. So enter the domain carefully.
All the main dashboard parameters are defined in a single file config.php at the root (next to the public/ folder) as ordinary define() constants. The file is created during installation; you rarely need to edit it by hand — mainly when changing the domain, migrating, or connecting to a different database. After any edit, restart PHP-FPM (otherwise the changes won't take effect because of OPcache).
Substitute your own values in the highlighted spots; leave the rest as is:
Database. MySQL/MariaDB connection credentials:
DB_HOST — the DBMS host, almost always localhost;DB_NAME — the dashboard's database name;DB_USER — the DB user (access to its own database only);DB_PASS — that user's password;DB_CHARSET — the connection encoding, leave it as utf8mb4.Application.
APP_URL — the full dashboard address (e.g. https://monitor.example.com). It must match the domain the license is activated for — otherwise the key will be rejected (see the "License" section);TIMEZONE — the PHP timezone: it only affects how the dashboard shows dates and times. It does not affect when cron jobs run — those use the system timezone (see "All cron jobs").Session lifetime. SESSION_LIFETIME — the session idle timeout in seconds (sliding: refreshed on activity). The default 28800 = 8 hours; after that much inactivity the dashboard will ask you to log in again. For example, 3600 = 1 hour, 86400 = one day.
Error logging. Errors are never shown to visitors; they are written to logs/php_errors.log — you can view them on the "Application logs" page. These lines (display_errors=0, log_errors=1, the error_log path) usually don't need changing — the settings are defined right in the file and don't depend on php.ini.
public/), and this dashboard's web root (DocumentRoot) is exactly the dashboard root, not public/. The file doesn't "leak" on its own: the root .htaccess explicitly denies it (Require all denied) — the server returns 403. Even without that rule the source wouldn't leak: this is PHP — the server executes it rather than serving it as text. Just in case: don't post it to public repositories and don't send it to support with the real password. File permissions — 640.
The panel is behind a password login and, when it is switched on, an IP allow-list. Neither suits a program, so external systems get their own entrance — two read-only addresses protected by a token. The token changes nothing on the server: all it can do is fetch numbers.
Switching it on — "Settings" → "Read-only API": the "Enable the API" toggle; the token is created automatically on the first save. The "New token" button issues a different one and, at that moment, cuts off everyone using the previous one.
/api/status.php — JSON: the security score, module states, the newest performance sample with its top processes, and the security events of the last 24 hours;/api/metrics.php — the same numbers in the Prometheus text format (arciveo_* metrics: arciveo_security_score, arciveo_fail2ban_banned, arciveo_cpu_busy_pct and so on).If your client cannot add a header, the token is also accepted as a parameter — ?token=…. Keep in mind that in that form it ends up in the web server access log, so the header is preferable.
Prometheus. A ready block for scrape_configs:
api disabled — the API is switched off in the settings. Rejected requests are written to the application log together with the sender's IP, so token guessing is visible on the "Application logs" page.
The panel is installed per server and answers the question "how is this machine doing" well. It does not answer "how are all my machines doing" — for that, every panel can send a short digest to the customer cabinet, where the servers appear in a single list.
What to take from the cabinet my.arciveo.com → the "My servers" section: it issues a token (of the form fk_…) and states the collector address https://my.arciveo.com/api/fleet.php. The token is one per account — the same one for all of your servers.
What to set in the panel — "Settings" → "Fleet summary": the collector URL, the token, the server name (how it is labelled in the list; leave it empty and the panel domain is used) and the "Send the digest" toggle. The "Send now" button checks the connection immediately, without waiting for the schedule.
After that the digest goes out on cron, every 15 minutes:
What exactly is sent: the server name and domain, the OS, the uptime, the PHP version; the security score; module states (UFW, the number of Fail2ban jails, bans today, whether CrowdSec, Suricata and Falco are running, ClamAV threats, AIDE changes, the Lynis index, pending updates, whether a reboot is required, days until the certificate expires, the number of risky open ports); the newest performance sample (load, CPU, RAM, disk, inodes); the event counters of the last 24 hours and the latest critical finding. There are no logs, no passwords and no file contents in the digest.
sudo crontab -l), and what the "Send now" button answers. Delivery errors are written to the application log under the FLEET tag — that shows whether the token was rejected (HTTP 401) or the connection itself never went through.
UFW (Uncomplicated Firewall) is a simple front-end for nftables/iptables. It closes all incoming ports except those explicitly allowed. The "UFW firewall" page shows the status and rules.
ufw enable, be sure to allow SSH (ufw allow OpenSSH), otherwise you will lose access to the server.
deny rule is not counted as reachable from outside.
Skipping adding existing rule is not an error. This is how UFW reports that the exact same rule already exists and won't add it again. When you re-run auto-configuration (it's idempotent), this is a normal message — no action needed.
Automatically bans an IP after too many failed login attempts. Analyzes logs from SSH, nginx, Apache and other services.
The basic install is above. Here is a working configuration that yields dozens of active jails and thousands of bans: global settings, key jails and auto-banning of malicious IPs from the ipsum list.
The /etc/fail2ban/jail.local file — global settings and the most important jails:
ignoreip, otherwise you may ban yourself. After editing: sudo fail2ban-client reload.
Auto-loading of the ipsum block list — in the root cron (sudo crontab -e): level 1 (100,000+ IPs) is loaded into the ipsum set, which is dropped at the firewall (more in the "IPset block list" section):
ipsum — that is exactly what the dashboard reads (the "IPset ipsum" card). Levels: levels/1.txt — maximum coverage, levels/3.txt — more precise (3+ sources).
Why the "Security Monitor" is split into two zones. Protection works on two levels, and the dashboard does not mix them:
sshd, apache-*, nginx-* jails, etc.) and persistent recidivists (the recidive jail — those already banned several times). These are IPs that really hit you — they appear on the attack map and the "Timeline".ipset ipsum, dropped at the firewall by a DROP rule. Most of these addresses never even touched your server — they are cut off in advance; the "IPset ipsum" counter shows how many were blocked preventively.The difference is simple: reactive means "these attacked and got banned", preventive means "these were blocked before they even tried". Previously, ipsum list-3 was artificially forced into recidive (hence the old "list recidive" split); now recidive holds only real recidivists, and prevention is entirely on the firewall.
ipsum is a public list of malicious IPs, updated daily. The Monitor shows the number of loaded addresses on the dashboard and attack map, and factors it into the Security Score (−10 if the set is not loaded).
The minimal option without fail2ban is a separate ipsum set with blocking via iptables:
@reboot. The create … -exist command also sets the limit maxelem 300000 (the default 65536 won't fit level 1, you'll get "Hash is full"):
ipsum set and, if the firewall is managed by the installer (fresh VPS — "Full"/"Lite" profiles), attaches the set to UFW with a DROP rule — traffic from these IPs is actually blocked. The rule sits after ESTABLISHED,RELATED, so current connections (including your SSH) aren't dropped — only new connections from the list are cut. The set is restored when the ipsum-load.service service loads before the firewall (otherwise UFW wouldn't come up), and is refreshed by cron at 04:00. On an already-configured server (panel, own firewall) the installer doesn't touch the firewall — there ipsum stays a list for the dashboard and attack map, and the DROP rule can be added manually if desired (the minimal option with iptables … --match-set ipsum … -j DROP is above). With auto-install nothing needs to be done manually.
A modern replacement for Fail2ban with collective threat intelligence: community-sourced bans plus your own rules. Requires a separate bouncer to apply bans to the firewall.
systemctl is-active crowdsec). Start it: sudo systemctl enable --now crowdsec; if it crashes, check sudo journalctl -u crowdsec -n 30. The same applies to any service showing "Not running" (Suricata, Falco, Monit, MySQL).
stream halted / bans are not applied. This is an orphaned API key: the bouncer was removed from cscli bouncers list, but its old key remains in /etc/crowdsec/bouncers/*.yaml. Re-register the bouncer and set the new key:
AIDE (Advanced Intrusion Detection Environment) takes a snapshot of the file system and, on every check, reports changes in /etc, /bin, /usr. After installation, initializing the database (aideinit) is mandatory.
aideinit the terminal stays on the line Running aide --init... for 5–15 minutes — this is normal (hashing the entire filesystem, heavy disk load). Do not interrupt with Ctrl+C. If the process seems to "hang" but writes nothing — it may be waiting for a hidden prompt Overwrite existing aide.db.new [Yn]? (press Y). Check activity from another session: pgrep -af aide.
aideinit error: "21_aide_spamassassin … printf: invalid number" (return code 20) — a known bug in the AIDE config snippet on Ubuntu 22.04. The database is not created. Move the broken snippet aside and retry:
aideinit was never run, or (Ubuntu 24.04) the /var/lib/aide directory was created in mode 700 and is inaccessible to www-data — fixed with sudo chmod 755 /var/lib/aide (see the block above). "No checks yet" = the database exists but no check has run yet — this is not an error. The monitor reads results from /var/log/aide/aide.log.
/etc/cron.daily/aide on newer Ubuntu/Debian may not write /var/log/aide/aide.log in the required form (and aide.wrapper is no longer present there). It is more reliable to add your own cron job with an explicit --config — it writes the log as root in mode 644, and the monitor reads it without extra groups:
chmod 755 /var/lib/aide and a check cron job at 02:00 — nothing needs to be done manually.
sudo aideinit && sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db.
Antivirus scanner for Linux. Especially useful for scanning /var/www for PHP shells and malicious code.
enable --now? Three common causes:
1. The config still has an Example line — clamd refuses to start while it's there:
2. The signature database isn't downloaded — clamd won't start without it:
3. It's simply loading — clamd loads ~8M signatures into memory in 30–60 sec. Wait and check: systemctl is-active clamav-daemon (status activating → still loading).
sudo journalctl -u clamav-daemon -n 30 --no-pager.
clamd daemon only keeps signatures in memory; it doesn't scan anything on a schedule by itself. The dashboard shows scheduled-scan results, so you need a cron job that scans and writes a log. Auto-install adds the wrapper /usr/local/bin/clamav-scan.sh and a cron job at 01:30 — after the first run, "Files scanned" and "Last scan" will populate. Run it right away without waiting for the schedule: sudo /usr/local/bin/clamav-scan.sh.
Linux Malware Detect (LMD) — a malware scanner for web threats: PHP shells, web backdoors, droppers. It uses the ClamAV engine and supplements it with its own signatures.
maldet --report.
update-rc.d: error: unable to read /etc/init.d/maldet may appear — it is harmless. maldet does not use init.d; signature updates and scans run via /etc/cron.daily/maldet. If installation completed appears below, everything was installed.
apt but into /usr/local/maldetect, and when open_basedir is enabled its presence is checked through the shell — see the section "The page is empty even though there is data on the server".
A network intrusion detection system: it analyzes traffic at the packet level and knows thousands of attack signatures. It complements ModSecurity (which works at the HTTP level, while Suricata works at the TCP/IP level).
/var/log/suricata/eve.json as root with mode 750 on the directory, and the web server (www-data) can't read it. Open the directory for traversal — the files inside stay protected:
Intercepts system calls via eBPF/kernel module and detects anomalies in real time: a shell spawned from nginx, a web process reading /etc/passwd, writes to /bin, etc.
journalctl -u falco (without sudo — through the systemd-journal group). Make sure www-data is in this group — see "Configuring sudo" (step 2) on the manual installation page.
/etc/passwd, writes to system directories). Zero critical events in a day on a quiet server is a healthy state.
journalctl requires journal permissions; so the panel sees events reliably, auto-install enables Falco's file_output → /var/log/falco/falco.log and sets UMask=0022 on the service (the log is readable by the web server). On a fresh install there's no need to configure this manually.
ModSecurity is a web firewall (WAF) for Apache or Nginx. It blocks application-layer attacks: SQL injection, XSS, path traversal, scanners.
IncludeOptional /etc/modsecurity/*.conf, but the package only ships modsecurity.conf-recommended — which does not match the *.conf pattern. If you don't copy it to modsecurity.conf, SecRuleEngine stays Off: the module is loaded, the CRS rules are loaded, but traffic isn't inspected and no audit log is created. The intermediate DetectionOnly mode only logs events without blocking requests — the dashboard shows it in yellow.
Dashboard access to the audit log. The log /var/log/apache2/modsec_audit.log is owned by root (permissions 640), so the web user can't read it. The dashboard reads data through a wrapper — create it:
SecRuleEngine directive: indented lines are inside <LocationMatch>/<Directory> blocks (for example, disabling the WAF for phpMyAdmin) and don't define the global mode.www-data, while in HestiaCP the site pool runs as the site owner (for example, admin) — check with grep -E '^user' /etc/php/*/fpm/pool.d/monitor.example.com.conf.X-Forwarded-For header. Only transactions with a triggered rule appear in the statistics: the SecAuditLogRelevantStatus directive writes any 4xx/5xx responses to the audit log, so ordinary 403/500 responses end up there too — the dashboard does not count them as WAF events.---RULES--- block is needed by the "All active rules" section — the dashboard shows not only triggered rules but every loaded CRS rule plus custom ones. The three paths in the for f in … loop are the typical locations for CRS rules and local additions; if your layout differs (the package installs files in its own directory, or your custom rules aren't in /etc/modsecurity/custom-rules.conf), find the real paths with sudo grep -rl 'IncludeOptional\|^Include ' /etc/apache2/mods-enabled/security2.conf /etc/apache2/conf-enabled/*.conf 2>/dev/null and add them to the list. If the wrapper is old (without this section) the section simply shows an "unavailable" warning, and the rest of the page works as before.
Auditd (Linux Audit Daemon) records system calls at the kernel level: logins and logouts, sudo commands, failed authentication attempts, file changes. The Monitor shows today's logins, failed attempts and sudo commands.
ausearch (/usr/sbin/ausearch) and, when needed, from /var/log/audit/audit.log using the tail command. Both must be in sudoers.
Watches services (nginx, php-fpm, mysql, etc.) and restarts them if they go down. Can send alerts by email.
monit status. The HTTP interface must be enabled in /etc/monit/monitrc (the set httpd block with allow localhost), otherwise monit status returns an error.
set httpd line in monitrc is commented out (it ships as # set httpd port 2812 … by default). Uncomment the block and allow localhost. (2) An enabled httpd by itself monitors nothing — Monit counts only what is described by check stanzas; without them the list is empty even with a working interface. A minimal working config:
conf.d with httpd on 2812 and a set of checks — on a fresh install there is nothing to configure by hand.
PSAD analyzes the iptables log and detects port scans and network attacks, assigning each source a threat level (1–5). It complements fail2ban and Suricata.
psad --Status (required in sudoers). Without iptables logging the page will be empty — this is normal until a scan occurs.
Mandatory Access Control restricts which files and resources a program can access, even if it has been compromised. Ubuntu/Debian use AppArmor by default (usually already installed and active).
aa-status (needs to be in sudoers). It shows the number of profiles in enforce/complain mode and processes without a profile.
"Profiles loaded" being higher than enforce + complain is normal. AppArmor 4.x (Ubuntu 24.04 and later) introduced the unconfined mode: the profile is loaded into the kernel but restricts nothing. Ubuntu flags dozens of profiles this way for programs that use user namespaces (browsers, torrent clients and the like). When such profiles exist, the "Profiles loaded" card turns amber and shows their count — for example unconfined: 90 with 120 loaded and 26 in enforce. Only profiles in enforce actually provide protection; on Ubuntu 22.04 (AppArmor 3.x) this mode does not exist and the numbers always add up.
unconfined should only be done knowingly: they are disabled not by mistake, but because otherwise the programs themselves break. Profiles in complain are a different matter: the rules are already written there and simply not enforced.
debsums verifies that installed package files match the checksums from the repository — it detects tampered system binaries (complementing AIDE). A full scan takes 1–2 minutes, so it runs via cron, and the dashboard reads the result from data/debsums/debsums.log and sorts it into categories itself (only binaries and libraries matter).
The job goes in the root crontab (sudo crontab -e). The ready-made wrapper debsums-scan.sh is placed in /usr/local/bin/ (chmod +x; see the cron job summary) and writes its report to the dashboard's data/debsums/ itself.
The debsums-scan.sh wrapper finds the dashboard's data/ itself — no need to specify the path.
/etc/ (configs) and /usr/share/ (resources) on the server are usually normal — the dashboard marks them in a separate color. Changes to binaries and libraries are the alarming ones (/bin, /sbin, /usr/lib, etc.) — the "Binaries / libraries" card shows exactly those.
Lynis runs manually or via cron. The report must be saved to the project's data/lynis/ folder — the monitor reads the lynis-report.dat file.
lynis-scan.sh in the background right from the dashboard (without waiting for cron): it shows "Scanning…" and refreshes the report itself when done. For this the web user needs a sudoers line to run the script — the installer adds it automatically to /etc/sudoers.d/monitor. If the dashboard was installed manually/earlier, add it with the same user already listed in the file:
Logwatch must save daily reports to the project's data/logwatch/ folder in .txt format. The Monitor shows the latest report and the archive.
The network monitor requires no installation — it is a built-in dashboard page. It shows the server's network status from local sources:
/proc/net/dev;ip;ss;journalctl -k.The first three sources work without sudo, so interfaces, traffic, connections and ports are visible right away. The "Kernel events" block uses journalctl -k — it is read via the systemd-journal group ("Configuring sudo", step 2), no sudo needed. Check that everything is accessible to the web user:
UFW BLOCK entries do not appear here — they are on the "UFW Firewall" and "Attack map" pages. An empty block with a green check = no network failures in the last 24 hours.
The built-in page shows three things:
df); the bar turns red at ≥90%;lsblk), real ones only (loop/snap hidden);smartctl).Space and the device list work right away, no setup needed. SMART requires the smartmontools package. The web process has no direct access to disk devices, so SMART is read via cron into the file data/disk/smart.txt, and the dashboard reads it from there.
The job goes in the root crontab (sudo crontab -e). A ready-made wrapper smart-scan.sh is placed in /usr/local/bin/ (chmod +x; see the cron job summary) and writes to the dashboard's data/disk/ itself.
The smart-scan.sh wrapper finds the dashboard's data/ on its own — no need to specify a path. Inside, lsblk -e7,11 excludes loop/cdrom.
The page shows the server load history for the last 24 hours — Load Average, CPU usage and I/O wait, RAM/Swap, network traffic (receive/transmit), disk I/O (read/write), disk and inode usage, open file descriptors and MySQL connections, plus the current number of TCP connections and processes. The 24-hour window is the default: the buttons at the top switch it to 7 days or 30 days.
The data is collected by cron/collect_metrics.php — every 5 minutes it writes one "raw" snapshot of counters (/proc/loadavg, /proc/meminfo, /proc/stat, /proc/net/dev, /proc/diskstats, df/df -i, /proc/sys/fs/file-nr, SHOW GLOBAL STATUS LIKE 'Threads_connected') into the system_metrics database table; the page computes percentages and rates itself from the difference between adjacent snapshots (disk/inode/descriptor usage and MySQL connections are instantaneous values, with no recalculation). Sudo is not required — the sources are read without root privileges.
Two storage depths. The raw five-minute snapshots live for a day (plus an hour of margin, so the oldest point on the chart still has a predecessor to derive rates from). Every finished hour is folded into a single row; that hourly series is kept for 30 days and is exactly what the "7 days" and "30 days" views read. A month of history then costs about 720 rows instead of roughly 8600.
The collect-metrics-all.sh wrapper (see the cron jobs summary) finds all installed panel instances on the server itself and runs each one's cron/collect_metrics.php as the site owner.
Top processes. Below the charts are two tables of the five heaviest processes by CPU:
Both tables always describe the last 24 hours, even when the charts show a month: only the five-minute rows carry a process snapshot, and those are not kept longer than a day.
ps, the collector PHP script and the cron/sudo chain above it) are excluded from the list — otherwise they would head every snapshot: ps reports almost 100 % CPU at the moment it starts. Other cron jobs that ran in the same minute do appear, and those are exactly what the table is for.
Load alerts ("Settings" → "Load alerts") — when the CPU/RAM/disk/inodes threshold is exceeded, the panel sends a notification over every channel that is switched on (Telegram, Email, webhook, push — the same ones the daily report uses, so nothing extra has to be enabled for alerts), and another one once the metric is back to normal. It does not keep spamming while the threshold is held: the next notification only comes after a "recovered → exceeded again" cycle.
collect_metrics.php on every run (every 5 minutes) — no separate cron is needed. The "already notified / not yet" state is stored in data/alerts_state.json, and the thresholds are in the panel settings.
The daily report answers the question "how is the server doing". The sensor answers a different one — "what just happened". Every 5 minutes it polls the fast sources and records only what has changed since the previous check; the "Security events" page in the menu shows the feed of findings.
Slow checks — Lynis, debsums, package updates, certificate expiry — are left alone: their numbers change once a day anyway, and they stay with the daily report.
What it notices — nine sources:
[Priority: 1] mark raises the finding to critical;FOUND lines in the scan log;root (critical) and a login from an address never seen before (warning);A tool that is not installed, or whose log the panel cannot read, simply stays silent — that is neither an error nor an event.
data/secwatch_state.json), and the first run only writes that mark. Otherwise switching the watch on would deliver a notification about everything the logs had collected over months.
Configuration — "Settings" → "Security watch":
23-7 format during which warnings are not sent.Notifications travel over the same channels as the daily report (Telegram, Email, webhook, push) — nothing has to be enabled separately. Every finding is written to the feed whether or not a notification was sent, and is kept for 90 days.
The feed. Filters: period (24 hours, 7, 30 or 90 days), severity and source; the latest 500 records are shown. The "Check now" button runs exactly the same pass cron runs — handy right after setup, so you do not have to wait five minutes; it also states the reason when nothing was sent ("below the severity threshold", "quiet hours", "no notification channel configured"). The red counter next to the menu item is the number of critical findings in the last 24 hours.
The address card. Clicking a row in the feed, or a row in the Correlated addresses block, opens everything known about one address: the timeline of its events, its country and, on the first line, a verdict. A feed row opens the card when the event is about exactly one address; where it names several, the addresses inside the row are the links. There are four verdicts:
Below the timeline sits the What happened on the server at the same time block: events that have no address at all. AIDE does not know who changed the file, ClamAV does not know who brought the infected one. Shown are those that fall within the address's activity and the six hours after it. It is the sign-in paired with what followed that separates an attempt from actual access.
Schedule. The job goes into the root crontab (sudo crontab -e), offset by 2 minutes from the metrics collector so the two do not start at once:
php cron/security_watch.php directly. The wrapper finds every panel installed on the server and runs the check as the site owner — with the same sudo rights the panel uses from the web. A direct call from the root crontab would create the state file data/secwatch_state.json owned by root, and the "Check now" button in the panel would stop working.
The "Web server" page parses the access log and shows how many requests there were, how many of them failed and who made them. ModSecurity answers the question "what was blocked"; this page answers "what reached the site at all".
What is on it: the request count, RPS, the error rate, a separate 5xx counter and the volume served; a chart of requests per hour; the 2xx/3xx/4xx/5xx breakdown; the most frequent paths, the most frequent IP addresses, the most frequent errors (code plus path) and the clients (User-Agent). The period is 24 hours or 7 days.
The source is the access log in the combined format, so both Nginx and Apache fit and no extra module is needed. The panel looks for the log among the standard files:
/var/log/nginx/access.log/var/log/apache2/access.log/var/log/apache2/other_vhosts_access.log/var/log/httpd/access_logBesides them, the panel checks files named *access*.log and *requests*.log in /var/log/nginx/, /var/log/apache2/ and /var/log/httpd/. This also finds a log that a site writes to its own file with the CustomLog directive — for example arciveo-monitor_requests.log, which the Arcivéo installer creates.
Only one log is read: when Nginx sits in front of Apache, adding two logs together would count every request twice. The panel takes the first log from the list above that was written to within the last day (the ones found by name pattern are checked after it, newest first); if none was written to for a day, it takes the most recent non-empty one. So a log emptied by logrotate or left behind by a stopped web server does not hide the live one. The last chunk of the log is read (3 MB) and the result is cached for 5 minutes — so on a busy site the selected period may not be covered in full; the coverage actually reached is printed above the chart.
adm group, and the web user is not in it. The Arcivéo installer adds the web user to that group; after a manual installation, a tiny root-owned wrapper allowed to do exactly one thing — print the log — solves it (the ModSecurity page works the same way). The wrapper picks the log by the same rules as the panel. The command is shown on the page itself; here it is as well. Paste it as a whole — it is a single command:
The web user may differ from www-data — check which user the site's PHP-FPM pool runs as (ps -o user= -C php-fpm) and put that name into the sudoers line. If the log's name contains neither access nor requests, add its path at the start of the L=… line inside the wrapper.
Known exploit attempts. The panel matches log requests against signatures of some twenty common exploits: PHP-CGI, Apache path traversal, PHPUnit, Log4Shell, and flaws in routers, cameras and VPN gateways. For each attempt it answers whether it can work on this server, based on facts: the installed Apache and PHP versions, whether Java is present, and the response code the server gave to the file probe. Only what can actually work is marked red.
Web server hardening. The panel requests every site on the server via 127.0.0.1 and checks what a visitor receives: whether the web server and PHP versions are visible, whether the TRACE method is enabled, and whether the protective headers Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and Content-Security-Policy are present. Sites come from the address the panel is opened on and from the virtual host files, if the panel can read them. The result refreshes every 10 minutes.
If something is missing, one command appears under the table. It creates a separate file, zz-arciveo-hardening.conf, and reloads the web server only after the config test passes; headers a site already sends are not duplicated. CSP is not offered as one line: the policy is built for a specific site, otherwise the site stops loading its own scripts, styles and fonts. To undo the change on Apache:
ServerSignature) is not flagged when the web server version is already hidden: without the version it shows only the server name and the host name.
The "Attack map" page determines the country by IP address using the geoiplookup command. Without the GeoIP package, countries won't be resolved and no points will appear on the map:
/usr/share/GeoIP/GeoIP.dat database is readable by everyone, and results are cached in tmp/geoip_cache.json. The map itself (Leaflet + OpenStreetMap tiles) loads in the browser — you need internet on the computer where the dashboard is open.
Two built-in dashboard cards that show not a tool's "on/off" state but the server's real security posture. They need no setup and are read locally without sudo.
External exposure — how many services listen on all interfaces (0.0.0.0/[::]) and are reachable from outside. It flags red if a database or cache is exposed (MySQL, PostgreSQL, Redis, MongoDB, Memcached, Elasticsearch) — a direct hole (−10 to the Security Score). Source: ss -tuln.
127.0.0.1 (bind-address in the MySQL/PostgreSQL config, bind 127.0.0.1 in Redis) or close the port in UFW.
127.0.0.1 (loopback) is visible only to the server itself — it cannot be reached from outside, even if the port is "open". That's why Postfix on port 25 bound to loopback is safe: auto-setup sets inet_interfaces = loopback-only (plus a neutral smtpd_banner — clearing the Lynis MAIL-8818 warning about version disclosure). The "External exposure" card counts as exposed only what listens on 0.0.0.0/[::]; loopback services don't count.
/etc/postfix/main.cf set smtpd_banner = $myhostname ESMTP (no version or OS) and inet_interfaces = loopback-only, then sudo systemctl restart postfix.
Security updates — how many security patches are pending and whether a reboot is required after a kernel update (−5 to the Security Score if patches exist). Source: /usr/lib/update-notifier/apt-check, file /var/run/reboot-required. Detailed list — on the "Security updates" page.
update-notifier-common). If apt-check is missing, the monitor counts patches via apt-get -s upgrade.
Automatic security updates (unattended-upgrades) — on the "Security updates" page a separate card shows whether automatic installation of security patches is enabled and when it last ran. No sudo needed — the status is read via apt-config dump.
A backup is your main insurance: losing data is worse than any breach. You need two things — a server/site backup and a separate backup of the panel database (it holds users, WebAuthn keys, settings, and the license).
Option A — HestiaCP: the Backup tab under the user → the create-backup button (or on a schedule in the server settings). The backup includes sites and their databases.
Option B — manually (cron): a database dump + an archive of the panel's data/ directory:
Updating to a new version. First make a backup. Then re-upload the code files, keeping your data:
public/, includes/, assets/, cron/, database/, plus the root .htaccess (front controller — routing must not be left over from the old version), manifest.json, sw.js;config.php (database data), data/ (reports), logs/, tmp/ (sessions and cache).SSH_FX_PERMISSION_DENIED — Permission denied. The panel files belong to www-data (that is how they were set during installation), while the SFTP client connects as your own user, which has no write access. Handing the whole panel to www-data “to make it work” is exactly what leads to this error; below are three ways, any of which solves the problem.
data/ (reports), tmp/ (sessions and cache), logs/; they stay owned by www-data. Everything else is code, and the web server only needs read access to it, which the www-data group provides with 644 permissions. A side benefit: with a PHP vulnerability the panel files can no longer be rewritten. On hosting panels (HestiaCP and similar) option A is not needed: there the site files already belong to the account you log in with over SFTP, and the web server reads them through the group.
chmod on the files resets the ACL mask, and access silently disappears. If after “tidying up the permissions” the upload again hits Permission denied — rerun both setfacl commands.
2 bit in option C is setgid: files uploaded over SFTP stay in the www-data group, otherwise the panel cannot overwrite them. After option C reconnect in FileZilla — the new group only takes effect on a new login. Check: id deploy (the www-data group must appear) and ls -ld /path/to/monitor (drwxrwsr-x — the letter s means setgid is set).
Migrating to another server:
config.php, data/.mysqldump on the old one → import on the new one; update the database data in config.php.adm group, cron jobs.The dashboard is already running and you need one more site on the same server — its own domain next to the monitor. After the automatic setup the server is a plain Apache + PHP-FPM machine, so the site needs three things: its own directory, its own vhost and its own certificate. The dashboard itself stays untouched: it watches the whole server, so a second copy for the second domain is not needed.
site2.example.com is the address of the new site. It can be either a separate domain (mycompany.com) or a subdomain (shop.mycompany.com) — Apache does not care, only the DNS record (step 1) and the certificate (step 5) differ. site2 is just a short name for the site: it names the directory /var/www/site2 and the configuration file site2.conf. It has nothing to do with the domain — Apache takes the address from ServerName only; the name can be anything, e.g. for shop.mycompany.com it is convenient to use shop: directory /var/www/shop, config shop.conf.
1. DNS. In the DNS panel at your domain registrar create an A record pointing to the IP of this server:
After a few minutes check it: dig +short site2.example.com must return the IP of the server.
2. Site directory.
3. The vhost. Paste the block into the terminal as a whole — the path to the PHP-FPM socket is detected automatically:
4. Enable the site and check the configuration before reloading:
5. SSL. certbot is already installed by the automatic setup; by now DNS must point to the server:
A third domain and further ones are added the same way — repeat steps 1–5, changing the domain and the directory.
Host matches no ServerName (a visit by IP, or somebody else's domain pointed at your server), Apache serves the alphabetically first enabled site from /etc/apache2/sites-enabled/. The stock 000-default is disabled by the automatic setup, so that “on duty” role stays with the dashboard vhost (arciveo-monitor.conf) — this does not affect sites reached by their own names. If you want another site to open by IP, name its file so that it comes first alphabetically (for example 000-site2.conf).
site2_error.log and site2_access.log under /var/log/apache2/.
.htaccess (a front controller), and a nested site would serve something other than you expect. Keep the sites side by side: /var/www/monitor, /var/www/site2.
If you can't log in, everything can be fixed directly in the database from the server. Open the database (its name is in config.php):
Lost WebAuthn key (second factor fails) — disable 2FA, log in with your password, and register a new key:
Forgot your password — set a new hash (generate it on the server and paste it in):
Locked yourself out with the IP filter — disable the restriction:
sudo mysql on the server, or phpMyAdmin / the database section in your hosting panel. After recovery, re-enable WebAuthn and the IP filter.
A summary of jobs lives in the server's root crontab (added via sudo crontab -e). Keep only the lines for the tools you use; adjust the paths to your server.
sudo crontab -l and that the cron service is active.
TIMEZONE from config.php. The TIMEZONE constant only affects PHP (how the dashboard shows dates), but the cron daemon runs jobs by the OS system time. If the server's timezone doesn't match yours, the "08:00" report will arrive at the wrong time. Example: the server is in another timezone (UTC, UTC+0) while you're in London (UTC+1) → the "08:00" report arrives at 09:00 your time. Check and, if needed, set the system timezone to your own:
0 8 * * * will fire at 08:00 local time. Otherwise you'd have to shift cron itself, but at each daylight-saving change the offset would drift again — so setting the system timezone is the right approach.
crontab.txt) sit in the system/ folder next to the project, outside public_html. This is not part of the site — don't upload it to the web root; place it on the server at the system paths (as in the crontab above):
lynis-scan.sh → /usr/local/bin/ (chmod +x) — runs lynis audit system, sets the flag /tmp/lynis-running during the scan, and copies lynis-report.dat into the dashboard's data/lynis/;logwatch_daily.sh → /usr/local/bin/ (chmod +x) — builds the daily Logwatch report (sshd, fail2ban, sudo, postfix) into data/logwatch/;smart-scan.sh → /usr/local/bin/ (chmod +x) — captures disk status (smartctl) into data/disk/;debsums-scan.sh → /usr/local/bin/ (chmod +x) — checks package integrity (debsums) into data/debsums/;clamav-scan.sh → /usr/local/bin/ (chmod +x) — ClamAV antivirus scan over risky paths (web, home, temp); writes a summary to /var/log/clamav/scan.log, which the ClamAV page reads (the 01:30 line in the crontab above);load-ipsum.sh → /usr/local/bin/ (chmod +x) — updates the ipset set ipsum (level 1) in place, without breaking the active firewall rules (the 04:00 line in the crontab above);daily-report-all.sh → /usr/local/bin/ (chmod +x) — runs the dashboard's report cron/daily_report.php (the 08:00 line in the crontab above);daily_report.php — already included in the dashboard (cron/daily_report.php), run via daily-report-all.sh, no separate install needed;collect-metrics-all.sh → /usr/local/bin/ (chmod +x) — runs the dashboard's cron/collect_metrics.php (the "Performance" page, the */5 line in the crontab above); collect_metrics.php is already included in the dashboard, no separate install needed;security-watch-all.sh → /usr/local/bin/ (chmod +x) — runs the panel's cron/security_watch.php as the site owner (the "Security events" page, the 2-57/5 line in the crontab above); security_watch.php already ships with the panel and needs no separate install;fleet-push-all.sh → /usr/local/bin/ (chmod +x) — sends the server digest to the customer cabinet through the panel's cron/fleet_push.php (the 3,18,33,48 line above);crontab.txt (system/cron/) — a sample of jobs; add the lines you need via sudo crontab -e./usr/local/bin/. You can't write there directly from FileZilla — the directory belongs to root, and the SFTP client will get SSH_FX_PERMISSION_DENIED. The order is: first upload the file to /tmp (everyone can write there), then move it into place with a single command:
/tmp at the server root — not /var/tmp and not tmp/ inside the dashboard itself (the latter belongs to www-data and is closed off from your user). In the FileZilla tree, /tmp is a top-level branch, next to var, not inside it.
/usr/local/bin/, log — /var/log/arciveo-cron.log) — nothing to do by hand.
/home/*/web/*/public_html and /var/www/*, and place reports in their data/. If the dashboard is at a different path, add it to the for app in … line inside the scripts, otherwise the Lynis/SMART/debsums/Logwatch reports won't reach the dashboard.
logs/cron.log is first created by the root crontab — it will belong to root, and the "Cron log" tab in the dashboard won't be able to read or clear it. Create the file in advance as the web user (owner of the site directory; on HestiaCP that's an account such as admin) — then the root cron will only append, without changing the owner:
stat -c %U /path/to/monitor.
sudo crontab -e), otherwise it will run twice.
sudo crontab (that would be a direct escalation to root by anyone who gains access to the dashboard session), but a narrow script with two commands (list/set) that touches only its own block between the service comments. Install it once:
www-data — check which user the site's PHP-FPM pool runs as (ps -o user= -C php-fpm) and substitute it in the sudoers line.
public/crontab_monitor.php was uploaded via FTP/SFTP under a different system user (for example, root) than the rest of the site files, the web server won't be able to read it. Compare the owner and permissions with a neighboring file and bring them in line:
Monitor detects tools through dpkg-query — the APT package database. If a tool was not installed through apt (manually, from snap, or from source), dpkg does not see it.
Error 500 — check the PHP, nginx and Monitor logs:
www-data but under a user account (e.g. admin — the owner of the site directory). All sudo rules and group memberships (adm, systemd-journal) must be assigned to that user, otherwise the modules will show "Inactive / 0" while the services are running. To find the real PHP user: ps -o user= -C php-fpm | sort -u or the owner of the site directory stat -c '%U' /path/to/monitor. In all commands below, substitute it for www-data. Auto-install detects the web user itself and writes sudoers for it.
No data shown — almost always missing sudo permissions. Check the specific command as the web user (replace www-data with your own). The -n flag = no password, just like PHP — if it asks for a password, the rule is missing from sudoers:
sudo aa-status in the terminal lists profiles, but the "AppArmor" page says "Inactive"). Cause: the web user has no sudo permission for that module's command. Check it against the list above: if it asks for a password, add the missing line to /etc/sudoers.d/monitor ("Configuring sudo"). Common "new" commands: /usr/sbin/aa-status (MAC), /usr/sbin/psad --Status (PSAD).
apache2ctl, ausearch, aa-status or ss is not allowed, or the web user is not in the adm/systemd-journal groups (the fail2ban/auth/modsec logs and journalctl — Falco and kernel events — are read from there).
Symptom: the data is present on the server (visible via shell), but the page shows "no data" or the wrong status — for example, AIDE reports "Not initialized" even though the database has been created.
The cause is open_basedir: many panels and hosts restrict the PHP-FPM pool to the domain's directory, so the PHP functions file_exists(), file_get_contents(), filemtime() are blocked for system paths (/var/lib/aide, /var/log, /proc…). The Monitor works around this by reading such paths with standard system commands (cat, test, stat).
open_basedir. The correct fix is reading via system commands (already done for AIDE and the Network Monitor). Extending open_basedir to /var, /proc is unnecessary and less secure.
The Monitor checks certificates by connecting to domains directly over port 443. If a domain is unreachable from the server itself or the port is blocked by a firewall, the check will fail.
/etc/nginx/sites-enabled/, /etc/nginx/conf.d/) and Apache (/etc/apache2/sites-enabled/), plus the current host from HTTP_HOST.
The Monitor connects to MySQL as the user from config.php, which only has access to its own database. MySQL lists only databases you have privileges on in information_schema — that's why the others aren't visible.
To let the Monitor see all databases, grant this user read-only access (once, as root; substitute the username from config.php):
sudo mysql at all: the database list comes through its own PDO connection.
PostgreSQL requires postgres user-level access, which the panel's web user does not have. Exposing a broad sudo psql from PHP is unsafe — instead, the panel calls a narrow parameterless wrapper that only prints the version, the number of connections and the list of databases with their sizes. Create it:
monitor-pgstat line from sudoers (step 13 of manual installation) and do not create the script itself: the PostgreSQL card will simply stay inactive.
The dashboard shows what is happening; below is what to do in common situations. General rule: don't panic, cross-check against legitimate activity (your actions, updates, backups), and respond according to severity.
ignoreip./etc, outside /usr/share) — potentially tampered. Verify the package: debsums PACKAGE_NAME, and if in doubt reinstall it (apt install --reinstall).127.0.0.1 or close the port in UFW. This is a real hole.certbot renew or the settings in the panel).sudo apt update && sudo apt upgrade; after a kernel update, reboot the server.