Offline install & upgrade

For a Wazuh manager that cannot reach GitHub or PyPI. You build one archive on a machine that has internet access — the release plus the Python packages it needs — carry it over, and run the installer from it. Nothing on the manager reaches the network.

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, scp through 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.

Pick the release. Releases are listed on GitHub Releases; below, 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-version is the manager's version from the first step.
  • On an ARM manager (aarch64) write manylinux2014_aarch64 and manylinux_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 with No matching distribution found for importlib-metadata. It is harmless on other versions.
One Python version per 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.sh from the extracted directory to /opt/jt-wazuh-mgr. No curl, no apt/dnf, no PyPI.
  • Writes config.yaml only if it does not exist yet.
  • Installs the Python packages from wheels/ into /opt/jt-wazuh-mgr/vendor, never into the system Python: with pip install --no-index when pip is present, otherwise by unpacking the wheels that fit this Python (a wheel is a zip). They are built in vendor.new and 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

  1. connected machine Build the bundle for the new X.Y.Z exactly as in step 1.
  2. 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)
  3. 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
  4. 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

Keptconfig.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/).
ReplacedProgram 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.

PackWhat needs a connectionOffline
jt-ioc, jt-malware-hashThreat feeds, downloaded by the masterRun 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-phpphp.net release dataDownload the release files elsewhere, copy them to etc/jt-packs/offline/php/, read them with --source file://….
jt-runtime-vulnCISA KEV list; Node.js, Python and OpenVPN release dataThe same with file:// options for each source.
jt-alert-digestTeams, Slack, Discord, Telegram, LINEMail (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-webOnly the optional Cloudflare blockA proxy= setting, or do not switch it on; the nginx block works offline.
jt-container-vulnThe Trivy image and its databases, on each Docker hostCopy them to a local registry and point the scanner at it.
jt-docker, jt-vc-portalThe Python docker library, on the Docker hostA local package mirror, or pip download on a connected machine.
jt-portable-detect, jt-linux-integritySysmon (Windows) and auditd (Linux), on the hostsCopy Sysmon64.exe over; auditd from a local mirror.
jt-ucs, jt-synology, jt-jitsiOne package or image on the monitored serverThe local repository, a manual package install, or a local registry.
All othersNothing

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.