Day-2 operations toolkit for Plesk servers — the bits the panel doesn't give you.
Plesk is great for provisioning. Day-2 operations — the part where you keep the box healthy, certificates fresh, mail reputation intact, and surprises out of the on-call rotation — is mostly left as an exercise for the operator.
plesk-toolbox is the toolkit a Plesk-running sysadmin builds for themselves
over the years, packaged up: read-only audits, ad-hoc tools, and a handful of
opt-in system mods. Built to be safe to run on production (audits are
read-only, tools support --dry-run, mods are fully reversible), easy to
extend (drop a file in a directory, no installer edit), and honest about
what it changed (everything writes a JSONL audit log).
- 🛡️ Security audit — SSH hardening, fail2ban, panel 2FA, exposed ports, TLS expiry on every domain, the 9 OWASP HTTP response headers (skipping vhosts that still serve Plesk's "Domain Default page"), MTA-STS, mail-client autoconfig + autodiscover, webmail reachability …
- 💚 Health audit — memory/disk pressure, critical services
(auto-aliases
apache2↔httpd/mariadb↔mysql), Plesk license expiry, consolidated mail hygiene (SPF + DKIM + DMARC + mailbox sieve sanity) per domain in a single coloured table — with a details: block under every table listing exactly which mailbox / setting tripped each ⚠/✗ - 🔧 Operator tools — flush the mail queue, tail one domain's maillog,
reissue a Let's Encrypt cert with
www, hunt top spam senders, dump slow MySQL queries, rotate weak/stale DKIM keys with zero signing-gap — all logged, all reversible where it matters - 🎨 MOTD mod — replace the noisy default SSH login banner with a clean branded dashboard (services, mem/disk bars, recent logins, pending updates)
- 📋 Structured output — human-readable tables on a TTY,
--jsonfor Prometheus exporters, log shippers, or your favourite IRC bot - ❓
--helpon every layer —plesk-toolbox --help,… audit --help,… tool --help,… tool mail/dkim-rotate --help— drill down as far as you need - 🪶 Zero runtime deps — stock
bash,coreutils,openssl,systemctl. No Python, no Ruby, no agents.
== health: mail hygiene ==
Domain SPF DKIM DMARC Mailboxes
────────────── ────── ──────── ──────── ─────────
example.com ✓ -all ✓ 2048b ✓ reject ✓ 8
example.org ✓ -all ✓ 2048b ⚠ p=none ✓ 23
example.net ⚠ ~all ✗ stale ✓ quar ⚠ 5
acme.test ✗ miss ✓ 1024b ✗ miss · 0
details:
⚠ example.org: spf=pass dkim=pass dmarc=warn mbx=pass/23
↳ DMARC policy is p=none — collect rua= reports for a few weeks,
then move to p=quarantine
✗ example.net: spf=pass dkim=fail dmarc=warn mbx=warn/5
↳ DNS pubkey doesn't match local key — republish
default._domainkey.example.net TXT |
sieve fileinto "INBOX" bug in 1 mailbox(es): marketing
— change to "INBOX.Spam"
== security: web response headers ==
Domain HSTS XFO XCTO CSP RP PP COOP COEP CORP
────────────── ───── ────── ───── ────── ────── ────── ────── ────── ───────
example.com ✓ 1y ✓ SAME ✓ set ✓ set ✓ set ✓ set ✓ set ✗ miss ✓ set
example.org ⚠ 6mo ✓ DENY ✓ set ✗ miss ⚠ unsa ✗ miss ✗ miss ✗ miss ⚠ cross
legacy.example · default
example.net ✓ 1y ✓ SAME ✓ set ✓ set ✓ set ✓ set ✗ miss ✗ miss ✓ set
== security: mail webmail ==
Domain DNS TLS Webmail
────────────── ────── ─────── ─────────
example.com ✓ A ✓ valid ✓ roundcube
example.org ✓ A ⚠ self ⚠ default
example.net ⚠ miss · n/a · n/a
summary: 47 pass 9 warn 3 fail 2 skip
Exit code is non-zero when anything fails — pipe it into cron with confidence.
git clone https://github.com/dmnk-rdr/plesk-toolbox
cd plesk-toolbox
sudo ./install.sh
sudo plesk-toolbox audit # full audit, takes 5-30s depending on domain countThe installer is idempotent — run it again to update; nothing is overwritten that you customised.
- Copies sources to
/opt/plesk-toolbox/ - Adds shims in
/usr/local/bin/:plesk-toolbox,plesk-audit,plesk-sec-audit,plesk-tool,plesk-mod - Creates
/var/log/plesk-toolbox/and/var/lib/plesk-toolbox/ - Writes
/etc/plesk-toolbox.conffrom the example if missing (your edits survive future installs) - Enables the
motdmod by default (disable withsudo plesk-mod disable motdif you want to keep the default Debian/Ubuntu MOTD)
Update:
cd plesk-toolbox && git pull && sudo ./install.shUninstall (restores any files the mods replaced):
sudo ./install.sh --uninstallA few realistic scenarios this toolkit was built for:
The 5-second answer before your coffee gets cold.
sudo plesk-toolbox audit # full coloured report, exit 1 on failuresWire it into cron with --json and pipe to your alerting:
0 7 * * * /usr/local/bin/plesk-toolbox audit --json | jq '.results[] | select(.status=="fail")'sudo plesk-toolbox audit sec/mail # autoconfig + autodiscover + webmail
# The details: block under the table tells you exactly which subdomain or
# record is missing per affected domain.sudo plesk-tool mail/queue-flush --older-than=4h --dry-run # show what would go
sudo plesk-tool mail/queue-flush --older-than=4h # actually flushsudo plesk-tool domain/show example.com # who owns it, where it lives
sudo plesk-tool domain/reissue-cert example.com --with-www # reissue from Let's EncryptThe single most common Plesk mail bug: the panel rotated the key locally but the new public key never made it to DNS — especially when the zone lives on an external provider Plesk can't update directly (AutoDNS, Cloudflare, registrar's web UI).
sudo plesk-toolbox audit health/mail
# Each warning row now expands in the "details:" block below the table:
# ✗ example.com — DNS pubkey doesn't match local key — republish
# default._domainkey.example.com TXTGet the exact record to publish, byte-for-byte:
sudo plesk-tool mail/dkim-show example.com
# Prints a copy-paste-ready block:
#
# Publish this DNS record:
# name default._domainkey.example.com
# type TXT
# value (copy the line below, single string):
# ────────────────────────────────────────────────────────────
# v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG…IDAQAB
# ────────────────────────────────────────────────────────────Or, for a bulk migration sweep:
sudo plesk-tool mail/dkim-show-all # only the problem domains
sudo plesk-tool mail/dkim-show-all --all # also the passing onesIf the local key is weak (≤1024-bit), don't downgrade DNS — rotate to a fresh 2048-bit key with zero signing-gap:
sudo plesk-tool mail/dkim-rotate example.com
# → generates default.new alongside the live key
# → prints the new TXT record to publish
# → live key keeps signing while DNS propagatesPublish the printed TXT in your DNS provider, then once propagated:
sudo plesk-tool mail/dkim-verify example.com # confirm DNS = staged key
sudo plesk-tool mail/dkim-rotate example.com --activate
# → re-verifies DNS one last time, takes a timestamped backup of the
# old key, atomic-swaps, restarts pc-remote + postfix
# → refuses to swap if DNS still doesn't match (safety check)End-to-end check (sends a signed mail through the port25 verifier):
sudo plesk-tool mail/dkim-verify example.com --verifier --from postmaster@example.comAfter installing the motd mod, every SSH login starts with:
Acme Hosting · Managed Server
───────────────────────────────────────────
OS: Debian 12 (bookworm) Plesk: Obsidian 18.0.62
Uptime: 37 days Load: 0.42, 0.55, 0.51
CPU ████░░░░░░░░░░░░░░░░░░░░░░░░░░ 14%
Memory █████████░░░░░░░░░░░░░░░░░░░░░ 30% (4.8G / 16G)
Disk / ███████░░░░░░░░░░░░░░░░░░░░░░░ 24% (47G / 196G)
✓ nginx · psa · postfix · dovecot ✗ — none down
🛡 fail2ban: 4 jails active, 12 bans in last 24h
📦 5 pending security updates (apt)
Every tool invocation appends a JSONL line to /var/log/plesk-toolbox/tool.log:
{"ts":"2026-05-25T08:14:02Z","user":"alice","tty":"pts/0","id":"mail/queue-flush","args":["--older-than=4h"],"result":"ok","duration_s":1.2,"dry_run":false}Ship it with vector / journalbeat / rsyslog imfile and you have a
permanent record of every operator action.
The codebase is organised around a clear distinction between observe, act, and change — so you always know what a command can do before you run it.
Structured checks that never modify the system. Output is human-readable
(coloured PASS/WARN/FAIL tables with suggested fixes) or machine-readable
(--json).
sudo plesk-toolbox audit # full audit (sec + health)
sudo plesk-toolbox audit sec # security pillar only
sudo plesk-toolbox audit health # health pillar only
sudo plesk-toolbox audit mail # mail-related checks across both
sudo plesk-toolbox audit --list # enumerate available checks
sudo plesk-toolbox audit sec --json > out.jsonLegacy shims still work: plesk-sec-audit, plesk-audit.
Security checks (audits.d/sec/)
| ID | What it checks |
|---|---|
10-system-ssh |
PermitRootLogin, PasswordAuthentication, Protocol, Port |
12-system-updates |
pending security updates (apt/dnf) |
20-network-ports |
externally-bound listeners vs. whitelist |
30-plesk-panel |
panel 2FA, panel cert, admin email |
32-plesk-fail2ban |
service, jails, recent ban count |
50-web-tls |
per-domain certificate expiry on :443 |
52-web-security-headers |
per-domain HTTP response headers — HSTS, X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, Permissions-Policy, COOP, COEP, CORP. Auto-skips vhosts serving the Plesk "Domain Default page" placeholder (set WEB_HEADERS_SKIP_DEFAULT_PAGE=0 to keep them) |
54-mail-tls |
per mail-enabled domain: mail.<d> on 25/465/587/993/995 — reachability + cert expiry + hostname match |
56-mail-mta-sts |
MTA-STS TXT + policy file (RFC 8461) + TLSRPT (RFC 8460) |
58-mail-autoconfig |
Thunderbird autoconfig (autoconfig.<d> or .well-known) + Outlook autodiscover (autodiscover.<d> host or _autodiscover._tcp SRV) |
60-mail-webmail |
per mail-enabled domain: webmail.<d> DNS + TLS (strict vs. insecure to distinguish self-signed from no-service) + content classification (Roundcube, Horde, SOGo, RainLoop, SnappyMail, AfterLogic, or Plesk default page) |
Health checks (audits.d/health/)
| ID | What it checks |
|---|---|
14-system-memory |
cgroup-aware memory + sw-engine RSS |
16-system-disk |
per-mountpoint usage + inode pressure |
18-system-services |
nginx/apache/mariadb/postfix/dovecot/sw-cp-server/psa — aliases apache2↔httpd and mariadb↔mysql so the same audit fires on Debian/Ubuntu and CentOS/RHEL |
40-plesk-license |
license validity, edition, expiry days from keyinfo --list |
42-mail-hygiene |
consolidated per-domain table — SPF (uniqueness, server-IP coverage, all-qualifier, lookup count, ptr), DKIM (key bits, DNS↔local byte equality, p= revocation, t=y flag), DMARC (policy strength, rua=, multi-record), Mailboxes (count + Plesk fileinto "INBOX" sieve bug + owner check) |
All mail-suite audits (42, 54, 56, 58, 60) share one filter:
they only enumerate domains where Plesk's mail service is actually
enabled (mail_settings.mail_service='true'), and self-skip on a
web-only server. No false warnings on hosts that don't do mail.
Every table audit prints a details: block under the table that
expands each ⚠/✗ row with the why and the how to fix — so a
warning in the Mailboxes column isn't just "⚠ 14", it's "sieve
fileinto \"INBOX\" bug in mailbox marketing — change to
\"INBOX.Spam\"".
Ad-hoc operator utilities. May read or write. Fix scripts under tools.d/fix/
use lib/safety.sh — they always offer --dry-run, prompt before destructive
actions on a TTY, and write a JSONL log line per invocation.
# Daily utilities
sudo plesk-tool domain/show example.com # everything about one domain
sudo plesk-tool domain/reissue-cert example.com --with-www
sudo plesk-tool mail/queue-flush --older-than=7d
sudo plesk-tool mail/tail-maillog example.com # filtered maillog tail
sudo plesk-tool mail/spam-top-senders # top abusers
sudo plesk-tool plesk/slow-queries # mysql slow log digest
sudo plesk-tool plesk/sw-engine-rss # PHP-FPM memory hogs
sudo plesk-tool system/top-open-files
# DKIM operator suite (see "DKIM stopped validating yesterday" recipe above)
sudo plesk-tool mail/dkim-show example.com # paste-ready TXT for one domain
sudo plesk-tool mail/dkim-show-all # same, all problem domains
sudo plesk-tool mail/dkim-verify example.com # local match-check; --verifier for end-to-end
sudo plesk-tool mail/dkim-rotate example.com # stage new 2048-bit key (no swap)
sudo plesk-tool mail/dkim-rotate example.com --activate # swap once DNS confirms
# Fix scripts — dry-run optional, confirmation by default
sudo plesk-tool fix/plesk-repair-mail --dry-run # describe only
sudo plesk-tool fix/plesk-repair-mail # prompts on TTY
sudo plesk-tool fix/postfix-tls-policy --yes # non-interactive
sudo plesk-tool fix/letsencrypt-renew-all
# Discoverability
sudo plesk-toolbox tool --list # enumerate available tools
sudo plesk-toolbox tool mail/dkim-rotate --help # per-tool usage blockEvery tool supports --help. Each one carries an embedded usage()
function listing modes, flags, side effects, and (where applicable)
exactly which files it writes and which services it restarts. Tools
without an explicit help block fall back to printing their leading
header comment so you still get a one-paragraph orientation.
Each mod is a self-contained directory with install.sh and uninstall.sh.
Enabling a mod writes a manifest to /var/lib/plesk-toolbox/mods/<name>.manifest
that tracks every file it created, linked, or hid — so disabling cleanly
reverses exactly those operations. No "I'm not sure what this did to my system"
moments.
sudo plesk-mod list # status of every mod (enabled / disabled)
sudo plesk-mod status motd # files owned by this mod
sudo plesk-mod enable motd
sudo plesk-mod disable motdmotd mod — SSH login dashboard. Replaces the default Debian/Ubuntu MOTD
with a branded dashboard showing OS, kernel, uptime, IPs, memory/disk bars,
critical service status, pending updates, fail2ban jails, and recent logins.
Slow data (updates count, public IP, last logins) is prerendered every 10 min
via a systemd timer into /var/cache/server-motd/, so SSH login stays fast.
The default Ubuntu MOTD scripts (10-uname, 50-motd-news,
90-updates-available, …) are renamed to .disabled-* rather than deleted,
so plesk-mod disable motd restores them byte-for-byte.
All knobs live in /etc/plesk-toolbox.conf — start from the comprehensive
plesk-toolbox.conf.example.
Most useful options:
IGNORE_DOMAINS="legacy.example.com dev.example.com"— skip noisy domainsEXPECTED_PORTS="22 25 80 443 …"— the port whitelist for the network auditSEVERITY_OVERRIDE_<check_id>="info"— "I accept this risk" downgrades for specific findings (e.g. you deliberately don't run fail2ban: overridesec.plesk.panel.fail2bantoinfo)MAIL_DOMAIN_TRUNCATE=22— narrow tables for very long domain namesMOTD_BRAND="Acme Hosting · Managed Server"— your brand on the SSH banner
- OS: Debian 11/12, Ubuntu 22.04/24.04, or RHEL family with Plesk Obsidian
- Required:
bash4+,coreutils,ss,openssl,systemctl— all standard on a Plesk box - Optional:
testssl.sh(richer TLS audits),sqlite3(fail2ban history),pt-query-digestormysqldumpslow(nicer slow-query summaries)
No Python, no Ruby, no agents, no outbound calls from the audit path.
plesk-toolbox/
├── bin/ # entry-point shims
│ ├── plesk-toolbox # subcommand dispatcher
│ ├── plesk-audit # → audit
│ ├── plesk-sec-audit # → audit sec
│ ├── plesk-tool # → tool
│ ├── plesk-mod # → mod
│ └── server-motd-refresh # systemd timer worker
├── lib/ # shared bash libraries
│ ├── common.sh # emit API, tables (with auto details:),
│ │ # formatting, config loading
│ ├── plesk.sh # plesk CLI helpers, domain enumeration
│ ├── dkim.sh # DKIM keyfile/pubkey/record helpers
│ ├── tls.sh # testssl.sh / openssl wrapper
│ ├── dispatch.sh # top-level subcommand router (--help aware)
│ ├── runner.sh # audits.d/ + tools.d/ loader (--help aware)
│ ├── safety.sh # dry-run, confirm, lock, reversible ops
│ └── logging.sh # JSONL log writer
├── audits.d/
│ ├── sec/ # NN-<group>-<name>.sh
│ └── health/
├── tools.d/
│ ├── fix/ # opt-in --dry-run, confirmation, logging
│ ├── mail/
│ ├── domain/
│ ├── plesk/
│ └── system/
├── mods.d/
│ └── motd/
├── share/
│ ├── systemd/ # timer + service units
│ └── completion/ # bash completion
├── install.sh
└── plesk-toolbox.conf.example
The whole point of the file-per-check layout is that adding a check should take ten minutes, not a tour of the codebase.
A new audit check: drop audits.d/<pillar>/NN-<group>-<name>.sh — that's
it. The runner auto-discovers via lib/runner.sh. Use the emit / table_*
APIs from lib/common.sh. Look at any existing check as a template.
A new tool: drop tools.d/<group>/<name>.sh with a main() function.
If it writes anything, source lib/safety.sh and use
tool_begin / tool_confirm / tool_run / tool_end so --dry-run,
confirmation, and logging come for free.
A new mod: create mods.d/<name>/{install,uninstall}.sh and use
hide_file / restore_file for reversible changes. Always write a manifest.
Issues, PRs, and "this would be nice to have" tickets are welcome. If you already wrote the check or fix script for yourself, even better — send it over, we probably want it too.
Run shellcheck before opening a PR:
shellcheck -x -s bash -e SC1090,SC1091 bin/* lib/*.sh audits.d/*/*.sh tools.d/*/*.shMIT — see also CODE_OF_CONDUCT.md and
SECURITY.md.