diff --git a/.github/.goss.yaml b/.github/.goss.yaml index 6ccdf1851..e724f2aeb 100644 --- a/.github/.goss.yaml +++ b/.github/.goss.yaml @@ -13,20 +13,11 @@ file: group: wazuh-manager filetype: file contains: [] - /var/wazuh-manager/etc/sslmanager.cert: + /var/wazuh-manager/etc/certs: exists: true - mode: "0644" owner: root - group: root - filetype: file - contains: [] - /var/wazuh-manager/etc/sslmanager.key: - exists: true - mode: "0600" - owner: root - group: root - filetype: file - contains: [] + group: wazuh-manager + filetype: directory user: wazuh-manager: exists: true diff --git a/.github/workflows/5_check_integration_tools.yml b/.github/workflows/5_check_integration_tools.yml index 9f7c60dad..e89af07db 100644 --- a/.github/workflows/5_check_integration_tools.yml +++ b/.github/workflows/5_check_integration_tools.yml @@ -597,7 +597,7 @@ jobs: set -e cd /tmp/wazuh-docker/${DEPLOYMENT} echo '=== Running certificate generation ===' - sudo bash /tmp/wazuh-docker/tools/utils/deployment/certificates-conf.sh --cert --copy + sudo bash /tmp/wazuh-docker/tools/utils/deployment/certificates-conf.sh --cert --copy --priv echo '' echo '=== Generated certificate files ===' find ./config -name '*.pem' | sort @@ -675,6 +675,35 @@ jobs: exit 1 " + - name: Verify manager certificates are unique per container + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + set -eo pipefail + cd /tmp/wazuh-docker/${DEPLOYMENT} + + rm -f /tmp/remoted-fingerprints.txt + for SVC in \$(sudo docker compose config --services | grep -E 'wazuh\.(manager|master|worker)'); do + echo \"=== \$SVC ===\" + sudo docker compose exec -T \$SVC ls -ld /var/wazuh-manager/etc/certs + sudo docker compose exec -T \$SVC ls -la /var/wazuh-manager/etc/certs + sudo docker compose exec -T \$SVC openssl x509 \ + -in /var/wazuh-manager/etc/certs/remoted.pem -noout -subject -dates + sudo docker compose exec -T \$SVC openssl x509 \ + -in /var/wazuh-manager/etc/certs/remoted.pem -noout -fingerprint \ + | tee -a /tmp/remoted-fingerprints.txt + done + + # Every manager node must hold its own self-signed certificate + DUPLICATED=\$(sort /tmp/remoted-fingerprints.txt | uniq -d) + if [ -n \"\$DUPLICATED\" ]; then + echo 'ERROR: the same remoted certificate is shared across manager nodes' + echo \"\$DUPLICATED\" + exit 1 + fi + echo 'OK: each manager node has its own remoted certificate' + " + - name: Cluster warm-up wait run: | if [ "${{ matrix.deployment_type }}" = "multi-node" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3f7f6b5..6264fbc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ | Issue | Comment | | - | - | | [#2461](https://github.com/wazuh/wazuh-docker/issues/2461) | Added explicit `permissions` blocks to the 4.x workflows to restrict the `GITHUB_TOKEN` scope | +| [#2601](https://github.com/wazuh/wazuh-docker/issues/2601) | Regenerate the manager self-signed server certificate per container at first boot | ### Removed diff --git a/build-docker-images/wazuh-manager/Dockerfile b/build-docker-images/wazuh-manager/Dockerfile index 91c6de341..1cd451ee0 100644 --- a/build-docker-images/wazuh-manager/Dockerfile +++ b/build-docker-images/wazuh-manager/Dockerfile @@ -45,8 +45,11 @@ RUN dnf install openssl findutils procps shadow-utils -y && \ mkdir -p /var/wazuh-manager/etc/certs && \ chown root:wazuh-manager /var/wazuh-manager/etc/certs && \ chmod 1770 /var/wazuh-manager/etc/certs && \ - rm -f /var/wazuh-manager/etc/sslmanager.key && \ - rm -f /var/wazuh-manager/etc/sslmanager.cert + # Remove manager self-signed certificate/key + rm -f /var/wazuh-manager/etc/certs/remoted.pem \ + /var/wazuh-manager/etc/certs/remoted-key.pem \ + /var/wazuh-manager/etc/certs/apid.pem \ + /var/wazuh-manager/etc/certs/apid-key.pem # Prepare permanent data snapshot (sync calls: https://github.com/docker/docker/issues/9547) RUN chmod 755 /permanent_data.sh && \ diff --git a/build-docker-images/wazuh-manager/config/etc/cont-init.d/0-wazuh-init b/build-docker-images/wazuh-manager/config/etc/cont-init.d/0-wazuh-init index fa5010819..096e00fbd 100644 --- a/build-docker-images/wazuh-manager/config/etc/cont-init.d/0-wazuh-init +++ b/build-docker-images/wazuh-manager/config/etc/cont-init.d/0-wazuh-init @@ -44,6 +44,8 @@ mount_permanent_data() { # Check if the path is not empty if find ${permanent_dir} -mindepth 1 | read; then print "The path ${permanent_dir} is already mounted" + elif [ ! -d "${data_tmp}" ]; then + print "No permanent data snapshot for ${permanent_dir}, skipping" else print "Installing ${permanent_dir}" exec_cmd "cp -ar ${data_tmp}. ${permanent_dir}" @@ -117,12 +119,93 @@ remove_data_files() { ############################################################################## # Create certificates: Manager +# +# The manager has a single self-signed server pair, etc/certs/remoted.*, read +# both by remoted's HTTPS agent listener () and by authd +# (). The package post-install generates it, so the image +# build removes it (see Dockerfile) and every container generates its own here: +# no two containers from the same image share a private key. +# +# Both consumers are read from the configuration and generated independently, so +# a deployment pointing one of them at a custom path still gets a valid pair. +# +# remoted opens these files after dropping privileges and does not start if they +# are missing or unreadable, so this must run before the manager is started and +# the files must end up wazuh-manager:wazuh-manager 0640. ############################################################################## -create_wazuh_key_cert() { - print "Creating wazuh-authd key and cert" - exec_cmd "openssl genrsa -out ${WAZUH_INSTALL_PATH}/etc/sslmanager.key 4096" - exec_cmd "openssl req -new -x509 -key ${WAZUH_INSTALL_PATH}/etc/sslmanager.key -out ${WAZUH_INSTALL_PATH}/etc/sslmanager.cert -days 3650 -subj /CN=${HOSTNAME}/" +# Certificate paths in the configuration are Wazuh-home relative, as the rest of +# the manager configuration is. +resolve_wazuh_path() { + case "$1" in + /*) echo "$1" ;; + *) echo "${WAZUH_INSTALL_PATH}/$1" ;; + esac +} + +# $1: XML tag, $2: enclosing block +get_conf_path() { + sed -n "/<$2>/,/<\/$2>/s|.*<$1>\(.*\).*|\1|p" \ + "${WAZUH_INSTALL_PATH}/etc/wazuh-manager.conf" | head -n1 +} + +# $1: certificate path, $2: key path +create_server_cert_pair() { + local cert=$(resolve_wazuh_path "$1") + local key=$(resolve_wazuh_path "$2") + + # Mixed-ownership directory: the daemons regenerate their own certificates here + # (group write), while the sticky bit keeps them from replacing the root-owned + # indexer trust material that shares the directory. Ownership and mode are + # best-effort: the directory may be a read-only mount, and failing to re-apply + # them must not stop the certificate from being created. + exec_cmd "mkdir -p $(dirname "${cert}") $(dirname "${key}")" + chown root:wazuh-manager "$(dirname "${cert}")" 2>/dev/null || true + chmod 1770 "$(dirname "${cert}")" 2>/dev/null || true + + if [ -e "${cert}" ] && [ -e "${key}" ] + then + print "Manager server certificate ${cert} already present, skipping generation" + elif [ -e "${cert}" ] || [ -e "${key}" ] + then + # Same guard as the package post-install: never overwrite operator-provided + # material. Half a pair cannot be completed, so warn instead of guessing. + print "Warning: only one of ${cert} / ${key} is present; remoted will not start" + return + else + print "Creating the manager server certificate and key (${cert})" + ${WAZUH_INSTALL_PATH}/bin/wazuh-manager-remoted -C 3650 -B 4096 \ + -S "/C=US/ST=California/CN=Wazuh/" -K "${key}" -X "${cert}" > /dev/null 2>&1 || \ + exec_cmd "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout ${key} -out ${cert} -subj /C=US/ST=California/CN=Wazuh/" + fi + + # remoted and authd read these after dropping privileges. Not fatal on failure: + # an operator-provided certificate may be a read-only bind mount. + chown wazuh-manager:wazuh-manager "${cert}" "${key}" 2>/dev/null || \ + print "Warning: could not set ownership on ${cert} (read-only mount?)" + chmod 640 "${cert}" "${key}" 2>/dev/null || true +} + +create_manager_server_certs() { + local https_cert=$(get_conf_path certificate https) + local https_key=$(get_conf_path key https) + local authd_cert=$(get_conf_path ssl_manager_cert auth) + local authd_key=$(get_conf_path ssl_manager_key auth) + + # The manager has one self-signed pair and one identity: authd's + # is derived from the HTTPS listener's by the + # installation itself, and /enroll's mTLS mode treats that certificate as the + # enrollment credential. Only that pair is generated, exactly as the package + # post-install does; a mismatch can only come from hand-edited configuration. + create_server_cert_pair "${https_cert:-etc/certs/remoted.pem}" \ + "${https_key:-etc/certs/remoted-key.pem}" + + if [ "${authd_cert:-etc/certs/remoted.pem}" != "${https_cert:-etc/certs/remoted.pem}" ] || \ + [ "${authd_key:-etc/certs/remoted-key.pem}" != "${https_key:-etc/certs/remoted-key.pem}" ] + then + print "Warning: authd (${authd_cert}) and the HTTPS listener (${https_cert}) use different certificates; both must present the same manager identity" + fi } ############################################################################## @@ -277,12 +360,6 @@ main() { # Remove some files in permanent_data (i.e. .template.db) remove_data_files - # Create wazuh-authd key and cert if not present - if [ ! -e ${WAZUH_INSTALL_PATH}/etc/sslmanager.key ] - then - create_wazuh_key_cert - fi - # Mount selected files (WAZUH_CONFIG_MOUNT) to container mount_files @@ -291,6 +368,12 @@ main() { # Configure wazuh-manager.conf based on environment variables function_configure_wazuh_manager_conf + + # Create the manager server certificates if not present. After mount_files and + # the configuration rewrite, so a mounted configuration with custom certificate + # paths is honoured and a mounted certificate is detected as already present. + create_manager_server_certs + # Delete temporary data folder rm -rf ${WAZUH_INSTALL_PATH}/data_tmp diff --git a/build-docker-images/wazuh-manager/config/etc/cont-init.d/1-manager b/build-docker-images/wazuh-manager/config/etc/cont-init.d/1-manager index 153f2b1c7..23da1bdf4 100644 --- a/build-docker-images/wazuh-manager/config/etc/cont-init.d/1-manager +++ b/build-docker-images/wazuh-manager/config/etc/cont-init.d/1-manager @@ -43,11 +43,6 @@ function_wazuh_migration(){ chown wazuh-manager:wazuh-manager /var/wazuh-manager/etc/client.keys chmod 640 /var/wazuh-manager/etc/client.keys - \cp -f /wazuh-migration/data/etc/sslmanager.cert /var/wazuh-manager/etc/sslmanager.cert - \cp -f /wazuh-migration/data/etc/sslmanager.key /var/wazuh-manager/etc/sslmanager.key - chown root:root /var/wazuh-manager/etc/sslmanager.cert /var/wazuh-manager/etc/sslmanager.key - chmod 640 /var/wazuh-manager/etc/sslmanager.cert /var/wazuh-manager/etc/sslmanager.key - \cp -f /wazuh-migration/data/etc/shared/default/agent.conf /var/wazuh-manager/etc/shared/default/agent.conf chown wazuh-manager:wazuh-manager /var/wazuh-manager/etc/shared/default/agent.conf chmod 660 /var/wazuh-manager/etc/shared/default/agent.conf diff --git a/docs/ref/configuration/configuration-files.md b/docs/ref/configuration/configuration-files.md index e1934003f..64bcab027 100644 --- a/docs/ref/configuration/configuration-files.md +++ b/docs/ref/configuration/configuration-files.md @@ -50,6 +50,12 @@ To persist files or directories in your Wazuh deployment, you can mount them as > **Important**: Ensure that files exist on the host before starting the containers. If the file doesn't exist, Docker will create a directory instead, which may cause startup failures. +### Wazuh manager self-signed certificate + +The `docker-compose.yml` files mount a named volume on `/var/wazuh-manager/etc` (`wazuh_etc` in single-node; `master-wazuh-etc` and `worker-wazuh-etc` in multi-node). That volume holds `certs/remoted.pem` and `certs/remoted-key.pem`, the self-signed pair each manager container generates on its first start and reuses on every later start. It is used by the HTTPS agent listener and by agent enrollment (`authd`), and it is unique per deployment and per cluster node. + +Removing the volume (for example, with `docker compose down -v`) deletes the pair, and the next start generates a new one. Agents do not validate this certificate by default, so a new pair does not break already enrolled agents. Do not copy the volume between deployments: that reuses the same private key in both. See [Security](../security.md) for rotation and for using your own certificate. + ### Wazuh Dashboard keystore The `docker-compose.yml` files mount the named volume `wazuh-dashboard-config` on `/usr/share/wazuh-dashboard/config`, which is where `opensearch_dashboards.keystore` is stored. Keeping this volume preserves the `wazuh_ai_assistant.encryptionKey` generated on the first start. diff --git a/docs/ref/integration_test/docker_integration_tests.md b/docs/ref/integration_test/docker_integration_tests.md index 6967f5db3..2d7ec42a9 100644 --- a/docs/ref/integration_test/docker_integration_tests.md +++ b/docs/ref/integration_test/docker_integration_tests.md @@ -215,7 +215,7 @@ Runs on the **runner** (not the VM): 3. **Copy `wazuh-docker/` to VM** via SCP: `scp -r wazuh-docker {remote}:/tmp/wazuh-docker` -4. **Generate certificates on VM**: runs `tools/utils/deployment/certificates-conf.sh --cert --copy` inside `/tmp/wazuh-docker/{deployment}/` +4. **Generate certificates on VM**: runs `tools/utils/deployment/certificates-conf.sh --cert --copy --priv` inside `/tmp/wazuh-docker/{deployment}/` #### Deployment diff --git a/docs/ref/security.md b/docs/ref/security.md index 7990b8200..95cb4877d 100644 --- a/docs/ref/security.md +++ b/docs/ref/security.md @@ -15,6 +15,20 @@ This section summarizes security recommendations for Wazuh Docker deployments (s - Regenerate certificates if private keys are leaked or if nodes are re-provisioned. - Use certificates and TLS settings appropriate for production (trusted CA, correct DNS names, and key protection). +### Manager self-signed certificate + +The Wazuh manager image ships no self-signed certificate. Each manager container generates its own `etc/certs/remoted.pem` and `etc/certs/remoted-key.pem` on its first start, so no two containers share a private key. That pair serves both the HTTPS agent listener and the agent enrollment service (`authd`), and it is stored in the manager `etc` volume (`wazuh_etc` in single-node; `master-wazuh-etc` and `worker-wazuh-etc` in multi-node), which keeps it stable across restarts and container recreation. + +- Do not copy or share the manager `etc` volume between deployments, and do not bake the pair into a derived image: either reintroduces a shared private key. +- To rotate the certificate, remove both files and restart the manager. The next start generates a new pair: + ```bash + docker compose exec wazuh.manager rm -f /var/wazuh-manager/etc/certs/remoted.pem \ + /var/wazuh-manager/etc/certs/remoted-key.pem + docker compose restart wazuh.manager + ``` +- To use your own certificate instead, mount the pair at those paths owned by `101:101` (`wazuh-manager:wazuh-manager`) with mode `640`. The container detects it and does not overwrite it. The manager opens these files after dropping privileges, so any other ownership prevents the HTTPS listener from starting. +- The Wazuh API certificate (`etc/certs/apid.pem` and `apid-key.pem`) is generated by the API on its first start, and is also unique per container. + ## Network exposure - Restrict access to exposed service ports at the host firewall and security group level. diff --git a/docs/ref/upgrade.md b/docs/ref/upgrade.md index effdf23d1..1e3458734 100644 --- a/docs/ref/upgrade.md +++ b/docs/ref/upgrade.md @@ -79,3 +79,17 @@ Below is a step-by-step example of how to perform this update: ```bash docker-compose up -d ``` + +## Manager self-signed certificate on existing deployments + +Manager images built before the per-container certificate change shipped `etc/certs/remoted.pem` and `etc/certs/remoted-key.pem` inside the image, so the pair was copied into the manager `etc` volume the first time the deployment started and is the same in every deployment created from that image. + +Upgrading the image tag does not replace it: the volume already holds a pair, and the container never overwrites an existing one. To move an existing deployment onto a certificate of its own, remove both files and restart the manager after the upgrade: + +```bash +docker compose exec wazuh.manager rm -f /var/wazuh-manager/etc/certs/remoted.pem \ + /var/wazuh-manager/etc/certs/remoted-key.pem +docker compose restart wazuh.manager +``` + +In multi-node, repeat it for `wazuh.master` and `wazuh.worker`. Agents do not validate this certificate by default, so the rotation does not require any change on the agents.