#39284·wazuh

Generate random passwords for the default API users (`wazuh`, `wazuh-wui`) at installation

Author: TomasTurinaCreated Sep 14, 2026Updated Sep 18, 2026
Labelstype/enhancementlevel/task

Description

The manager ships two Server API users, and both are created with a password equal to their own username:

User ID Password today Consumer
wazuh 1 wazuh Operators, scripts, API clients
wazuh-wui 2 wazuh-wui The Wazuh dashboard, to reach the Server API on port 55000

Both are linked to the administrator role, the credentials are public (they are literals in framework/wazuh/rbac/default/users.yaml:6,16 and are documented in the installation guide), and the API starts and serves normally with them. Anyone who can reach port 55000 on a fresh installation has full administrator access to the manager until the operator changes the passwords by hand. The only current mitigation is a WARNING line at startup (api/scripts/wazuh_manager_apid.py:97-112).

We want every installation to end up with unique, randomly generated, policy-compliant passwords for both users, disclosed to the operator in a secure way, and propagated to the Wazuh dashboard so that it keeps working out of the box — in all installation flows (packages, sources, installation assistant all-in-one, installation assistant distributed, containers, Kubernetes) and in all cluster topologies.

How it works today

  • rbac.db (/var/wazuh-manager/api/configuration/security/rbac.db) is not created by any installer. It is created and seeded lazily on the first wazuh-manager-apid start, by check_database_integrity() (framework/wazuh/rbac/orm.py:2461-2548) → DatabaseManager.insert_default_resources() (orm.py:2092-2099), which reads the plaintext password: values from users.yaml and hashes them with Werkzeug scrypt.
  • No installation script in this repo generates any API credential. The only random-secret precedent is the cluster key at src/init/inst-functions.sh:995-999.
  • The password policy (12–64 characters, at least one uppercase, one lowercase, one digit and one symbol) is enforced only in framework/wazuh/security.py:23; reserved users (ID ≤ 99) can only be modified by another reserved user (security.py:232-240, error 5011).
  • The only first-party tooling to change them is bin/rbac_control change-password (interactive prompt, --password-file, --passwords-file; never accepts a password as a command-line argument). rbac_control factory-reset deletes the DB and restores the shipped defaults.
  • wazuh-manager-apid runs on the master node only (src/init/wazuh-server.sh:193,392), and rbac.db is not cluster-synchronized: every node keeps its own copy with the shipped defaults until it becomes master.
  • The dashboard holds the only external copy of the wazuh-wui credential, in wazuh_core.hosts.<host>.password inside /etc/wazuh-dashboard/opensearch_dashboards.yml, read once at startup (in-tree sample: api/tools/env/wazuh-dashboard/wazuh.dashboard.yml:44-54). Container images take it from the API_USERNAME / API_PASSWORD environment variables.
  • wazuh-install.sh and wazuh-passwords-tool.sh are not in this repository — they live in wazuh/wazuh-installation-assistant, which is where the all-in-one and distributed flows are orchestrated.

Proposed approach

  1. Generate at RBAC seed time (framework). Remove the password: keys from users.yaml (keep description and allow_run_as) and have insert_default_resources() produce a random password per default user through a new helper (e.g. generate_default_password() in framework/wazuh/core/security.py), built on secrets and guaranteed to satisfy _user_password (security.py:23). Since seeding is lazy and centralized, this single code path covers every installation flow: DEB, RPM, sources, Docker, devcontainer and QA environments. No new hook is needed in install.sh, postinst or the RPM spec.

  2. Provisioning escape hatch. When a pre-seed credentials input is present at DB-creation time (a file under api/configuration/security/, or an equivalent environment input), use those passwords instead of generating new ones. This is what lets the installation assistant, the container images and CI provision deterministic credentials non-interactively. Defining its exact path, format, ownership and lifetime is part of this issue.

  3. Disclose once, securely. Write the generated passwords to WAZUH_HOME/api/configuration/security/wazuh-api-passwords.txt with the most restrictive ownership and mode the apid runtime user allows (target 0400), and log a WARNING at first start pointing at that file and instructing the operator to read it and then delete it. The password itself must never appear in the manager log, in the installer output, or in ps.

  4. Rework the default-password warning. get_users_with_default_password() (framework/wazuh/core/security.py:137-150) and warn_about_default_passwords() lose their meaning once there is no shipped password. Replace the check with "the generated-credentials file is still on disk", keeping the pointer to rbac_control change-password.

  5. factory-reset must not restore a known password. rbac_db_factory_reset() (core/security.py:125-134) has to regenerate and re-disclose, exactly like a fresh install.

  6. Cluster. Each node seeds its own rbac.db, so with random generation a promoted worker would hold a different password and silently break the dashboard (3002 - Request failed with status code 401, then 403 after max_login_attempts). We do not add credential synchronization over the cluster protocol: instead, multi-node installations must push one common password to every node with rbac_control change-password --passwords-file, and this must be documented and verified, including that the tooling works on a node whose apid has never run.

  7. Dashboard and cross-repo wiring. The manager exposes the credentials (generated file) and accepts injected ones; the installation assistant reads them on an all-in-one host and injects/pushes them on a distributed deployment, then writes wazuh_core.hosts.<host>.password in the dashboard configuration. Container and Kubernetes deployments keep using API_USERNAME/API_PASSWORD and the wazuh-api-cred Secret.

