Before you start
What you need
- A connected machine with Python 3 and
pip(Linux, macOS or Windows) — or, simplest, a Linux machine with Docker. - A way to copy one file (about 10 MB) to the manager: USB drive,
scpthrough a jump host, etc. - root on the Wazuh manager — the master node in a cluster. Nothing goes on workers or agents.
Two facts to note on the manager first
Wazuh manager The Python packages are compiled per Python version and CPU, so the bundle has to match them:
/usr/bin/python3 -V
uname -m
The service runs /usr/bin/python3, so that is the version that counts — for example
Python 3.9.25 → 3.9, and x86_64. pip does not need to be
installed on the manager.
X.Y.Z stands for the version you choose (without the leading v).
install.sh --offline exists in releases after v1.19.0; for v1.19.0 and earlier, use the
manual steps.1. Build the bundle
Download the release
connected machine
VER=X.Y.Z
curl -fsSLO https://github.com/jasoncheng7115/jt-wazuh-mgr/archive/refs/tags/v$VER.tar.gz
tar xzf v$VER.tar.gz # creates jt-wazuh-mgr-$VER/
GitHub names the directory inside the archive jt-wazuh-mgr-X.Y.Z (no v).
Download the Python packages (wheels) — recommended way
connected machine Run pip download with the same Python version as the manager. A throwaway
container is the easiest way to get it; replace 3.9 with the manager's version:
docker run --rm -v "$PWD":/w -w /w python:3.9-slim \
pip download --only-binary=:all: -r jt-wazuh-mgr-$VER/requirements.txt -d jt-wazuh-mgr-$VER/wheels
Running the same pip download on a machine with the same OS release as the manager works just as well.
--only-binary=:all: makes a missing wheel fail here, on the connected machine, rather than on the manager.
The container runs on the CPU of the machine it is started on; for an ARM manager prepared on an x86 machine,
use the method below.
Without Docker: download for another Python version
connected machine pip can fetch wheels for a version other than its own:
python3 -m pip download -r jt-wazuh-mgr-$VER/requirements.txt "importlib-metadata>=3.6" \
--only-binary=:all: --implementation cp --python-version 3.9 \
--platform manylinux2014_x86_64 --platform manylinux_2_17_x86_64 \
-d jt-wazuh-mgr-$VER/wheels
--python-versionis the manager's version from the first step.- On an ARM manager (
aarch64) writemanylinux2014_aarch64andmanylinux_2_17_aarch64. - Keep
"importlib-metadata>=3.6". In this mode pip evaluates conditional requirements against its own Python, so it skips the package Flask needs only on Python 3.9 — the install on a 3.9 manager (RHEL 9, Rocky/Alma 9, Amazon Linux 2023, the Wazuh manager container) then stops withNo matching distribution found for importlib-metadata. It is harmless on other versions.
wheels/ directory. If your managers run different Python
versions, build one bundle per version.Pack it up
connected machine
tar czf jt-wazuh-mgr-$VER-offline.tar.gz jt-wazuh-mgr-$VER
sha256sum jt-wazuh-mgr-$VER-offline.tar.gz > jt-wazuh-mgr-$VER-offline.tar.gz.sha256
2. Install on the manager
Copy, check, extract, install
Wazuh manager Copy both files over (for example to /root), then as root:
cd /root
sha256sum -c jt-wazuh-mgr-X.Y.Z-offline.tar.gz.sha256
tar xzf /root/jt-wazuh-mgr-X.Y.Z-offline.tar.gz -C /root
bash /root/jt-wazuh-mgr-X.Y.Z/install.sh --offline
Always extract with -C and run the installer by its full path; that way the result does not depend on
the directory you happen to be in.
What --offline does
- Copies the program, the rule-pack catalogue, icons, the service file and
uninstall.shfrom the extracted directory to/opt/jt-wazuh-mgr. Nocurl, noapt/dnf, no PyPI. - Writes
config.yamlonly if it does not exist yet. - Installs the Python packages from
wheels/into/opt/jt-wazuh-mgr/vendor, never into the system Python: withpip install --no-indexwhen pip is present, otherwise by unpacking the wheels that fit this Python (a wheel is a zip). They are built invendor.newand swapped in only after they import, so a bad bundle leaves the working copy alone. - Installs
/etc/systemd/system/jt-wazuh-mgr.service, enables and starts it, and checks two seconds later that it is still running.
Wheels kept somewhere else: install.sh --offline --wheels /path/to/wheels.
3. Verify
Read the result back from the system, not from the installer
Wazuh manager
grep __version__ /opt/jt-wazuh-mgr/lib/__init__.py # the version you installed
systemctl is-active jt-wazuh-mgr # active
curl -ks -o /dev/null -w '%{http_code}\n' https://127.0.0.1:5000/login # 200
journalctl -u jt-wazuh-mgr -n 20 --no-pager # if any of the above is wrong
Then open https://YOUR_WAZUH_MANAGER_IP:5000 and log in with a Wazuh API user (not the
wazuh-wui account the dashboard uses). If the browser does not get through but the
curl above returns 200, open TCP 5000 in the manager's firewall.
4. Upgrade
Same bundle, newer version
- connected machine Build the bundle for the new
X.Y.Zexactly as in step 1. - Wazuh manager Back up the current installation and the service file:
tar czf /root/jt-wazuh-mgr-backup-$(date +%F).tar.gz -C /opt jt-wazuh-mgr cp /etc/systemd/system/jt-wazuh-mgr.service /root/jt-wazuh-mgr.service.bak-$(date +%F) - Wazuh manager Extract the new release and run its installer:
tar xzf /root/jt-wazuh-mgr-X.Y.Z-offline.tar.gz -C /root bash /root/jt-wazuh-mgr-X.Y.Z/install.sh --offline - Wazuh manager Verify as in step 3 — the version must be read from
/opt/jt-wazuh-mgr, then reload the browser with Ctrl+Shift+R.
What an upgrade keeps and what it replaces
| Kept | config.yaml; the certificate --ssl-auto generated (ssl_cert.pem,
ssl_key.pem); wazuh_agent_mgr.log; your drop-ins in
/etc/systemd/system/jt-wazuh-mgr.service.d/; rule packs already installed on the manager
(/var/ossec/etc/..., state in /var/ossec/etc/jt-packs/). |
|---|---|
| Replaced | Program files, the rule-pack catalogue in /opt/jt-wazuh-mgr/packs, the icons,
/etc/systemd/system/jt-wazuh-mgr.service, and vendor/ when the bundle has wheels. |
Packs are updated separately, from the Rule Packs tab: it marks the packs whose catalogue version is newer than what the manager has and shows a plan before changing anything. Reload the ruleset on all nodes afterwards.
If the new release's requirements.txt is the same as the installed one
(diff /opt/jt-wazuh-mgr/requirements.txt /root/jt-wazuh-mgr-X.Y.Z/requirements.txt), you may leave the
wheels out: without a wheels/ directory the installer keeps the current vendor/ as long as it still
imports. Carrying the wheels every time is simpler.
5. Roll back
Back to the backup from step 4
Wazuh manager Move the failed version aside rather than deleting it, so you can still look at it:
systemctl stop jt-wazuh-mgr
mv /opt/jt-wazuh-mgr /opt/jt-wazuh-mgr.failed
tar xzf /root/jt-wazuh-mgr-backup-YYYY-MM-DD.tar.gz -C /opt
cp /root/jt-wazuh-mgr.service.bak-YYYY-MM-DD /etc/systemd/system/jt-wazuh-mgr.service
systemctl daemon-reload
systemctl start jt-wazuh-mgr
grep __version__ /opt/jt-wazuh-mgr/lib/__init__.py
Replace YYYY-MM-DD with the date in the backup's file name. Once the old version runs again,
/opt/jt-wazuh-mgr.failed can be removed.
Manual steps (without --offline)
For v1.19.0 and earlier, or if you prefer to see every step
Wazuh manager This is what install.sh --offline does, written out. Run as root, after extracting the bundle as in
step 2 (and, for an upgrade, after the backup in step 4):
VER=X.Y.Z
SRC=/root/jt-wazuh-mgr-$VER
DST=/opt/jt-wazuh-mgr
mkdir -p "$DST"
cp -r "$SRC"/wazuh_agent_mgr.py "$SRC"/create_api_user.py "$SRC"/requirements.txt \
"$SRC"/lib "$SRC"/packs "$SRC"/images "$SRC"/jt-wazuh-mgr.service "$SRC"/uninstall.sh "$DST"/
[ -f "$DST/config.yaml" ] || cp "$SRC/config.yaml" "$DST/" # never overwrite your config
rm -rf "$DST/vendor.new"
Then install the Python packages into vendor.new, one of the two:
# a) pip is installed on the manager
python3 -m pip install --no-index --find-links "$SRC/wheels" --target "$DST/vendor.new" -r "$DST/requirements.txt"
# b) no pip: a wheel is a zip
mkdir "$DST/vendor.new"
for w in "$SRC"/wheels/*.whl; do python3 -m zipfile -e "$w" "$DST/vendor.new"; done
Swap them in only if they import, and (re)start the service:
PYTHONPATH="$DST/vendor.new" /usr/bin/python3 -c 'import flask, yaml, requests, rich, tabulate' \
&& rm -rf "$DST/vendor" && mv "$DST/vendor.new" "$DST/vendor"
cp "$DST/jt-wazuh-mgr.service" /etc/systemd/system/
systemctl daemon-reload
systemctl enable jt-wazuh-mgr
systemctl restart jt-wazuh-mgr
Verify as in step 3.
When the wheels do not fit
No matching distribution found
ERROR: No matching distribution found for PyYAML>=6.0 The wheels in /root/jt-wazuh-mgr-X.Y.Z/wheels do not cover requirements.txt for Python 3.12.The wheels were downloaded for another Python version or CPU. The installer stops before touching
vendor/ or restarting the service, so the running version keeps working (the program files were already
copied and take effect at the next restart). Check /usr/bin/python3 -V and uname -m again, rebuild the
wheels/ directory for that version (step 1), and run the installer again —
or roll back.
If the missing package is importlib-metadata, the wheels were made with the
--python-version method without that extra argument; see step 1.
The wheels do not import on Python 3.x
The wheels in ... do not import on Python 3.9 (missing, or built for another Python version).Same cause, seen on a manager without pip: the compiled packages (PyYAML, MarkupSafe, charset-normalizer) carry the Python
version in their file names, e.g. cp39, and only fitting ones are used. Rebuild the wheels for the
manager's version.
A Python version with no published wheels
Very new or unusual Python versions can be ahead of the binary wheels on PyPI, and --only-binary=:all: then
fails on the connected machine. Build the wheels yourself there, with the same OS release and Python version as the manager
(packages without a published wheel are built from source, which may need a compiler):
python3 -m pip wheel -r jt-wazuh-mgr-$VER/requirements.txt -w jt-wazuh-mgr-$VER/wheels
Distribution packages instead of wheels
If the manager has a local package mirror, the distribution's own packages work too. Install them, leave
wheels/ out of the bundle, and the installer keeps going because the modules already import:
apt install python3-flask python3-yaml python3-requests python3-rich python3-tabulate # Debian / Ubuntu
requirements.txt asks for Flask 2.3 or newer; check what your distribution ships
(apt-cache policy python3-flask) before choosing this route.
Rule packs without internet
Installing, updating and removing packs needs no internet: they ship inside the tool and are installed from the Rule packs tab. What may need a connection is what some packs do afterwards. Each such pack has a step "Manager without Internet access" (or one for its hosts) in its deployment guide, with the exact commands.
| Pack | What needs a connection | Offline |
|---|---|---|
jt-ioc, jt-malware-hash | Threat feeds, downloaded by the master | Run the updater on a connected machine, copy its cache to the master, run it with --skip-download; stop the scheduled download with --reload-if-stale. |
jt-php | php.net release data | Download the release files elsewhere, copy them to etc/jt-packs/offline/php/, read them with --source file://…. |
jt-runtime-vuln | CISA KEV list; Node.js, Python and OpenVPN release data | The same with file:// options for each source. |
jt-alert-digest | Teams, Slack, Discord, Telegram, LINE | Mail (internal SMTP relay), Zulip, Mattermost and the generic webhook work on your own network; the cloud channels through https_proxy, or leave them unset. |
jt-web | Only the optional Cloudflare block | A proxy= setting, or do not switch it on; the nginx block works offline. |
jt-container-vuln | The Trivy image and its databases, on each Docker host | Copy them to a local registry and point the scanner at it. |
jt-docker, jt-vc-portal | The Python docker library, on the Docker host | A local package mirror, or pip download on a connected machine. |
jt-portable-detect, jt-linux-integrity | Sysmon (Windows) and auditd (Linux), on the hosts | Copy Sysmon64.exe over; auditd from a local mirror. |
jt-ucs, jt-synology, jt-jitsi | One package or image on the monitored server | The local repository, a manual package install, or a local registry. |
| All others | Nothing |
Options that survive pack updates
The tool rewrites /etc/cron.d/jt-<pack> whenever it installs or updates a pack, so do not edit that file. Put site options in /var/ossec/etc/jt-packs/<pack>.cron.local instead (owned by root, not writable by others): a line <script>: <options> adds options to that script, a line NAME=value sets an environment variable. The tool applies it every time it writes the schedule; to apply it now, open the pack on the Rule packs tab and press Rewrite schedule. Workers run a schedule you copied by hand: add the same options there.
# /var/ossec/etc/jt-packs/jt-ioc.cron.local
update-threat-ip-list.py: --reload-if-stale
# /var/ossec/etc/jt-packs/jt-alert-digest.cron.local
https_proxy=http://proxy.example.com:3128
no_proxy=localhost,127.0.0.1
An offline copy never ages by itself: refresh it after each security release, and set stale_hours where the pack offers it, so an overdue import raises an alert.
What else to know offline
What works without internet, and what does not
- Everything that talks to Wazuh works: the tool only uses the manager's own API and files.
- Rule packs install from the catalogue that ships in the release. What some of them do afterwards may need a connection: see Rule packs without internet.
- The configuration editor's syntax highlighting is loaded by your browser from cdnjs. Without it the editor still works, just without colours.
Uninstall
Works offline as is: bash /opt/jt-wazuh-mgr/uninstall.sh. Remove installed rule packs from the
Rule Packs tab first if you want the original files restored.
Something else went wrong? See Install & upgrade troubleshooting.