Installation cases that must be analysed and tested

  • Packages: DEB fresh install, RPM fresh install, sources install.sh.
  • Reinstall / upgrade over an existing rbac.db: passwords must be preserved, never regenerated (DEB carries the file over in preinst/postinst, RPM simply does not own it).
  • Topologies: single node; cluster master + worker; worker promoted to master.
  • Installation assistant, all-in-one: manager + indexer + dashboard on the same host.
  • Installation assistant, distributed: dashboard on a different host from the manager.
  • Containers: api/tools/env/ docker-compose and the devcontainer e2e dashboard image (src/engine/tools/devContainer/e2e/wazuh-dashboard/Dockerfile:17-20), which today rely on the shipped defaults.
  • Kubernetes: dashboard fed from the wazuh-api-cred Secret.
  • rbac_control factory-reset and rbac_control change-password in all of the above.

Impact on existing tests

Roughly fifteen suites hard-code these credentials and will break; they must be migrated to the new provisioning mechanism. The main ones: api/test/integration/common.yaml:14-17 (including the base64 basic_auth / basic_auth_context blobs), the RBAC/security tavern suites, the env SQL fixtures under api/test/integration/env/configurations/, tests/integration/test_api/test_miscs/test_recursion.py:56-57, tests/integration/test_api/test_miscs/test_run_as_chunked_body.py:58-59, and api/tools/env/wazuh-dashboard/wazuh.dashboard.yml:52-53.

Tasks

Manager (this repository)

  • Add a random password generator (secrets-based) that always satisfies the API password policy at framework/wazuh/security.py:23, with unit tests asserting compliance and uniqueness across invocations.
  • Remove the plaintext password: entries from framework/wazuh/rbac/default/users.yaml and generate the password per default user in insert_default_resources() (orm.py:2092-2099).
  • Design and implement the pre-seed/injection input for automated provisioning (path, format, ownership, who writes it, when it is consumed and removed).
  • Write the generated credentials to a restricted-permission file and log a WARNING pointing to it; guarantee the secret is never logged nor exposed through the process list.
  • Rework get_users_with_default_password() and warn_about_default_passwords() to warn about an undeleted credentials file instead of a shipped default password.
  • Make rbac_db_factory_reset() regenerate and re-disclose new random passwords.
  • Verify rbac_control change-password still satisfies the reserved-user rule (5011) with the new flow, and that it can set credentials on a node whose apid has never started (worker); extend the tool if it cannot.
  • Confirm package upgrade paths preserve the credentials (DEB preinst/postinst copy of rbac.db, RPM spec not owning the file) and that nothing regenerates on restart.
  • Migrate every test fixture and suite that hard-codes wazuh:wazuh / wazuh-wui:wazuh-wui (tavern common.yaml + base64 blobs, env SQL fixtures, test_recursion.py, test_run_as_chunked_body.py, api/tools/env/wazuh-dashboard/wazuh.dashboard.yml, devcontainer e2e dashboard image) to the provisioning mechanism.
  • Add unit and API integration tests for: generation, disclosure file (content and permissions), credential injection, no-regeneration on restart, and factory-reset.
  • Update the documentation: docs/ref/getting-started/installation.md:189-271 ("Change the default API passwords") and docs/ref/modules/server-api/authentication.md:42-91 (default users, what a password change does, cluster/worker-promotion behaviour).
  • Add the CHANGELOG.md entry.

Cross-repository (open as linked child issues)

  • wazuh/wazuh-installation-assistant — consume or inject the credentials, write wazuh_core.hosts.<host>.password in the dashboard configuration, keep wazuh-passwords-tool.sh working, and report/store the generated passwords for both the all-in-one and the distributed flows.
  • wazuh/wazuh-dashboard, wazuh/wazuh-docker, wazuh/wazuh-kubernetes — confirm the credential sources (opensearch_dashboards.yml, API_USERNAME/API_PASSWORD, wazuh-api-cred Secret) still work and document the change.

Validation

  • Execute the full installation-case matrix listed above and attach evidence per case.
  • For every case, confirm that wazuh:wazuh and wazuh-wui:wazuh-wui are rejected with 401, that the provisioned credential authenticates, and that the dashboard reaches the API.

Definition of Done

  • No installation flow ends with a predictable API password; the default passwords are gone from the repository.
  • Every fresh installation gets unique, policy-compliant passwords for wazuh and wazuh-wui; restarts and package upgrades over an existing rbac.db preserve them; factory-reset produces new random ones.
  • The generated passwords are disclosed exactly once, through a restricted-permission file, never in logs, installer output or ps, with a clear operator message on how to read, use and remove it.
  • The Wazuh dashboard authenticates against the Server API out of the box in all-in-one and in distributed installations; container and Kubernetes deployments keep working through their environment/Secret path.
  • Automated and unattended provisioning (installation assistant, Docker, CI) can inject credentials non-interactively, without ever passing them on a command line.
  • Cluster behaviour is documented and verified, including the worker-promotion scenario, with a documented procedure to align credentials across nodes.
  • Every test suite that hard-coded the default credentials is updated and green in CI; new unit and integration tests cover generation, disclosure, injection, no-regeneration and factory-reset.
  • E2E evidence attached for every case of the installation matrix (install output, credentials file permissions, 401 on the old defaults, successful dashboard login, manager logs).
  • Documentation updated and CHANGELOG.md entry added.
  • Cross-repository child issues opened, linked and resolved.
  • Reviewed and merged into 5.0.0.