#!/bin/bash

# Wazuh installer
# Copyright (C) 2015, Wazuh Inc.
#
# This program is a free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License (version 2) as published by the FSF - Free Software
# Foundation.
adminpem="/etc/wazuh-indexer/certs/admin.pem"
adminkey="/etc/wazuh-indexer/certs/admin-key.pem"
manager_keystore="/var/wazuh-manager/bin/wazuh-manager-keystore"
readonly base_path="$(dirname "$(readlink -f "$0")")"
readonly config_file="${base_path}/config.yml"
readonly logfile="${base_path}/wazuh-certificates-tool.log"
cert_tmp_path="/tmp/wazuh-certificates"
debug=">> ${logfile} 2>&1"
readonly cert_tool_script_name=".*certs.*\.sh"

# ------------ wazuh-credentials.sh ------------ 
#!/bin/sh

# Shared credential helpers for Wazuh package scripts.
#
# This file is a library. Source it with:
#
#   . /usr/share/wazuh-<component>/lib/wazuh-credentials.sh
#
# Importing it performs no action and does not change the caller's shell
# options, umask, IFS, working directory, or traps.
#
# Public functions and return values
# ----------------------------------
#   wazuh_base_get_dir / wazuh_env_get_file
#       Base: process WAZUH_BASE_DIR or /etc/wazuh. ENV: <base>/credentials.env.
#       The base cannot come from that file (circular lookup). Empty is invalid.
#       Getters do not create anything; writers create missing parents safely.
#       All helpers on a host MUST use the same base to share the same lock.
#   wazuh_ca_get_dir
#       Prints the resolved CA directory. Resolution order is the value in
#       <base>/credentials.env, then WAZUH_CA_DIR from the process
#       environment, with <base>/ca as default. Empty is invalid.
#       WAZUH_CA_DIR moves only the CA, never the ENV or the lock.
#
#   wazuh_ca_validate
#       Returns 0 for a valid anchor, with or without its matching private key.
#       Returns non-zero for an absent, partial, invalid, or insecure CA.
#
#   wazuh_ca_ensure
#       Creates root-ca.pem and root-ca.key only when both are absent. Existing
#       material is validated and never regenerated. An anchor without a key is
#       a valid external CA; a key without an anchor is an error.
#
#   wazuh_env_get NAME
#       Reads data only; unquoted values or single/double quoted single lines.
#       Writes require a newline-terminated file; multiline values are rejected.
#       Prints the last value assigned to NAME in credentials.env without
#       sourcing the file. Returns 0 when found, 1 when absent, and 2 when the
#       file or request is invalid.
#
#   wazuh_env_set NAME VALUE
#       Adds or replaces NAME inside the Wazuh-managed block only.
#
#   wazuh_env_unset NAME
#       Removes NAME from the Wazuh-managed block only.
#
#   wazuh_password_generate
#       Prints a 32-character password generated from /dev/urandom, drawn from
#       the password character set below, with at least one character of each
#       of its four classes.
#
#   wazuh_password_validate VALUE
#       Enforces 12-64 characters drawn only from the password character set
#       A-Z a-z 0-9 . , _ + : @ % ^ = ~ -, with at least one uppercase letter,
#       one lowercase letter, one digit and one symbol. The rejected value is
#       never included in diagnostics.
#
# The library deliberately uses flock(1), GNU stat(1), OpenSSL, awk, and the
# usual Linux userland (including ln -T) on Debian- and RPM-based systems.
# Capture password output; do not use shell xtrace around secret operations.
# Pin BOTH helper files to matching versions: the manager uses private helpers.

_wazuh_error() (
    printf '%s\n' "wazuh-credentials: $*" >&2
)

wazuh_base_get_dir() (
    _wazuh_base=${WAZUH_BASE_DIR-/etc/wazuh}
    _wazuh_validate_absolute_path "$_wazuh_base" || return 1
    printf '%s\n' "$_wazuh_base"
)

wazuh_env_get_file() (
    _wazuh_base=$(wazuh_base_get_dir) || return 1
    printf '%s/credentials.env\n' "$_wazuh_base"
)

# Validate from the root down; never traverse an unchecked symlink first.
# Missing components are allowed by this read-only helper.
_wazuh_check_existing_tree() (
    [ "$1" = / ] && { _wazuh_validate_directory_node / ''; return $?; }
    _wazuh_parent=${1%/*}
    [ -n "$_wazuh_parent" ] || _wazuh_parent=/
    _wazuh_check_existing_tree "$_wazuh_parent" || return 1
    if [ -e "$1" ] || [ -L "$1" ]; then
        _wazuh_validate_directory_node "$1" '' || return 1
    fi
)

# Create one component at a time, never chmod/chown an existing directory.
# Parent validation excludes group/world writers, including sticky /tmp.
_wazuh_make_secure_tree() (
    [ "$(id -u)" = 0 ] || { _wazuh_error 'root is required'; return 1; }
    [ "$1" = / ] && { _wazuh_validate_directory_node / ''; return $?; }
    _wazuh_parent=${1%/*}
    [ -n "$_wazuh_parent" ] || _wazuh_parent=/
    _wazuh_make_secure_tree "$_wazuh_parent" || return 1
    if [ ! -e "$1" ] && [ ! -L "$1" ]; then
        # Another cooperating process may create it first; always revalidate.
        (umask 077; mkdir -m 0700 -- "$1") 2>/dev/null || {
            [ -d "$1" ] || { _wazuh_error "cannot create directory: $1"; return 1; }
        }
    fi
    _wazuh_validate_directory_node "$1" ''
)

_wazuh_validate_name() (
    case ${1-} in
        ''|[0-9]*|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]*)
            _wazuh_error "invalid environment variable name"
            return 1
            ;;
    esac
)

_wazuh_validate_absolute_path() (
    _wazuh_path=${1-}

    case $_wazuh_path in
        ''|/)
            _wazuh_error "directory must be a non-empty absolute path other than /"
            return 1
            ;;
        /*) ;;
        *)
            _wazuh_error "directory must be an absolute path"
            return 1
            ;;
    esac

    case $_wazuh_path in
        *'//'*)
            _wazuh_error "directory must not contain repeated slashes"
            return 1
            ;;
        */./*|*/.|*/../*|*/..)
            _wazuh_error "directory must not contain . or .. path components"
            return 1
            ;;
        */)
            _wazuh_error "directory must not end with a slash"
            return 1
            ;;
    esac

    case $_wazuh_path in
        *"
"*)
            _wazuh_error "directory must not contain a newline"
            return 1
            ;;
    esac
    _wazuh_cr=$(printf '\r')
    case $_wazuh_path in
        *"$_wazuh_cr"*)
            _wazuh_error "directory must not contain a carriage return"
            return 1
            ;;
    esac
)

_wazuh_validate_directory_node() (
    _wazuh_path=${1-}
    _wazuh_exact_mode=${2-}

    if [ -L "$_wazuh_path" ]; then
        _wazuh_error "refusing symbolic-link directory: $_wazuh_path"
        return 1
    fi
    if [ ! -d "$_wazuh_path" ]; then
        _wazuh_error "not a directory: $_wazuh_path"
        return 1
    fi

    _wazuh_owner=$(stat -c '%u:%g' -- "$_wazuh_path" 2>/dev/null) || {
        _wazuh_error "cannot inspect directory: $_wazuh_path"
        return 1
    }
    if [ "$_wazuh_owner" != '0:0' ]; then
        _wazuh_error "directory must be owned by root:root: $_wazuh_path"
        return 1
    fi

    _wazuh_mode=$(stat -c '%a' -- "$_wazuh_path" 2>/dev/null) || return 1
    if [ -n "$_wazuh_exact_mode" ]; then
        if [ "$_wazuh_mode" != "$_wazuh_exact_mode" ]; then
            _wazuh_error "directory $_wazuh_path must have mode $_wazuh_exact_mode (found $_wazuh_mode)"
            return 1
        fi
        return 0
    fi

    _wazuh_world=${_wazuh_mode#${_wazuh_mode%?}}
    _wazuh_prefix=${_wazuh_mode%?}
    _wazuh_group=${_wazuh_prefix#${_wazuh_prefix%?}}
    case $_wazuh_group$_wazuh_world in
        *[2367]*)
            _wazuh_error "directory must not be group- or world-writable: $_wazuh_path"
            return 1
            ;;
    esac
)

_wazuh_validate_ancestors() (
    _wazuh_parent=${1%/*}
    [ -n "$_wazuh_parent" ] || _wazuh_parent=/
    _wazuh_check_existing_tree "$_wazuh_parent" || return 1
    _wazuh_validate_directory_node "$_wazuh_parent" ''
)
_wazuh_validate_regular_file() (
    _wazuh_path=${1-}
    _wazuh_required_mode=${2-}

    if [ -L "$_wazuh_path" ]; then
        _wazuh_error "refusing symbolic-link file: $_wazuh_path"
        return 1
    fi
    if [ ! -f "$_wazuh_path" ]; then
        _wazuh_error "not a regular file: $_wazuh_path"
        return 1
    fi

    _wazuh_owner=$(stat -c '%u:%g' -- "$_wazuh_path" 2>/dev/null) || {
        _wazuh_error "cannot inspect file: $_wazuh_path"
        return 1
    }
    if [ "$_wazuh_owner" != '0:0' ]; then
        _wazuh_error "file must be owned by root:root: $_wazuh_path"
        return 1
    fi

    _wazuh_mode=$(stat -c '%a' -- "$_wazuh_path" 2>/dev/null) || return 1
    if [ "$_wazuh_mode" != "$_wazuh_required_mode" ]; then
        _wazuh_error "file $_wazuh_path must have mode $_wazuh_required_mode (found $_wazuh_mode)"
        return 1
    fi
)

_wazuh_restorecon() (
    _wazuh_path=${1-}
    if command -v restorecon >/dev/null 2>&1; then
        restorecon "$_wazuh_path" >/dev/null 2>&1 || {
            _wazuh_error "failed to restore the SELinux context of $_wazuh_path"
            return 1
        }
    fi
)

_wazuh_validate_base_dir_if_present() (
    _wazuh_base=$(wazuh_base_get_dir) || return 1
    _wazuh_check_existing_tree "$_wazuh_base" || return 1
    if [ -e "$_wazuh_base" ] || [ -L "$_wazuh_base" ]; then
        _wazuh_validate_directory_node "$_wazuh_base" 700 || return 1
    fi
)

_wazuh_ensure_base_dir() (
    _wazuh_base=$(wazuh_base_get_dir) || return 1
    _wazuh_make_secure_tree "$_wazuh_base" || return 1
    _wazuh_validate_directory_node "$_wazuh_base" 700 || return 1
    _wazuh_restorecon "$_wazuh_base"
)

_wazuh_validate_credentials_file() (
    _wazuh_validate_base_dir_if_present || return 1
    _wazuh_file=$(wazuh_env_get_file) || return 1
    _wazuh_validate_regular_file "$_wazuh_file" 600
)

_wazuh_with_lock() (
    _wazuh_ensure_base_dir || return 1
    _wazuh_base=$(wazuh_base_get_dir) || return 1
    _wazuh_lock=$_wazuh_base/.credentials.lock

    if [ -L "$_wazuh_lock" ]; then
        _wazuh_error "refusing symbolic-link lock file: $_wazuh_lock"
        return 1
    fi
    if [ ! -e "$_wazuh_lock" ]; then
        (umask 077; set -C; : >"$_wazuh_lock") 2>/dev/null || {
            [ -e "$_wazuh_lock" ] || return 1
        }
        _wazuh_restorecon "$_wazuh_lock" || return 1
    fi
    _wazuh_validate_regular_file "$_wazuh_lock" 600 || return 1

    exec 9<>"$_wazuh_lock" || {
        _wazuh_error "cannot open lock file: $_wazuh_lock"
        return 1
    }
    flock -x 9 || {
        _wazuh_error "cannot acquire lock: $_wazuh_lock"
        return 1
    }

    "$@"
)

wazuh_env_get() (
    [ "$#" -eq 1 ] || { _wazuh_error 'usage: wazuh_env_get NAME'; return 2; }
    _wazuh_name=${1-}
    _wazuh_validate_name "$_wazuh_name" || return 2
    _wazuh_file=$(wazuh_env_get_file) || return 2
    if [ ! -e "$_wazuh_file" ] && [ ! -L "$_wazuh_file" ]; then
        _wazuh_validate_base_dir_if_present || return 2
        return 1
    fi
    _wazuh_validate_credentials_file || return 2

    awk -v wanted="$_wazuh_name" '
        function trim_left(s)  { sub(/^[[:space:]]+/, "", s); return s }
        function trim_right(s) { sub(/[[:space:]]+$/, "", s); return s }
        function decode_double(s,    out, i, c, n) {
            out = ""
            for (i = 1; i <= length(s); i++) {
                c = substr(s, i, 1)
                if (c == "\\" && i < length(s)) {
                    n = substr(s, i + 1, 1)
                    if (n == "\\" || n == "\"" || n == "$" || n == "`") {
                        out = out n
                        i++
                        continue
                    }
                }
                out = out c
            }
            return out
        }
        {
            line = trim_left($0)
            if (line == "" || substr(line, 1, 1) == "#")
                next

            equal = index(line, "=")
            if (!equal)
                next

            name = trim_right(substr(line, 1, equal - 1))
            if (name != wanted)
                next

            value = trim_left(substr(line, equal + 1))
            value = trim_right(value)
            if ((substr(value, 1, 1) == "\047" && substr(value, length(value), 1) != "\047") ||
                (substr(value, 1, 1) == "\"" && substr(value, length(value), 1) != "\"")) {
                bad = 1
                next
            }
            if (length(value) >= 2 && substr(value, 1, 1) == "\047" &&
                substr(value, length(value), 1) == "\047") {
                value = substr(value, 2, length(value) - 2)
            } else if (length(value) >= 2 && substr(value, 1, 1) == "\"" &&
                       substr(value, length(value), 1) == "\"") {
                value = decode_double(substr(value, 2, length(value) - 2))
            }
            result = value
            found = 1
        }
        END {
            if (!found)
                exit (bad ? 2 : 1)
            if (bad) exit 2
            print result
        }
    ' "$_wazuh_file"
)

_wazuh_env_mutate_locked() (
    _wazuh_action=${1-}
    _wazuh_name=${2-}
    _wazuh_value_file=${3-}
    _wazuh_file=$(wazuh_env_get_file) || return 1
    _wazuh_base=$(wazuh_base_get_dir) || return 1

    if [ -e "$_wazuh_file" ] || [ -L "$_wazuh_file" ]; then
        _wazuh_validate_credentials_file || return 1
        if [ -s "$_wazuh_file" ] && [ "$(tail -c 1 -- "$_wazuh_file" | od -An -tu1 | tr -d '[:space:]')" != 10 ]; then
            _wazuh_error 'credentials.env must end with a newline; refusing to alter operator bytes'
            return 1
        fi
        _wazuh_source=$_wazuh_file
    else
        if [ "$_wazuh_action" = unset ]; then
            return 0
        fi
        _wazuh_source=/dev/null
    fi

    _wazuh_tmp=$(mktemp "$_wazuh_base/.credentials.env.XXXXXX") || {
        _wazuh_error 'cannot create a temporary credentials file'
        return 1
    }
    trap 'rm -f -- "$_wazuh_tmp"' 0
    trap 'return 130' 1 2 3 15

    if ! awk -v action="$_wazuh_action" -v wanted="$_wazuh_name" \
             -v value_file="$_wazuh_value_file" '
        function lhs_name(line,    equal, lhs) {
            sub(/^[[:space:]]+/, "", line)
            if (substr(line, 1, 1) == "#")
                return ""
            equal = index(line, "=")
            if (!equal)
                return ""
            lhs = substr(line, 1, equal - 1)
            sub(/[[:space:]]+$/, "", lhs)
            return lhs
        }
        function quote_value(s,    out, i, c) {
            # The password alphabet needs no quoting; anything else is still escaped.
            if (s ~ /^[A-Za-z0-9.,_+:@%^=~-]*$/)
                return s
            out = "\""
            for (i = 1; i <= length(s); i++) {
                c = substr(s, i, 1)
                if (c == "\\" || c == "\"" || c == "$" || c == "`")
                    out = out "\\"
                out = out c
            }
            return out "\""
        }
        function print_header(    i) {
            for (i = 1; i <= header_lines; i++)
                print header[i]
        }
        BEGIN {
            begin_marker = "# >>> wazuh generated — do not edit <<<"
            end_marker = "# >>> end wazuh generated <<<"
            # The header follows the begin marker. Every header line, and the ones of earlier
            # versions, is dropped from the block and printed again, so it is never duplicated.
            header_lines = split("" \
                "# Written by the Wazuh packages and tools. Editing a value here changes nothing.\n" \
                "# To change a password: wazuh-passwords-tool.sh -u <user>\n" \
                "# A host only holds the keys of the components installed on it.\n" \
                "#\n" \
                "#                 User           Key                                  Used for\n" \
                "# Logins:\n" \
                "#                 admin          WAZUH_INDEXER_ADMIN_PASSWORD         Wazuh dashboard (web UI) and Wazuh indexer API\n" \
                "#                 wazuh          WAZUH_MANAGER_API_PASSWORD           Wazuh server API (curl, scripts)\n" \
                "# Service accounts the components connect with, not logins:\n" \
                "#                 kibanaserver   WAZUH_INDEXER_KIBANASERVER_PASSWORD  dashboard to indexer\n" \
                "#                 wazuh-manager  WAZUH_INDEXER_MANAGER_PASSWORD       manager to indexer\n" \
                "#                 wazuh-wui      WAZUH_MANAGER_WUI_PASSWORD           dashboard to server API\n" \
                "#", header, "\n")
            for (i = 1; i <= header_lines; i++)
                known_header[header[i]] = 1
            known_header["# Editing a value here does not change the deployment."] = 1
            known_header["# To rotate, use wazuh-passwords-tool.sh."] = 1
            known_header["# admin: login of the Wazuh dashboard and administrator of the indexer"] = 1
            value = ""
            if (action == "set") {
                read_status = (getline value < value_file)
                close(value_file)
                if (read_status < 0) {
                    print "wazuh-credentials: cannot read temporary value" > "/dev/stderr"
                    exit 42
                }
                assignment = wanted "=" quote_value(value)
            }
        }
        $0 == begin_marker {
            if (inside || begin_count > 0) {
                bad = 1
                next
            }
            begin_count++
            inside = 1
            print
            print_header()
            next
        }
        $0 == end_marker {
            if (!inside || end_count > 0) {
                bad = 1
                next
            }
            if (action == "set" && !written) {
                print assignment
                written = 1
            }
            end_count++
            inside = 0
            print
            next
        }
        {
            if (inside && ($0 in known_header))
                next
            if (inside && lhs_name($0) == wanted) {
                if (action == "set" && !written) {
                    print assignment
                    written = 1
                }
                next
            }
            print
        }
        END {
            if (inside || begin_count != end_count || begin_count > 1 || bad) {
                print "wazuh-credentials: malformed Wazuh-managed block" > "/dev/stderr"
                exit 42
            }
            if (begin_count == 0 && action == "set") {
                if (NR > 0)
                    print ""
                print begin_marker
                print_header()
                print assignment
                print end_marker
            }
        }
    ' "$_wazuh_source" >"$_wazuh_tmp"; then
        _wazuh_error 'credentials file was not modified'
        return 1
    fi

    chown root:root "$_wazuh_tmp" || return 1
    chmod 0600 "$_wazuh_tmp" || return 1
    mv -f -- "$_wazuh_tmp" "$_wazuh_file" || {
        _wazuh_error 'cannot replace credentials.env atomically'
        return 1
    }
    trap - 0 1 2 3 15
    _wazuh_restorecon "$_wazuh_file" || return 1
    _wazuh_validate_credentials_file
)

wazuh_env_set() (
    if [ "$#" -ne 2 ]; then
        _wazuh_error 'usage: wazuh_env_set NAME VALUE'
        return 1
    fi
    _wazuh_name=$1
    _wazuh_value=$2
    _wazuh_validate_name "$_wazuh_name" || return 1

    case $_wazuh_value in
        *"
"*)
            _wazuh_error "value for $_wazuh_name must not contain a newline"
            return 1
            ;;
    esac
    _wazuh_cr=$(printf '\r')
    case $_wazuh_value in
        *"$_wazuh_cr"*)
            _wazuh_error "value for $_wazuh_name must not contain a carriage return"
            return 1
            ;;
    esac

    _wazuh_ensure_base_dir || return 1
    _wazuh_base=$(wazuh_base_get_dir) || return 1
    _wazuh_value_file=$(mktemp "$_wazuh_base/.credential-value.XXXXXX") || {
        _wazuh_error 'cannot create a temporary value file'
        return 1
    }
    trap 'rm -f -- "$_wazuh_value_file"' 0
    trap 'return 130' 1 2 3 15
    chmod 0600 "$_wazuh_value_file" || return 1
    printf '%s' "$_wazuh_value" >"$_wazuh_value_file" || return 1

    _wazuh_with_lock _wazuh_env_mutate_locked set "$_wazuh_name" "$_wazuh_value_file"
)

wazuh_env_unset() (
    if [ "$#" -ne 1 ]; then
        _wazuh_error 'usage: wazuh_env_unset NAME'
        return 1
    fi
    _wazuh_validate_name "$1" || return 1
    _wazuh_with_lock _wazuh_env_mutate_locked unset "$1" ''
)

wazuh_ca_get_dir() (
    _wazuh_ca_dir=
    _wazuh_ca_dir_is_set=0

    _wazuh_status=0
    _wazuh_file_value=$(wazuh_env_get WAZUH_CA_DIR) || _wazuh_status=$?
    case $_wazuh_status in
        0)
            _wazuh_ca_dir=$_wazuh_file_value
            _wazuh_ca_dir_is_set=1
            ;;
        1) ;;
        *) return 1 ;;
    esac

    if [ "${WAZUH_CA_DIR+x}" = x ]; then
        _wazuh_ca_dir=${WAZUH_CA_DIR-}
        _wazuh_ca_dir_is_set=1
    fi

    if [ "$_wazuh_ca_dir_is_set" -eq 0 ]; then
        _wazuh_base=$(wazuh_base_get_dir) || return 1
        _wazuh_ca_dir=$_wazuh_base/ca
    fi

    _wazuh_validate_absolute_path "$_wazuh_ca_dir" || return 1
    printf '%s\n' "$_wazuh_ca_dir"
)

_wazuh_validate_ca_files() (
    _wazuh_ca_dir=${1-}
    _wazuh_cert=$_wazuh_ca_dir/root-ca.pem
    _wazuh_key=$_wazuh_ca_dir/root-ca.key

    _wazuh_validate_ancestors "$_wazuh_ca_dir" || return 1
    _wazuh_validate_directory_node "$_wazuh_ca_dir" 700 || return 1

    _wazuh_cert_exists=0
    _wazuh_key_exists=0
    if [ -e "$_wazuh_cert" ] || [ -L "$_wazuh_cert" ]; then
        _wazuh_cert_exists=1
    fi
    if [ -e "$_wazuh_key" ] || [ -L "$_wazuh_key" ]; then
        _wazuh_key_exists=1
    fi

    if [ "$_wazuh_cert_exists" -eq 0 ] && [ "$_wazuh_key_exists" -eq 0 ]; then
        _wazuh_error "CA material is absent from $_wazuh_ca_dir"
        return 1
    fi
    if [ "$_wazuh_cert_exists" -eq 0 ]; then
        _wazuh_error "root-ca.key exists without root-ca.pem in $_wazuh_ca_dir"
        return 1
    fi

    _wazuh_validate_regular_file "$_wazuh_cert" 644 || return 1
    if ! openssl x509 -in "$_wazuh_cert" -noout >/dev/null 2>&1; then
        _wazuh_error "invalid X.509 certificate: $_wazuh_cert"
        return 1
    fi
    if ! openssl x509 -in "$_wazuh_cert" -checkend 0 -noout >/dev/null 2>&1; then
        _wazuh_error "expired X.509 certificate: $_wazuh_cert"
        return 1
    fi
    _wazuh_text=$(LC_ALL=C openssl x509 -in "$_wazuh_cert" -noout -text 2>/dev/null) || return 1
    case $_wazuh_text in
        *'CA:TRUE'*) ;;
        *) _wazuh_error "certificate is not a CA: $_wazuh_cert"; return 1 ;;
    esac
    openssl verify -CAfile "$_wazuh_cert" "$_wazuh_cert" >/dev/null 2>&1 || {
        _wazuh_error "CA certificate is not currently valid: $_wazuh_cert"
        return 1
    }

    # Anchor-only is the expected state for an externally managed CA.
    if [ "$_wazuh_key_exists" -eq 0 ]; then
        return 0
    fi

    _wazuh_validate_regular_file "$_wazuh_key" 400 || return 1
    if ! openssl pkey -in "$_wazuh_key" -passin pass: -check -noout </dev/null >/dev/null 2>&1; then
        _wazuh_error "invalid private key: $_wazuh_key"
        return 1
    fi

    _wazuh_cert_pub=$(mktemp "$_wazuh_ca_dir/.cert-pub.XXXXXX") || return 1
    _wazuh_key_pub=$(mktemp "$_wazuh_ca_dir/.key-pub.XXXXXX") || {
        rm -f -- "$_wazuh_cert_pub"
        return 1
    }
    trap 'rm -f -- "$_wazuh_cert_pub" "$_wazuh_key_pub"' 0
    trap 'return 130' 1 2 3 15
    chmod 0600 "$_wazuh_cert_pub" "$_wazuh_key_pub" || return 1

    openssl x509 -in "$_wazuh_cert" -pubkey -noout >"$_wazuh_cert_pub" 2>/dev/null || return 1
    openssl pkey -in "$_wazuh_key" -passin pass: -pubout </dev/null >"$_wazuh_key_pub" 2>/dev/null || return 1
    if ! cmp -s -- "$_wazuh_cert_pub" "$_wazuh_key_pub"; then
        _wazuh_error 'root-ca.key does not match root-ca.pem'
        return 1
    fi

    rm -f -- "$_wazuh_cert_pub" "$_wazuh_key_pub"
    trap - 0 1 2 3 15
)

_wazuh_ensure_ca_directory() (
    _wazuh_ca_dir=${1-}
    _wazuh_validate_absolute_path "$_wazuh_ca_dir" || return 1

    if [ -L "$_wazuh_ca_dir" ]; then
        _wazuh_error "refusing symbolic-link CA directory: $_wazuh_ca_dir"
        return 1
    fi
    if [ ! -e "$_wazuh_ca_dir" ]; then
        _wazuh_make_secure_tree "$_wazuh_ca_dir" || {
            _wazuh_error "cannot create CA directory: $_wazuh_ca_dir"
            return 1
        }
        _wazuh_restorecon "$_wazuh_ca_dir" || return 1
    fi

    _wazuh_validate_ancestors "$_wazuh_ca_dir" || return 1
    _wazuh_validate_directory_node "$_wazuh_ca_dir" 700
)

_wazuh_ca_validate_locked() (
    _wazuh_ca_dir=$(wazuh_ca_get_dir) || return 1
    _wazuh_validate_ca_files "$_wazuh_ca_dir"
)

wazuh_ca_validate() (
    _wazuh_with_lock _wazuh_ca_validate_locked
)

_wazuh_ca_ensure_locked() (
    _wazuh_ca_dir=$(wazuh_ca_get_dir) || return 1
    _wazuh_ensure_ca_directory "$_wazuh_ca_dir" || return 1

    _wazuh_cert=$_wazuh_ca_dir/root-ca.pem
    _wazuh_key=$_wazuh_ca_dir/root-ca.key

    _wazuh_cert_exists=0
    _wazuh_key_exists=0
    if [ -e "$_wazuh_cert" ] || [ -L "$_wazuh_cert" ]; then
        _wazuh_cert_exists=1
    fi
    if [ -e "$_wazuh_key" ] || [ -L "$_wazuh_key" ]; then
        _wazuh_key_exists=1
    fi

    if [ "$_wazuh_cert_exists" -eq 1 ] || [ "$_wazuh_key_exists" -eq 1 ]; then
        _wazuh_validate_ca_files "$_wazuh_ca_dir"
        return $?
    fi

    command -v openssl >/dev/null 2>&1 || {
        _wazuh_error 'openssl is required to generate the root CA'
        return 1
    }

    _wazuh_tmp_dir=$(mktemp -d "$_wazuh_ca_dir/.root-ca.XXXXXX") || {
        _wazuh_error "cannot create a temporary directory in $_wazuh_ca_dir"
        return 1
    }
    trap 'rm -rf -- "$_wazuh_tmp_dir"' 0
    trap 'return 130' 1 2 3 15
    chmod 0700 "$_wazuh_tmp_dir" || return 1
    _wazuh_config=$_wazuh_tmp_dir/openssl.cnf

    (umask 077; printf '%s\n' \
        '[req]' \
        'distinguished_name = dn' \
        'x509_extensions = v3_ca' \
        'prompt = no' \
        '[dn]' \
        'OU = Wazuh' \
        'O = Wazuh' \
        'L = California' \
        '[v3_ca]' \
        'basicConstraints = critical, CA:TRUE' \
        'keyUsage = critical, keyCertSign, cRLSign' \
        'subjectKeyIdentifier = hash' \
        'authorityKeyIdentifier = keyid:always' \
        >"$_wazuh_config") || return 1

    if ! (umask 077; openssl req -x509 -new -nodes -newkey rsa:2048 \
        -sha256 -days 3650 -batch -config "$_wazuh_config" \
        -keyout "$_wazuh_tmp_dir/root-ca.key" \
        -out "$_wazuh_tmp_dir/root-ca.pem" >/dev/null 2>&1); then
        _wazuh_error 'OpenSSL failed to generate the root CA'
        return 1
    fi

    chown root:root "$_wazuh_tmp_dir/root-ca.key" "$_wazuh_tmp_dir/root-ca.pem" || return 1
    chmod 0400 "$_wazuh_tmp_dir/root-ca.key" || return 1
    chmod 0644 "$_wazuh_tmp_dir/root-ca.pem" || return 1

    # Publishing the key first (hard links refuse existing targets) makes an interrupted installation fail closed as an
    # explicit key-without-certificate state instead of silently looking like
    # an externally managed anchor.
    ln -T -- "$_wazuh_tmp_dir/root-ca.key" "$_wazuh_key" || return 1
    ln -T -- "$_wazuh_tmp_dir/root-ca.pem" "$_wazuh_cert" || return 1
    _wazuh_restorecon "$_wazuh_key" || return 1
    _wazuh_restorecon "$_wazuh_cert" || return 1

    rm -rf -- "$_wazuh_tmp_dir"
    trap - 0 1 2 3 15
    _wazuh_validate_ca_files "$_wazuh_ca_dir"
)

wazuh_ca_ensure() (
    _wazuh_with_lock _wazuh_ca_ensure_locked
)

_wazuh_random_below() (
    _wazuh_limit=${1-}
    case $_wazuh_limit in
        ''|*[!0-9]*|0) return 1 ;;
    esac

    _wazuh_ceiling=$((65536 - (65536 % _wazuh_limit)))
    while :; do
        _wazuh_number=$(od -An -N2 -tu2 /dev/urandom 2>/dev/null) || return 1
        _wazuh_number=$(printf '%s' "$_wazuh_number" | tr -d '[:space:]')
        case $_wazuh_number in
            ''|*[!0-9]*) return 1 ;;
        esac
        if [ "$_wazuh_number" -lt "$_wazuh_ceiling" ]; then
            printf '%s\n' $((_wazuh_number % _wazuh_limit))
            return 0
        fi
    done
)

_wazuh_random_char() (
    _wazuh_alphabet=${1-}
    _wazuh_length=${#_wazuh_alphabet}
    [ "$_wazuh_length" -gt 0 ] || return 1
    _wazuh_index=$(_wazuh_random_below "$_wazuh_length") || return 1
    _wazuh_position=$((_wazuh_index + 1))
    printf '%s' "$_wazuh_alphabet" | cut -c "$_wazuh_position"
)

_wazuh_replace_char() (
    _wazuh_text=${1-}
    _wazuh_position=${2-}
    _wazuh_character=${3-}

    if [ "$_wazuh_position" -le 1 ]; then
        _wazuh_prefix=
    else
        _wazuh_prefix=$(printf '%s' "$_wazuh_text" | cut -c "1-$((_wazuh_position - 1))")
    fi
    _wazuh_suffix=$(printf '%s' "$_wazuh_text" | cut -c "$((_wazuh_position + 1))-" )
    printf '%s%s%s' "$_wazuh_prefix" "$_wazuh_character" "$_wazuh_suffix"
)

wazuh_password_generate() (
    # The password character set; wazuh_password_validate accepts exactly this.
    _wazuh_alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,_+:@%^=~-'

    # Finite entropy reads avoid SIGPIPE under a caller's pipefail. Rejection
    # sampling (bytes < 219 for a 73-character alphabet) avoids modulo bias.
    _wazuh_password=
    while [ "${#_wazuh_password}" -lt 32 ]; do
        _wazuh_bytes=$(od -An -v -N128 -tu1 /dev/urandom) || return 1
        [ -n "$_wazuh_bytes" ] || return 1
        _wazuh_chunk=$(printf '%s\n' "$_wazuh_bytes" | awk -v a="$_wazuh_alphabet" '
            { for (i=1; i<=NF; i++) if ($i < 219) printf "%s", substr(a, ($i % 73)+1, 1) }
        ') || return 1
        _wazuh_password=$_wazuh_password$_wazuh_chunk
    done
    _wazuh_password=$(printf '%s' "$_wazuh_password" | cut -c 1-32) || return 1

    _wazuh_index=$(_wazuh_random_below 32) || return 1
    _wazuh_pos_lower=$((_wazuh_index + 1))
    while :; do
        _wazuh_index=$(_wazuh_random_below 32) || return 1
        _wazuh_pos_upper=$((_wazuh_index + 1))
        [ "$_wazuh_pos_upper" -ne "$_wazuh_pos_lower" ] && break
    done
    while :; do
        _wazuh_index=$(_wazuh_random_below 32) || return 1
        _wazuh_pos_digit=$((_wazuh_index + 1))
        if [ "$_wazuh_pos_digit" -ne "$_wazuh_pos_lower" ] &&
           [ "$_wazuh_pos_digit" -ne "$_wazuh_pos_upper" ]; then
            break
        fi
    done
    while :; do
        _wazuh_index=$(_wazuh_random_below 32) || return 1
        _wazuh_pos_symbol=$((_wazuh_index + 1))
        if [ "$_wazuh_pos_symbol" -ne "$_wazuh_pos_lower" ] &&
           [ "$_wazuh_pos_symbol" -ne "$_wazuh_pos_upper" ] &&
           [ "$_wazuh_pos_symbol" -ne "$_wazuh_pos_digit" ]; then
            break
        fi
    done

    _wazuh_char=$(_wazuh_random_char 'abcdefghijklmnopqrstuvwxyz') || return 1
    _wazuh_password=$(_wazuh_replace_char "$_wazuh_password" "$_wazuh_pos_lower" "$_wazuh_char") || return 1
    _wazuh_char=$(_wazuh_random_char 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') || return 1
    _wazuh_password=$(_wazuh_replace_char "$_wazuh_password" "$_wazuh_pos_upper" "$_wazuh_char") || return 1
    _wazuh_char=$(_wazuh_random_char '0123456789') || return 1
    _wazuh_password=$(_wazuh_replace_char "$_wazuh_password" "$_wazuh_pos_digit" "$_wazuh_char") || return 1
    _wazuh_char=$(_wazuh_random_char '.,_+:@%^=~-') || return 1
    _wazuh_password=$(_wazuh_replace_char "$_wazuh_password" "$_wazuh_pos_symbol" "$_wazuh_char") || return 1

    wazuh_password_validate "$_wazuh_password" >/dev/null || return 1
    printf '%s\n' "$_wazuh_password"
)

wazuh_password_validate() (
    if [ "$#" -ne 1 ]; then
        _wazuh_error 'usage: wazuh_password_validate VALUE'
        return 1
    fi
    _wazuh_password=$1

    # Only the generator's character set, spelled out rather than as ranges so
    # the match does not depend on the locale. It is checked first because it
    # makes every accepted value ASCII: the length below then means the same
    # under dash (bytes) as under bash and the consumers (characters). It also
    # excludes whitespace, control characters (newline and carriage return
    # included), quotes and the characters that need escaping in the file, in
    # a shell or in an HTTP Basic credential.
    case $_wazuh_password in
        *[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,_+:@%^=~-]*)
            _wazuh_error 'password must only contain characters from A-Z a-z 0-9 . , _ + : @ % ^ = ~ -'
            return 1
            ;;
    esac

    _wazuh_length=${#_wazuh_password}
    if [ "$_wazuh_length" -lt 12 ] || [ "$_wazuh_length" -gt 64 ]; then
        _wazuh_error 'password must contain between 12 and 64 characters'
        return 1
    fi
    case $_wazuh_password in
        *[abcdefghijklmnopqrstuvwxyz]*) ;;
        *)
            _wazuh_error 'password must contain at least one lowercase letter'
            return 1
            ;;
    esac
    case $_wazuh_password in
        *[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*) ;;
        *)
            _wazuh_error 'password must contain at least one uppercase letter'
            return 1
            ;;
    esac
    case $_wazuh_password in
        *[0123456789]*) ;;
        *)
            _wazuh_error 'password must contain at least one digit'
            return 1
            ;;
    esac
    case $_wazuh_password in
        *[.,_+:@%^=~-]*) ;;
        *)
            _wazuh_error 'password must contain at least one symbol from . , _ + : @ % ^ = ~ -'
            return 1
            ;;
    esac
)
# ------------ end of wazuh-credentials.sh ------------ 

# ------------ certFunctions.sh ------------ 
function cert_validatePath() {
    local path="$1"
    local path_type="${2:-file}"

    # Check if path is empty
    if [[ -z "${path}" ]]; then
        common_logger -e "Path cannot be empty."
        return 1
    fi

    # Prevent path traversal attacks - reject paths with suspicious patterns
    if [[ "${path}" =~ \.\./|\.\.\\ ]]; then
        common_logger -e "Path traversal detected in: ${path}"
        return 1
    fi

    # Reject paths with newlines, carriage returns, or tabs (specific problematic characters)
    if [[ "${path}" =~ $'\n'|$'\r'|$'\t' ]]; then
        common_logger -e "Invalid characters detected in path: ${path}"
        return 1
    fi

    # For absolute paths validation
    if [[ "${path}" == /* ]]; then
        # Resolve to canonical path to prevent symlink attacks
        if command -v realpath >/dev/null 2>&1; then
            local canonical_path
            canonical_path=$(realpath -m "${path}" 2>/dev/null) || return 1

            # Ensure the canonical path doesn't escape expected boundaries
            if [[ ! "${canonical_path}" =~ ^/[a-zA-Z0-9/_.\-]+$ ]]; then
                common_logger -e "Invalid canonical path: ${canonical_path}"
                return 1
            fi
        fi
    fi

    return 0
}
function cert_sanitizeFilename() {
    local filename="$1"

    # Remove any path components
    filename="${filename##*/}"

    # Only allow alphanumeric, dash, underscore, and dot
    filename=$(echo "${filename}" | sed 's/[^a-zA-Z0-9._-]/_/g')

    # Prevent hidden files
    filename="${filename#.}"

    # Limit length to 255 characters
    if [[ ${#filename} -gt 255 ]]; then
        filename="${filename:0:255}"
    fi

    echo "${filename}"
}
function cert_sanitizeNodeName() {
    local component_name="$1"
    local node_names_var="$2"

    # Use nameref for safe dynamic array access
    declare -n component_node_names="${node_names_var}"

    for i in "${!component_node_names[@]}"; do

        # Only allow alphanumeric, dash, underscore, and dot (typical for hostnames)
        if [[ ! "${component_node_names[$i]}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
            common_logger -e "Invalid ${component_name} node name: ${component_node_names[$i]}. Only alphanumeric characters, dots, dashes, and underscores are allowed."
            exit 1
        fi

        # Prevent names starting with dash or dot
        if [[ "${component_node_names[$i]}" =~ ^[-\.] ]]; then
            common_logger -e "${component_name} node name cannot start with dash or dot: ${component_node_names[$i]}"
            exit 1
        fi

        # Limit length
        if [[ ${#component_node_names[$i]} -gt 253 ]]; then
            common_logger -e "${component_name} node name too long: ${component_node_names[$i]}"
            exit 1
        fi
    done

    return 0
}
function cert_cleanFiles() {

    # Validate cert_tmp_path before use
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    # Remove files
    rm -f "${cert_tmp_path}"/*.csr
    rm -f "${cert_tmp_path}"/*.srl
    rm -f "${cert_tmp_path}"/*.conf
    rm -f "${cert_tmp_path}"/admin-key-temp.pem
    # The root CA key never leaves the CA directory.
    rm -f "${cert_tmp_path}"/root-ca.key
    # Single-use CA workspaces of the server leaves, in case one was left behind.
    for workspace in "${cert_tmp_path}"/*-ca; do
        if [ -d "${workspace}" ]; then
            rm -rf "${workspace}"
        fi
    done

}
function cert_checkOpenSSL() {

    local missing=()

    common_logger -d "Checking if OpenSSL and the commands the root CA needs are installed."

    if [ -z "$(command -v openssl)" ]; then
        missing+=("openssl (package openssl)")
    fi
    if [ -z "$(command -v cmp)" ]; then
        missing+=("cmp (package diffutils)")
    fi
    if [ -z "$(command -v flock)" ]; then
        missing+=("flock (package util-linux)")
    fi
    if ! stat -c '%a' / > /dev/null 2>&1; then
        missing+=("GNU stat (package coreutils)")
    fi
    if ! ln --help 2>&1 | grep -q -- '-T'; then
        missing+=("GNU ln (package coreutils)")
    fi

    if [ "${#missing[@]}" -gt 0 ]; then
        common_logger -e "The following commands are required and could not be found: $(IFS=','; echo "${missing[*]}" | sed 's/,/, /g'). Install them and run the tool again."
        exit 1
    fi

}
function cert_checkRootCA() {

    local mode="${1:-validate}"

    common_logger -d "Checking the root CA."

    if [ "${mode}" == "create" ]; then
        cert_generateRootCAcertificate
    else
        if ! cert_ca_dir=$(wazuh_ca_get_dir); then
            common_logger -e "Could not resolve the root CA directory."
            cert_cleanFiles
            exit 1
        fi
        if ! wazuh_ca_validate; then
            common_logger -e "There is no valid root CA in ${cert_ca_dir}. Create it with -ca|--root-ca-certificates, or set WAZUH_CA_DIR to the directory of an existing one."
            cert_cleanFiles
            exit 1
        fi
    fi

    rootca="${cert_ca_dir}/root-ca.pem"
    rootcakey="${cert_ca_dir}/root-ca.key"

    if [ ! -f "${rootcakey}" ]; then
        common_logger -e "The root CA in ${cert_ca_dir} has no private key (root-ca.key), so it cannot sign certificates. Run the tool on the host that holds the key."
        cert_cleanFiles
        exit 1
    fi

    if ! cp "${rootca}" "${cert_tmp_path}/root-ca.pem"; then
        common_logger -e "Could not copy ${rootca} to ${cert_tmp_path}."
        cert_cleanFiles
        exit 1
    fi

}
function cert_executeAndValidate() {

    command_output=$("$@" 2>&1)
    e_code="${PIPESTATUS[0]}"

    if [ "${e_code}" -ne 0 ]; then
        common_logger -e "Error generating the certificates."
        common_logger -d "Error executing command: $@"
        common_logger -d "Error output: ${command_output}"
        cert_cleanFiles
        exit 1
    fi

}
function cert_generateAdmincertificate() {

    common_logger "Generating Admin certificates."

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    common_logger -d "Generating Admin private key."
    cert_executeAndValidate openssl genrsa -out "${cert_tmp_path}/admin-key-temp.pem" 2048
    common_logger -d "Converting Admin private key to PKCS8 format."
    cert_executeAndValidate openssl pkcs8 -inform PEM -outform PEM -in "${cert_tmp_path}/admin-key-temp.pem" -topk8 -nocrypt -v1 PBE-SHA1-3DES -out "${cert_tmp_path}/admin-key.pem"
    common_logger -d "Generating Admin CSR."
    # Same subject order as the Wazuh indexer package, which writes this DN in admin_dn.
    cert_executeAndValidate openssl req -new -key "${cert_tmp_path}/admin-key.pem" -out "${cert_tmp_path}/admin.csr" -batch -subj '/CN=admin/OU=Wazuh/O=Wazuh/L=California/C=US'

    # The admin certificate carries no SAN: it is never dialled, it authenticates
    # against the indexer security API. Hence an extension file of its own rather
    # than cert_generateCertificateconfiguration, which builds a SAN.
    cat > "${cert_tmp_path}/admin.conf" <<- EOF
        [ v3_admin ]
        authorityKeyIdentifier=keyid,issuer
        basicConstraints = critical, CA:FALSE
        keyUsage = critical, digitalSignature, keyEncipherment
        extendedKeyUsage = clientAuth
	EOF

    common_logger -d "Creating Admin certificate."
    cert_executeAndValidate openssl x509 -days 3650 -req -in "${cert_tmp_path}/admin.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -sha256 -out "${cert_tmp_path}/admin.pem" -extfile "${cert_tmp_path}/admin.conf" -extensions v3_admin

}
function cert_generateCertificateconfiguration() {

    common_logger -d "Generating certificate configuration."

    local node_name="$1"
    local extended_key_usage="$2"

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    if [ -z "${extended_key_usage}" ]; then
        common_logger -e "No extendedKeyUsage specified for ${node_name}."
        exit 1
    fi

    # Subject order of the Wazuh manager and dashboard packages. With cert_cn_first set, the
    # order of the Wazuh indexer package, which writes that DN in nodes_dn and admin_dn.
    local subject="C = US
        L = California
        O = Wazuh
        OU = Wazuh
        CN = cname"
    if [ -n "${cert_cn_first}" ]; then
        subject="CN = cname
        OU = Wazuh
        O = Wazuh
        L = California
        C = US"
    fi

    cat > "${cert_tmp_path}/${node_name}.conf" <<- EOF
        [ req ]
        prompt = no
        default_bits = 2048
        default_md = sha256
        distinguished_name = req_distinguished_name
        x509_extensions = v3_req

        [req_distinguished_name]
        ${subject}

        [ v3_req ]
        authorityKeyIdentifier=keyid,issuer
        basicConstraints = critical, CA:FALSE
        keyUsage = critical, digitalSignature, keyEncipherment
        extendedKeyUsage = ekusage
        subjectAltName = @alt_names

        [alt_names]
        IP.1 = cip
	EOF


    conf="$(awk '{sub("CN = cname", "CN = '"${node_name}"'"); sub("extendedKeyUsage = ekusage", "extendedKeyUsage = '"${extended_key_usage}"'")}1' "${cert_tmp_path}/${node_name}.conf")"
    echo "${conf}" > "${cert_tmp_path}/${node_name}.conf"

    if [ "${#@}" -gt 2 ]; then
        sed -i '/IP.1/d' "${cert_tmp_path}/${node_name}.conf"
        local ip_counter=0
        local dns_counter=0
        for (( i=3; i<=${#@}; i++ )); do
            if cert_isIP "${!i}"; then
                ip_counter=$((ip_counter+1))
                printf '%s\n' "        IP.${ip_counter} = ${!i}" >> "${cert_tmp_path}/${node_name}.conf"
            elif cert_isDNS "${!i}"; then
                dns_counter=$((dns_counter+1))
                printf '%s\n' "        DNS.${dns_counter} = ${!i}" >> "${cert_tmp_path}/${node_name}.conf"
            else
                common_logger -e "Invalid IP or DNS ${!i}"
                exit 1
            fi
        done
    else
        common_logger -e "No IP or DNS specified"
        exit 1
    fi

}
function cert_generateIndexercertificates() {

    if [ ${#indexer_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh indexer certificates."

        for i in "${!indexer_node_names[@]}"; do
            indexer_node_name=${indexer_node_names[$i]}

            common_logger -d "Creating the certificates for ${indexer_node_name} indexer node."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n idx_ip="indexer_node_ip_${j}"
            declare -n idx_dns="indexer_node_dns_${j}"
            declare -a idx_san=()
            if [ "${#idx_ip[@]}" -gt 0 ]; then
                idx_san+=("${idx_ip[@]}")
            fi
            if [ "${#idx_dns[@]}" -gt 0 ]; then
                idx_san+=("${idx_dns[@]}")
            fi
            cert_cn_first=1 cert_generateCertificateconfiguration "${indexer_node_name}" "serverAuth, clientAuth" "${idx_san[@]}"
            common_logger -d "Creating the Wazuh indexer tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${indexer_node_name}-key.pem" -out "${cert_tmp_path}/${indexer_node_name}.csr" -config "${cert_tmp_path}/${indexer_node_name}.conf"
            common_logger -d "Creating the Wazuh indexer certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${indexer_node_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${indexer_node_name}.pem" -extfile "${cert_tmp_path}/${indexer_node_name}.conf" -extensions v3_req -days 3650
        done
    else
        return 1
    fi

}
function cert_generateManagercertificates() {

    if [ ${#manager_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh manager certificates."

        for i in "${!manager_node_names[@]}"; do
            manager_name="${manager_node_names[i]}"

            common_logger -d "Generating the certificates for ${manager_name} manager node."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n manager_ip="manager_node_ip_${j}"
            declare -n mgr_dns="manager_node_dns_${j}"
            declare -a manager_san=()
            if [ "${#manager_ip[@]}" -gt 0 ]; then
                manager_san+=("${manager_ip[@]}")
            fi
            if [ "${#mgr_dns[@]}" -gt 0 ]; then
                manager_san+=("${mgr_dns[@]}")
            fi
            cert_generateCertificateconfiguration "${manager_name}" "clientAuth" "${manager_san[@]}"
            common_logger -d "Creating the Wazuh manager tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${manager_name}-key.pem" -out "${cert_tmp_path}/${manager_name}.csr" -config "${cert_tmp_path}/${manager_name}.conf"
            common_logger -d "Creating the Wazuh manager certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${manager_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${manager_name}.pem" -extfile "${cert_tmp_path}/${manager_name}.conf" -extensions v3_req -days 3650
            # Agent-facing listener leaf for this manager node: the node SAN plus the
            # addresses given with --agent-san, which every node shares. Only this leaf
            # gets them; the manager certificate above keeps the node SAN alone.
            cert_generateRemotedcertificate "${manager_name}" "${manager_san[@]}" "${agent_san[@]}"
        done
    else
        return 1
    fi

}
function cert_generateServerLeafconfiguration() {

    common_logger -d "Generating server leaf certificate configuration."

    local conf_file="$1"
    local node_name="$2"
    local ip_counter=0
    local dns_counter=0
    local san
    local -A listed=()

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    if [ "${#@}" -le 2 ]; then
        common_logger -e "No IP or DNS specified"
        exit 1
    fi

    {
        printf '%s\n' "[ req ]"
        printf '%s\n' "prompt = no"
        printf '%s\n' "default_bits = 2048"
        printf '%s\n' "default_md = sha256"
        printf '%s\n' "distinguished_name = req_distinguished_name"
        printf '%s\n' "x509_extensions = v3_remoted"
        printf '\n'
        printf '%s\n' "[req_distinguished_name]"
        printf '%s\n' "C = US"
        printf '%s\n' "L = California"
        printf '%s\n' "O = Wazuh"
        printf '%s\n' "OU = Wazuh"
        printf '%s\n' "CN = ${node_name}"
        printf '\n'
        printf '%s\n' "[ v3_remoted ]"
        printf '%s\n' "authorityKeyIdentifier = keyid,issuer"
        printf '%s\n' "subjectKeyIdentifier = hash"
        printf '%s\n' "basicConstraints = critical,CA:FALSE"
        printf '%s\n' "keyUsage = critical,digitalSignature,keyEncipherment"
        printf '%s\n' "extendedKeyUsage = serverAuth"
        printf '%s\n' "subjectAltName = @alt_names"
        printf '\n'
        printf '%s\n' "[alt_names]"
    } > "${conf_file}"

    for (( i=3; i<=${#@}; i++ )); do
        san="${!i}"
        if cert_isIP "${san}"; then
            if [ -n "${listed[${san}]+listed}" ]; then
                continue
            fi
            listed["${san}"]=1
            ip_counter=$((ip_counter+1))
            printf '%s\n' "IP.${ip_counter} = ${san}" >> "${conf_file}"
        elif cert_isDNS "${san}"; then
            if [ -n "${listed[${san,,}]+listed}" ]; then
                continue
            fi
            listed["${san,,}"]=1
            dns_counter=$((dns_counter+1))
            printf '%s\n' "DNS.${dns_counter} = ${san}" >> "${conf_file}"
        else
            common_logger -e "Invalid IP or DNS ${san}"
            exit 1
        fi
    done

    if cert_isDNS "${node_name}" && [ -z "${listed[${node_name,,}]+listed}" ]; then
        dns_counter=$((dns_counter+1))
        printf '%s\n' "DNS.${dns_counter} = ${node_name}" >> "${conf_file}"
    fi

}
function cert_generateRemotedcertificateconfiguration() {

    local node_name="$1"
    shift

    cert_generateServerLeafconfiguration "${cert_tmp_path}/${node_name}-remoted.conf" "${node_name}" "$@"

}
function cert_generateRemotedCAworkspace() {

    local ca_dir="$1"

    rm -rf "${ca_dir}"
    if ! mkdir -p "${ca_dir}/newcerts"; then
        common_logger -e "Could not create the temporary CA directory ${ca_dir}."
        cert_cleanFiles
        exit 1
    fi
    chmod 700 "${ca_dir}"
    : > "${ca_dir}/index.txt"
    printf '%s\n' "unique_subject = no" > "${ca_dir}/index.txt.attr"
    # A random 16-byte serial: each node gets its own workspace, so an incrementing
    # counter would hand every manager node the same serial from the same CA.
    openssl rand -hex 16 > "${ca_dir}/serial"

    {
        printf '%s\n' "[ ca ]"
        printf '%s\n' "default_ca = CA_remoted"
        printf '\n'
        printf '%s\n' "[ CA_remoted ]"
        printf '%s\n' "dir = ${ca_dir}"
        printf '%s\n' "database = ${ca_dir}/index.txt"
        printf '%s\n' "serial = ${ca_dir}/serial"
        printf '%s\n' "new_certs_dir = ${ca_dir}/newcerts"
        printf '%s\n' "certificate = ${cert_tmp_path}/root-ca.pem"
        printf '%s\n' "private_key = ${rootcakey}"
        printf '%s\n' "default_md = sha256"
        printf '%s\n' "preserve = yes"
        printf '%s\n' "email_in_dn = no"
        printf '%s\n' "policy = policy_remoted"
        printf '\n'
        printf '%s\n' "[ policy_remoted ]"
        printf '%s\n' "countryName = optional"
        printf '%s\n' "stateOrProvinceName = optional"
        printf '%s\n' "localityName = optional"
        printf '%s\n' "organizationName = optional"
        printf '%s\n' "organizationalUnitName = optional"
        printf '%s\n' "commonName = supplied"
    } > "${ca_dir}/ca.conf"

}
function cert_generateServerLeaf() {

    local prefix="$1"
    local node_name="$2"
    local ca_dir="${cert_tmp_path}/${prefix}-ca"
    local start_date
    local end_date

    shift 2

    common_logger -d "Creating the server leaf certificate ${prefix}."

    cert_generateServerLeafconfiguration "${cert_tmp_path}/${prefix}.conf" "${node_name}" "$@"
    common_logger -d "Creating the ${prefix} tmp key pair."
    cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${prefix}-key.pem" -out "${cert_tmp_path}/${prefix}.csr" -config "${cert_tmp_path}/${prefix}.conf"

    # Two-digit years: OpenSSL reads them as 20YY below 50, and the notAfter of a
    # 3650-day certificate is well before 2049.
    start_date="$(date -u -d '-1 day' '+%y%m%d%H%M%SZ' 2>/dev/null)"
    end_date="$(date -u -d '+3650 days' '+%y%m%d%H%M%SZ' 2>/dev/null)"
    if [[ -z "${start_date}" || -z "${end_date}" ]]; then
        common_logger -e "Could not compute the validity dates of the ${prefix} certificate."
        cert_cleanFiles
        exit 1
    fi

    common_logger -d "Creating the ${prefix} certificate, valid from ${start_date} to ${end_date}."
    cert_generateRemotedCAworkspace "${ca_dir}"
    cert_executeAndValidate openssl ca -batch -notext -md sha256 -config "${ca_dir}/ca.conf" -in "${cert_tmp_path}/${prefix}.csr" -out "${cert_tmp_path}/${prefix}.pem" -extfile "${cert_tmp_path}/${prefix}.conf" -extensions v3_remoted -startdate "${start_date}" -enddate "${end_date}"
    rm -rf "${ca_dir}"

    if ! cat "${cert_tmp_path}/root-ca.pem" >> "${cert_tmp_path}/${prefix}.pem"; then
        common_logger -e "Could not append root-ca.pem to ${prefix}.pem."
        cert_cleanFiles
        exit 1
    fi

}
function cert_generateRemotedcertificate() {

    local node_name="$1"
    shift

    common_logger -d "Creating the remoted (agent listener) certificate for ${node_name}."

    cert_generateServerLeaf "${node_name}-remoted" "${node_name}" "$@"

}
function cert_verifyRemotedcertificates() {

    local certs_dir="${1}"
    local manager_name

    if [ ${#manager_node_names[@]} -eq 0 ]; then
        return 0
    fi

    if ! cert_validatePath "${certs_dir}" "directory"; then
        common_logger -e "Invalid certificates directory."
        exit 1
    fi

    for manager_name in "${manager_node_names[@]}"; do
        if ! openssl verify -CAfile "${certs_dir}/root-ca.pem" "${certs_dir}/${manager_name}-remoted.pem" > /dev/null 2>&1; then
            common_logger -e "The certificate ${certs_dir}/${manager_name}-remoted.pem does not verify against ${certs_dir}/root-ca.pem."
            exit 1
        fi
        common_logger -d "Verified ${manager_name}-remoted.pem against root-ca.pem."
    done

}
function cert_generateDashboardcertificates() {
    if [ ${#dashboard_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh dashboard certificates."

        for i in "${!dashboard_node_names[@]}"; do
            dashboard_node_name="${dashboard_node_names[i]}"

            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n dash_ip="dashboard_node_ip_${j}"
            declare -n dash_dns="dashboard_node_dns_${j}"
            declare -a dash_san=()
            if [ "${#dash_ip[@]}" -gt 0 ]; then
                dash_san+=("${dash_ip[@]}")
            fi
            if [ "${#dash_dns[@]}" -gt 0 ]; then
                dash_san+=("${dash_dns[@]}")
            fi
            cert_generateCertificateconfiguration "${dashboard_node_name}" "serverAuth" "${dash_san[@]}"
            common_logger -d "Creating the Wazuh dashboard tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${dashboard_node_name}-key.pem" -out "${cert_tmp_path}/${dashboard_node_name}.csr" -config "${cert_tmp_path}/${dashboard_node_name}.conf"
            common_logger -d "Creating the Wazuh dashboard certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${dashboard_node_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${dashboard_node_name}.pem" -extfile "${cert_tmp_path}/${dashboard_node_name}.conf" -extensions v3_req -days 3650
        done
    else
        return 1
    fi

}
function cert_generateLoadbalancercertificates() {

    if [ ${#lb_node_names[@]} -gt 0 ]; then
        common_logger "Generating load balancer certificates."

        for i in "${!lb_node_names[@]}"; do
            lb_node_name="${lb_node_names[i]}"

            common_logger -d "Creating the certificate for ${lb_node_name} load balancer."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n lb_ip="lb_node_ip_${j}"
            declare -n lb_dns="lb_node_dns_${j}"
            declare -a lb_san=()
            if [ "${#lb_ip[@]}" -gt 0 ]; then
                lb_san+=("${lb_ip[@]}")
            fi
            if [ "${#lb_dns[@]}" -gt 0 ]; then
                lb_san+=("${lb_dns[@]}")
            fi
            cert_generateServerLeaf "${lb_node_name}" "${lb_node_name}" "${lb_san[@]}"
        done
    else
        return 1
    fi

}
function cert_verifyLoadbalancercertificates() {

    local certs_dir="${1}"
    local lb_name

    if [ ${#lb_node_names[@]} -eq 0 ]; then
        return 0
    fi

    if ! cert_validatePath "${certs_dir}" "directory"; then
        common_logger -e "Invalid certificates directory."
        exit 1
    fi

    for lb_name in "${lb_node_names[@]}"; do
        if ! openssl verify -CAfile "${certs_dir}/root-ca.pem" "${certs_dir}/${lb_name}.pem" > /dev/null 2>&1; then
            common_logger -e "The certificate ${certs_dir}/${lb_name}.pem does not verify against ${certs_dir}/root-ca.pem."
            exit 1
        fi
        common_logger -d "Verified ${lb_name}.pem against root-ca.pem."
    done

}
function cert_generateRootCAcertificate() {

    if ! cert_ca_dir=$(wazuh_ca_get_dir); then
        common_logger -e "Could not resolve the root CA directory."
        cert_cleanFiles
        exit 1
    fi

    if [ -e "${cert_ca_dir}/root-ca.pem" ] || [ -e "${cert_ca_dir}/root-ca.key" ]; then
        common_logger "Using the existing root CA in ${cert_ca_dir}."
    else
        common_logger "Generating the root certificate in ${cert_ca_dir}."
        # A new CA is right for a new deployment, but the nodes of an existing one
        # only trust the CA they were issued from: a node issued from this one
        # would not match them. See #1049.
        common_logger -w "There is no root CA in ${cert_ca_dir}, so a new one is created. The certificates issued from it do not chain to the root CA of any existing deployment. To add nodes to an existing deployment, stop here and run the tool on the host that holds its root CA, or set WAZUH_CA_DIR to a directory with a copy of that root CA and its key."
    fi

    if ! wazuh_ca_ensure; then
        common_logger -e "The root CA in ${cert_ca_dir} could not be created or is not valid."
        cert_cleanFiles
        exit 1
    fi

}
function cert_normalizeYamlFormat() {

    # Normalize the certs-tool YAML schema regardless of incoming indentation.
    # It supports optional node fields (ip, dns, node_type). Both ip and dns may be
    # given as a scalar value or as a list; node_type is always a scalar.
    #
    # list_key holds the field whose list is currently being read, so that the items
    # under it are folded into that field instead of being taken for new nodes.
    awk '
    function ltrim(str) {
        sub(/^[ \t]+/, "", str)
        return str
    }
    function rtrim(str) {
        sub(/[ \t]+$/, "", str)
        return str
    }
    function trim(str) {
        return rtrim(ltrim(str))
    }
    BEGIN {
        in_nodes = 0
        nodes_indent = 0
        current_section = ""
        in_node = 0
        list_key = ""
    }
    {
        line = $0
        match(line, /^[ \t]*/)
        indent = RLENGTH

        if (match(line, /^[ \t]*$/)) {
            print ""
            list_key = ""
            next
        }

        if (match(line, /^[ \t]*#/)) {
            print line
            next
        }

        stripped = trim(line)

        if (stripped == "nodes:") {
            print "nodes:"
            in_nodes = 1
            nodes_indent = indent
            current_section = ""
            in_node = 0
            list_key = ""
            next
        }

        if (in_nodes == 1 && current_section != "" && match(stripped, /^-[ \t]/)) {
            list_payload = trim(substr(stripped, 2))

            if (match(list_payload, /^name:[ \t]*/)) {
                name_value = trim(substr(list_payload, 6))
                print "    - name: " name_value
                in_node = 1
                list_key = ""
                next
            }

            if (in_node == 1 && list_key != "") {
                print "        - " list_payload
                next
            }

            print "    - " list_payload
            in_node = 1
            list_key = ""
            next
        }

        if (in_nodes == 1 && in_node == 1 && list_key != "" && match(stripped, /^-[ \t]/)) {
            item_value = trim(substr(stripped, 2))
            print "        - " item_value
            next
        }

        if (in_nodes == 1 && match(stripped, /^[a-zA-Z0-9_ ]+:[ \t]*/)) {
            key_name = trim(substr(stripped, 1, index(stripped, ":") - 1))
            key_value = trim(substr(stripped, index(stripped, ":") + 1))

            if (key_name == "node type") {
                key_name = "node_type"
            }

            if (in_node == 1 && key_name == "name") {
                print "    - name: " key_value
                list_key = ""
                next
            }

            if (in_node == 1 && key_name == "node_type") {
                print "      node_type: " key_value
                list_key = ""
                next
            }

            if (in_node == 1 && (key_name == "ip" || key_name == "dns")) {
                if (key_value == "") {
                    print "      " key_name ":"
                    list_key = key_name
                } else {
                    print "      " key_name ": " key_value
                    list_key = ""
                }
                next
            }

            if (key_value == "" && (in_node == 0 || indent <= nodes_indent + 2)) {
                print "  " key_name ":"
                current_section = key_name
                in_node = 0
                list_key = ""
                next
            }

            if (in_node == 1) {
                if (key_value == "") {
                    print "      " key_name ":"
                } else {
                    print "      " key_name ": " key_value
                }
                list_key = ""
                next
            }
        }

        if (!match(stripped, /^-[ \t]/)) {
            list_key = ""
        }

        print line
    }
    '
}
function cert_parseYaml() {

    local config_file_path="$1"
    local prefix="$2"
    local separator="${3:-_}"
    local indexfix

    # Detect awk flavor
    if awk --version 2>&1 | grep -q "GNU Awk" ; then
    # GNU Awk detected
    indexfix=-1
    elif awk -Wv 2>&1 | grep -q "mawk" ; then
    # mawk detected
    indexfix=0
    fi

    local s='[[:space:]]*' sm='[ \t]*' w='[a-zA-Z0-9_]*' fs=${fs:-$(echo @|tr @ '\034')} i=${i:-  }

    # Normalize YAML format first to handle both valid YAML indentation styles
    cat $config_file_path 2>/dev/null | cert_normalizeYamlFormat | \
    awk -F$fs "{multi=0;
        if(match(\$0,/$sm\|$sm$/)){multi=1; sub(/$sm\|$sm$/,\"\");}
        if(match(\$0,/$sm>$sm$/)){multi=2; sub(/$sm>$sm$/,\"\");}
        while(multi>0){
            str=\$0; gsub(/^$sm/,\"\", str);
            indent=index(\$0,str);
            indentstr=substr(\$0, 0, indent+$indexfix) \"$i\";
            obuf=\$0;
            getline;
            while(index(\$0,indentstr)){
                obuf=obuf substr(\$0, length(indentstr)+1);
                if (multi==1){obuf=obuf \"\\\\n\";}
                if (multi==2){
                    if(match(\$0,/^$sm$/))
                        obuf=obuf \"\\\\n\";
                        else obuf=obuf \" \";
                }
                getline;
            }
            sub(/$sm$/,\"\",obuf);
            print obuf;
            multi=0;
            if(match(\$0,/$sm\|$sm$/)){multi=1; sub(/$sm\|$sm$/,\"\");}
            if(match(\$0,/$sm>$sm$/)){multi=2; sub(/$sm>$sm$/,\"\");}
        }
    print}" | \
    sed  -e "s|^\($s\)?|\1-|" \
        -ne "s|^$s#.*||;s|$s#[^\"']*$||;s|^\([^\"'#]*\)#.*|\1|;t1;t;:1;s|^$s\$||;t2;p;:2;d" | \
    sed -ne "s|,$s\]$s\$|]|" \
        -e ":1;s|^\($s\)\($w\)$s:$s\(&$w\)\?$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: \3[\4]\n\1$i- \5|;t1" \
        -e "s|^\($s\)\($w\)$s:$s\(&$w\)\?$s\[$s\(.*\)$s\]|\1\2: \3\n\1$i- \4|;" \
        -e ":2;s|^\($s\)-$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1- [\2]\n\1$i- \3|;t2" \
        -e "s|^\($s\)-$s\[$s\(.*\)$s\]|\1-\n\1$i- \2|;p" | \
    sed -ne "s|,$s}$s\$|}|" \
        -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1$i\3: \4|;t1" \
        -e "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1$i\2|;" \
        -e ":2;s|^\($s\)\($w\)$s:$s\(&$w\)\?$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1\2: \3 {\4}\n\1$i\5: \6|;t2" \
        -e "s|^\($s\)\($w\)$s:$s\(&$w\)\?$s{$s\(.*\)$s}|\1\2: \3\n\1$i\4|;p" | \
    sed  -e "s|^\($s\)\($w\)$s:$s\(&$w\)\(.*\)|\1\2:\4\n\3|" \
        -e "s|^\($s\)-$s\(&$w\)\(.*\)|\1- \3\n\2|" | \
    sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)\(---\)\($s\)||" \
        -e "s|^\($s\)\(\.\.\.\)\($s\)||" \
        -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p;t" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p;t" \
        -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\?\(.*\)$s\$|\1$fs\2$fs\3|" \
        -e "s|^\($s\)[\"']\?\([^&][^$fs]\+\)[\"']$s\$|\1$fs$fs$fs\2|" \
        -e "s|^\($s\)[\"']\?\([^&][^$fs]\+\)$s\$|\1$fs$fs$fs\2|" \
        -e "s|$s\$||p" | \
    awk -F$fs "{
        gsub(/\t/,\"        \",\$1);
        gsub(\"name: \", \"\");
        if(NF>3){if(value!=\"\"){value = value \" \";}value = value  \$4;}
        else {
        if(match(\$1,/^&/)){anchor[substr(\$1,2)]=full_vn;getline};
        indent = length(\$1)/length(\"$i\");
        vname[indent] = \$2;
        value= \$3;
        for (i in vname) {if (i > indent) {delete vname[i]; idx[i]=0}}
        if(length(\$2)== 0){  vname[indent]= ++idx[indent] };
        vn=\"\"; for (i=0; i<indent; i++) { vn=(vn)(vname[i])(\"$separator\")}
        vn=\"$prefix\" vn;
        full_vn=vn vname[indent];
        if(vn==\"$prefix\")vn=\"$prefix$separator\";
        if(vn==\"_\")vn=\"__\";
        }
        assignment[full_vn]=value;
        if(!match(assignment[vn], full_vn))assignment[vn]=assignment[vn] \" \" full_vn;
        if(match(value,/^\*/)){
            ref=anchor[substr(value,2)];
            if(length(ref)==0){
            printf(\"%s=\\\"%s\\\"\n\", full_vn, value);
            } else {
            for(val in assignment){
                if((length(ref)>0)&&index(val, ref)==1){
                    tmpval=assignment[val];
                    sub(ref,full_vn,val);
                if(match(val,\"$separator\$\")){
                    gsub(ref,full_vn,tmpval);
                } else if (length(tmpval) > 0) {
                    printf(\"%s=\\\"%s\\\"\n\", val, tmpval);
                }
                assignment[val]=tmpval;
                }
            }
        }
    } else if (length(value) > 0) {
        printf(\"%s=\\\"%s\\\"\n\", full_vn, value);
    }
    }END{
        for(val in assignment){
            if(match(val,\"$separator\$\"))
                printf(\"%s=\\\"%s\\\"\n\", val, assignment[val]);
        }
    }"

}
function cert_checkPrivateIp() {

    local ip="$1"
    common_logger -d "Checking if ${ip} is private."

    # Check private IPv4 ranges
    if [[ $ip =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^(127\.) ]]; then
        return 0
    fi

    # Check private IPv6 ranges (fc00::/7 prefix), link-local (fe80::/10), and loopback (::1)
    if [[ $ip =~ ^(fc|fd) ]] || [[ $ip =~ ^fe[89abAB] ]] || [[ $ip == "::1" ]]; then
        return 0
    fi

    return 1

}
function cert_isIPv4() {

    local ip="$1"
    local octet

    [[ ${ip} =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1

    for octet in ${ip//./ }; do
        # A leading zero is read as octal by some resolvers and as decimal by others,
        # so 010.0.0.1 is not one address but two. OpenSSL rejects both forms below.
        if [[ ${#octet} -gt 1 && ${octet:0:1} == "0" ]]; then
            return 1
        fi
        if [ "${octet}" -gt 255 ]; then
            return 1
        fi
    done

    return 0

}
function cert_isIPv6() {

    local ip="$1"
    [[ ${ip} =~ ^(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,7}:|:([0-9A-Fa-f]{1,4}:){1,7}|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:((:[0-9A-Fa-f]{1,4}){1,6})|::)$ ]]

}
function cert_isIP() {

    local ip="$1"
    cert_isIPv4 "${ip}" || cert_isIPv6 "${ip}"

}
function cert_isDNS() {

    local dns="$1"
    if ! cert_isIP "${dns}" && [[ ${dns} =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then
        return 0
    fi
    return 1

}
function cert_hostAddresses() {

    local address
    local name
    local -a addresses=()

    if command -v hostname > /dev/null 2>&1; then
        mapfile -t addresses < <(hostname -I 2>/dev/null | tr ' ' '\n')
    fi

    if [ "${#addresses[@]}" -eq 0 ] && command -v ip > /dev/null 2>&1; then
        mapfile -t addresses < <(ip -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d / -f 1)
    fi

    for address in "${addresses[@]}"; do
        if [ -z "${address}" ] || ! cert_isIP "${address}"; then
            continue
        fi
        # Link-local addresses are no more dialable from another host than loopback is.
        if [[ "${address}" =~ ^127\. ]] || [ "${address}" == "::1" ] || [[ "${address}" =~ ^169\.254\. ]] || [[ "${address}" =~ ^fe[89abAB] ]]; then
            continue
        fi
        printf '%s\n' "${address}"
    done

    for name in "$(hostname -f 2>/dev/null)" "$(hostname 2>/dev/null)"; do
        # localhost.localdomain is what a host with no name of its own reports, and it
        # names this host to every other one.
        if [ -n "${name}" ] && [[ ! "${name,,}" =~ ^localhost(\..*)?$ ]] && cert_isDNS "${name}"; then
            printf '%s\n' "${name}"
        fi
    done

}
function cert_listenerSanIsUnreachable() {

    local san

    for san in "$@"; do
        if cert_isIP "${san}"; then
            if [[ ! "${san}" =~ ^127\. ]] && [ "${san}" != "::1" ]; then
                return 1
            fi
        elif cert_isDNS "${san}"; then
            if [ "${san,,}" != "localhost" ]; then
                return 1
            fi
        fi
    done

    return 0

}
function cert_checkListenerReachability() {

    local severity="${1:-error}"
    local i
    local j
    local -a listener_san=()

    for i in "${!manager_node_names[@]}"; do
        j=$((i+1))
        declare -n manager_ip="manager_node_ip_${j}"
        declare -n mgr_dns="manager_node_dns_${j}"
        listener_san=()
        if [ "${#manager_ip[@]}" -gt 0 ]; then
            listener_san+=("${manager_ip[@]}")
        fi
        if [ "${#mgr_dns[@]}" -gt 0 ]; then
            listener_san+=("${mgr_dns[@]}")
        fi
        if [ "${#agent_san[@]}" -gt 0 ]; then
            listener_san+=("${agent_san[@]}")
        fi

        if cert_listenerSanIsUnreachable "${listener_san[@]}"; then
            if [ "${severity}" == "warning" ]; then
                common_logger -w "The agent listener certificate of the Wazuh manager node ${manager_node_names[$i]} only names loopback addresses, so no agent on another host can verify it. Set the address agents dial in the ip or dns field of the node in ${config_file}, or pass it with -as|--agent-san."
            else
                common_logger -e "The agent listener certificate of the Wazuh manager node ${manager_node_names[$i]} would only name loopback addresses, and no agent could verify it. Set the address agents dial in the ip or dns field of the node in ${config_file}, or pass it with -as|--agent-san."
                exit 1
            fi
        fi
    done

}
function cert_validateAgentSan() {

    local san

    if [ "${#agent_san[@]}" -eq 0 ]; then
        return 0
    fi

    if [[ -z "${all}" && -z "${cmanager}" && -z "${AIO}" && -z "${configurations}" ]]; then
        common_logger -e "The option -as|--agent-san must be used along with one of these options: -A, -wm in wazuh-certs-tool.sh, or -a, -g in wazuh-install.sh"
        exit 1
    fi

    for san in "${agent_san[@]}"; do
        if ! cert_isIP "${san}" && ! cert_isDNS "${san}"; then
            common_logger -e "Invalid IP or DNS in -as|--agent-san: ${san}."
            exit 1
        fi
    done

}
function cert_validateComponentSanValues() {

    local component_name="$1"
    local node_names_var="$2"
    local node_ip_prefix="$3"
    local node_dns_prefix="$4"
    local i
    local j

    # Use nameref for safe dynamic array access
    declare -n component_node_names="${node_names_var}"

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        # Use namerefs for dynamic array names
        declare -n component_ip="${node_ip_prefix}_${j}"
        declare -n component_dns="${node_dns_prefix}_${j}"

        if [ "${#component_ip[@]}" -eq 0 ] && [ "${#component_dns[@]}" -eq 0 ]; then
            common_logger -e "${component_name} node ${component_node_names[$i]} requires at least one field: ip or dns."
            exit 1
        fi

        for ip in "${component_ip[@]}"; do
            if ! cert_isIP "${ip}"; then
                common_logger -e "Invalid IP in field ip for ${component_name,,} node ${component_node_names[$i]}: ${ip}."
                exit 1
            fi
            # A public address is legitimate: a manager agents reach over the internet,
            # a node behind a cloud load balancer. It is worth naming, not refusing.
            if ! cert_checkPrivateIp "$ip"; then
                common_logger -w "The IP ${ip} of ${component_name,,} node ${component_node_names[$i]} is public. Make sure it is meant to be reachable from outside your network."
            fi
        done

        for dns in "${component_dns[@]}"; do
            if ! cert_isDNS "${dns}"; then
                common_logger -e "Invalid DNS in field dns for ${component_name,,} node ${component_node_names[$i]}: ${dns}."
                exit 1
            fi
        done
    done

}
function cert_validateComponentDuplicatedValues() {

    local component_name="$1"
    local node_names_var="$2"
    local node_ips_var="$3"
    local node_dns_prefix="$4"
    local i
    local j

    # Use namerefs for safe dynamic array access
    declare -n component_node_names="${node_names_var}"
    declare -n component_node_ips="${node_ips_var}"
    declare -a component_node_dns=()

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        # Use nameref for dynamic DNS array
        declare -n node_dns="${node_dns_prefix}_${j}"
        if [ "${#node_dns[@]}" -gt 0 ]; then
            component_node_dns+=("${node_dns[@]}")
        fi
    done

    unique_names=($(echo "${component_node_names[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_names[@]}" -ne "${#component_node_names[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node names."
        exit 1
    fi

    unique_ips=($(echo "${component_node_ips[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_ips[@]}" -ne "${#component_node_ips[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node ips."
        exit 1
    fi

    unique_dns=($(echo "${component_node_dns[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_dns[@]}" -ne "${#component_node_dns[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node dns."
        exit 1
    fi

}
function cert_validateManagerNodeTypes() {

    for i in "${manager_node_types[@]}"; do
        if ! echo "$i" | grep -ioq master && ! echo "$i" | grep -ioq worker; then
            common_logger -e "Incorrect node_type $i must be master or worker"
            exit 1
        fi
    done

    if [ "${#manager_node_names[@]}" -le 1 ]; then
        if [ "${#manager_node_types[@]}" -ne 0 ]; then
            common_logger -e "The tag node_type can only be used with more than one Wazuh manager."
            exit 1
        fi
    elif [ "${#manager_node_names[@]}" -gt "${#manager_node_types[@]}" ]; then
        common_logger -e "The tag node_type needs to be specified for all Wazuh manager nodes."
        exit 1
    elif [ "${#manager_node_names[@]}" -lt "${#manager_node_types[@]}" ]; then
        common_logger -e "Found extra node_type tags."
        exit 1
    elif [ "$(grep -io master <<< "${manager_node_types[*]}" | wc -l)" -ne 1 ]; then
        common_logger -e "Wazuh cluster needs a single master node."
        exit 1
    elif [ "$(grep -io worker <<< "${manager_node_types[*]}" | wc -l)" -ne $(( ${#manager_node_types[@]} - 1 )) ]; then
        common_logger -e "Incorrect number of workers."
        exit 1
    fi

}
function cert_firstAddressPerNode() {

    local target_var="$1"
    local node_names_var="$2"
    local node_ip_prefix="$3"
    local i
    local j

    declare -n target_array="${target_var}"
    declare -n component_node_names="${node_names_var}"

    target_array=()

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        declare -n component_ip="${node_ip_prefix}_${j}"
        if [ "${#component_ip[@]}" -gt 0 ]; then
            target_array+=("${component_ip[0]}")
        fi
    done

}
function cert_readConfig() {

    common_logger -d "Reading configuration file."

    if [ -f "${config_file}" ]; then
        if [ ! -s "${config_file}" ]; then
            common_logger -e "File ${config_file} is empty"
            exit 1
        fi
        # Convert CRLF to LF without eval
        cert_convertCRLFtoLF "${config_file}"

        # Use mapfile for safe array assignment (prevents command injection)
        mapfile -t indexer_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t dashboard_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t lb_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t indexer_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t dashboard_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_types < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+[_]+node_type=" | cut -d = -f 2 | sed 's/^"//;s/"$//')

        # Parse DNS entries for each indexer node
        for i in "${!indexer_node_names[@]}"; do
            j=$((i+1))
            # Create dynamic arrays using declare and mapfile
            mapfile -t "indexer_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "indexer_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # Parse DNS entries for each dashboard node
        for i in "${!dashboard_node_names[@]}"; do
            j=$((i+1))
            mapfile -t "dashboard_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "dashboard_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        for i in $(seq 1 "${#manager_node_names[@]}"); do
            mapfile -t "manager_node_ip_$i" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+${i}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//' | sed -r 's/\s+//g')
            mapfile -t "manager_node_dns_$i" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+${i}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # Parse the addresses of each load balancer entry
        for i in "${!lb_node_names[@]}"; do
            j=$((i+1))
            mapfile -t "lb_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "lb_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # The installer addresses a node by its position in these arrays, so they hold
        # one entry per node. A node reachable at several addresses puts them all in
        # its certificate through the per-node arrays above; the first one is the
        # address the components are configured with.
        cert_firstAddressPerNode "indexer_node_ips" "indexer_node_names" "indexer_node_ip"
        cert_firstAddressPerNode "manager_node_ips" "manager_node_names" "manager_node_ip"
        cert_firstAddressPerNode "dashboard_node_ips" "dashboard_node_names" "dashboard_node_ip"

        cert_validateComponentSanValues "Indexer" "indexer_node_names" "indexer_node_ip" "indexer_node_dns"
        cert_validateComponentSanValues "Manager" "manager_node_names" "manager_node_ip" "manager_node_dns"
        cert_validateComponentSanValues "Dashboard" "dashboard_node_names" "dashboard_node_ip" "dashboard_node_dns"
        cert_validateComponentSanValues "Load balancer" "lb_node_names" "lb_node_ip" "lb_node_dns"

        cert_sanitizeNodeName "Indexer" "indexer_node_names"
        cert_sanitizeNodeName "Manager" "manager_node_names"
        cert_sanitizeNodeName "Dashboard" "dashboard_node_names"
        cert_sanitizeNodeName "Load balancer" "lb_node_names"

        cert_validateComponentDuplicatedValues "Indexer" "indexer_node_names" "indexer_node_all_ips" "indexer_node_dns"
        cert_validateComponentDuplicatedValues "Wazuh manager" "manager_node_names" "manager_node_all_ips" "manager_node_dns"
        cert_validateComponentDuplicatedValues "Dashboard" "dashboard_node_names" "dashboard_node_all_ips" "dashboard_node_dns"

        cert_validateManagerNodeTypes

    else
        common_logger -e "No configuration file found."
        exit 1
    fi

}
function cert_rejectCAPaths() {

    if [[ -n "${2}" && "${2}" != -* ]]; then
        common_logger -e "${1} does not take root CA files anymore. The root CA is read from $(wazuh_ca_get_dir 2>/dev/null). Set WAZUH_CA_DIR to use the root CA of another directory."
        exit 1
    fi

}
function cert_setpermisions() {
    # Validate cert_tmp_path before setting permissions
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        return 1
    fi

    # Private keys (every node/admin/remoted key) must stay
    # owner-only: the umask this tool sets at startup already creates them
    # at 0600, this only re-asserts it. Public certificates can be 0644 -
    # they carry no secret. The directory itself must stay 700 so the mode
    # on the files inside cannot be reached even if a later 'chmod 755' on
    # a copy of this directory relaxes traversal.
    if [ -n "${debugEnabled}" ]; then
        chmod 700 "${cert_tmp_path}"
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*-key.pem' -exec chmod 600 {} +
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*.pem' ! -name '*-key.pem' -exec chmod 644 {} +
    else
        chmod 700 "${cert_tmp_path}" > /dev/null 2>&1
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*-key.pem' -exec chmod 600 {} + > /dev/null 2>&1
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*.pem' ! -name '*-key.pem' -exec chmod 644 {} + > /dev/null 2>&1
    fi
}
function cert_convertCRLFtoLF() {
    local config_file_path="$1"
    local temp_dir="/tmp/wazuh-install-files"

    # Validate input file path
    if ! cert_validatePath "${config_file_path}" "file"; then
        common_logger -e "Invalid config file path."
        return 1
    fi

    # Create temp directory if it doesn't exist
    if [[ ! -d "${temp_dir}" ]]; then
        if [ -n "${debugEnabled}" ]; then
            mkdir "${temp_dir}"
        else
            mkdir "${temp_dir}" > /dev/null 2>&1
        fi
    fi

    # Set permissions on temp directory
    if [ -n "${debugEnabled}" ]; then
        chmod -R 755 "${temp_dir}"
    else
        chmod -R 755 "${temp_dir}" > /dev/null 2>&1
    fi

    # Convert CRLF to LF
    tr -d '\015' < "${config_file_path}" > "${temp_dir}/new_config.yml"

    # Move converted file back
    if [ -n "${debugEnabled}" ]; then
        mv "${temp_dir}/new_config.yml" "${config_file_path}"
    else
        mv "${temp_dir}/new_config.yml" "${config_file_path}" > /dev/null 2>&1
    fi
}

# ------------ certMain.sh ------------ 
function getHelp() {

    echo -e ""
    echo -e "NAME"
    echo -e "        wazuh-certs-tool.sh - Manages the creation of certificates of the Wazuh components."
    echo -e ""
    echo -e "SYNOPSIS"
    echo -e "        wazuh-certs-tool.sh [OPTIONS]"
    echo -e ""
    echo -e "DESCRIPTION"
    echo -e "        -a,  --admin-certificates"
    echo -e "                Creates the admin certificates."
    echo -e ""
    echo -e "        -as, --agent-san <ip|dns>"
    echo -e "                Adds an extra address to the subject alternative name of every"
    echo -e "                agent listener certificate, on top of the ip and dns entries of"
    echo -e "                each manager node in config.yml. Repeat it for more than one."
    echo -e "                It is how to name an address agents dial that no single node"
    echo -e "                owns, such as a load balancer shared by every node of a cluster."
    echo -e "                Must be used along with one of these options: -A, -wm"
    echo -e ""
    echo -e "        -A, --all"
    echo -e "                Creates certificates specified in config.yml and admin certificates."
    echo -e "                If there is no root CA in the CA directory, a new one is created there."
    echo -e ""
    echo -e "        -ca, --root-ca-certificates"
    echo -e "                Creates the root CA in the CA directory, if it does not exist yet."
    echo -e ""
    echo -e "        -lb, --load-balancer-certificates"
    echo -e "                Creates the certificates of the load_balancer entries of config.yml."
    echo -e "                Only needed by a proxy that terminates"
    echo -e "                TLS: it is then the certificate agents validate, signed by the same"
    echo -e "                root-ca they pin. A layer 4 passthrough load balancer terminates"
    echo -e "                nothing and needs -as|--agent-san instead."
    echo -e ""
    echo -e "        -v,  --verbose"
    echo -e "                Enables verbose mode."
    echo -e ""
    echo -e "        -wd,  --wazuh-dashboard-certificates"
    echo -e "                Creates the Wazuh dashboard certificates."
    echo -e ""
    echo -e "        -wi,  --wazuh-indexer-certificates"
    echo -e "                Creates the Wazuh indexer certificates."
    echo -e ""
    echo -e "        -wm,  --wazuh-manager-certificates"
    echo -e "                Creates the Wazuh manager certificates."
    echo -e "                Each manager node also gets <name>-remoted.pem and <name>-remoted-key.pem,"
    echo -e "                the certificate of the agent listener (remoted and authd)."
    echo -e ""
    echo -e "        -tmp,  --cert_tmp_path </path/to/tmp_dir>"
    echo -e "                Modifies the default tmp directory (/tmp/wazuh-ceritificates) to the specified one."
    echo -e "                Must be used along with one of these options: -a, -A, -ca, -wi, -wd, -wm, -lb"
    echo -e ""
    echo -e "ROOT CA"
    echo -e "        The root CA is read from $(wazuh_ca_get_dir 2>/dev/null), or from the directory set in WAZUH_CA_DIR."
    echo -e "        Its private key, root-ca.key, stays there and is never copied to the wazuh-certificates directory."
    echo -e "        The tool must be run as root."
    echo -e ""

    exit 1

}
function main() {

    # Set a restrictive umask so new regular files default to 600 and directories to 700,
    # limiting access to the current user.
    umask 0077

    # The shared credentials library only creates and reads the root CA as root.
    common_checkRoot
    cert_checkOpenSSL

    declare -a agent_san=()

    if [ -n "${1}" ]; then
        while [ -n "${1}" ]
        do
            case "${1}" in
            "-a"|"--admin-certificates")
                cert_rejectCAPaths "-a|--admin-certificates" "${2}"
                cadmin=1
                shift 1
                ;;
            "-A"|"--all")
                cert_rejectCAPaths "-A|--all" "${2}"
                all=1
                shift 1
                ;;
            "-as"|"--agent-san")
                if [[ -z "${2}" || "${2}" == -* ]]; then
                    common_logger -e "Error on arguments. Probably missing <ip|dns> after -as|--agent-san"
                    getHelp
                    exit 1
                else
                    agent_san+=("${2}")
                    shift 2
                fi
                ;;
            "-ca"|"--root-ca-certificates"|"--root-ca-certificate")
                ca=1
                shift 1
                ;;
            "-lb"|"--load-balancer-certificates")
                cert_rejectCAPaths "-lb|--load-balancer-certificates" "${2}"
                clb=1
                shift 1
                ;;
            "-h"|"--help")
                getHelp
                ;;
            "-v"|"--verbose")
                debugEnabled=1
                shift 1
                ;;
            "-wd"|"--wazuh-dashboard-certificates")
                cert_rejectCAPaths "-wd|--wazuh-dashboard-certificates" "${2}"
                cdashboard=1
                shift 1
                ;;
            "-wi"|"--wazuh-indexer-certificates")
                cert_rejectCAPaths "-wi|--wazuh-indexer-certificates" "${2}"
                cindexer=1
                shift 1
                ;;
            "-wm"|"--wazuh-manager-certificates")
                cert_rejectCAPaths "-wm|--wazuh-manager-certificates" "${2}"
                cmanager=1
                shift 1
                ;;
            "-tmp"|"--cert_tmp_path")
                if [[ -n "${3}" || ( "${cadmin}" == 1 || "${all}" == 1 || "${ca}" == 1 || "${cdashboard}" == 1 || "${cindexer}" == 1 || "${cmanager}" == 1 || "${clb}" == 1 ) ]]; then
                    if [[ -z "${2}" || ! "${2}" == /* ]]; then
                        common_logger -e "Error on arguments. Probably missing </path/to/tmp_dir> or path does not start with '/'."
                        getHelp
                        exit 1
                    else
                        cert_tmp_path="${2}"
                        shift 2
                    fi
                else
                    common_logger -e "Error: -tmp must be used along with one of these options: -a, -A, -ca, -wi, -wd, -wm, -lb"
                    getHelp
                    exit 1
                fi
                ;;
            *)
                echo "Unknown option: ${1}"
                getHelp
            esac
        done

        common_logger "Verbose logging redirected to ${logfile}"

        cert_validateAgentSan

        if [[ -d "${base_path}"/wazuh-certificates ]]; then
            if [ -n "$(ls -A "${base_path}"/wazuh-certificates)" ]; then
                common_logger -e "Directory wazuh-certificates already exists in the same path as the script. Please, remove the certs directory to create new certificates."
                exit 1
            fi
        fi

        if [[ ! -d "${cert_tmp_path}" ]]; then
            # Create directory with secure permissions
            mkdir -p "${cert_tmp_path}"
            chmod 700 "${cert_tmp_path}"
        else
            # Ensure existing directory has secure permissions
            chmod 700 "${cert_tmp_path}"
        fi

        cert_readConfig

        if [[ -n "${all}" || -n "${cmanager}" ]]; then
            cert_checkListenerReachability "warning"
        fi

        if [ -n "${debugEnabled}" ]; then
            debug="2>&1 | tee -a ${logfile}"
        fi

        if [[ -n "${cadmin}" ]]; then
            cert_checkRootCA
            cert_generateAdmincertificate
            common_logger "Admin certificates created."
            cert_cleanFiles
            cert_setpermisions
            if [ -n "${debugEnabled}" ]; then
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
            else
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
            fi
        fi

        if [[ -n "${all}" ]]; then
            cert_checkRootCA "create"
            cert_generateAdmincertificate
            common_logger "Admin certificates created."
            if cert_generateIndexercertificates; then
                common_logger "Wazuh indexer certificates created."
            fi
            if cert_generateManagercertificates; then
                common_logger "Wazuh manager certificates created."
            fi
            if cert_generateDashboardcertificates; then
                common_logger "Wazuh dashboard certificates created."
            fi
            # Only when config.yml carries a load_balancer section: it is optional and
            # its absence is not an error.
            if cert_generateLoadbalancercertificates; then
                common_logger "Load balancer certificates created."
            fi
            cert_cleanFiles
            cert_setpermisions
            if [ -n "${debugEnabled}" ]; then
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
            else
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
            fi
            cert_verifyRemotedcertificates "${base_path}/wazuh-certificates"
            cert_verifyLoadbalancercertificates "${base_path}/wazuh-certificates"
        fi

        if [[ -n "${ca}" ]]; then
            cert_generateRootCAcertificate
            cp "${cert_ca_dir}/root-ca.pem" "${cert_tmp_path}/root-ca.pem"
            common_logger "The root CA is in ${cert_ca_dir}. Its private key, root-ca.key, stays there."
            cert_cleanFiles
            cert_setpermisions
            if [ -n "${debugEnabled}" ]; then
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
            else
                mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
            fi
        fi

        if [[ -n "${cindexer}" ]]; then
            if [ ${#indexer_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateIndexercertificates
                common_logger "Wazuh indexer certificates created."
                cert_cleanFiles
                cert_setpermisions
                if [ -n "${debugEnabled}" ]; then
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
                else
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
                fi
            else
                common_logger -e "Indexer node not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${cmanager}" ]]; then
            if [ ${#manager_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateManagercertificates
                common_logger "Wazuh manager certificates created."
                cert_cleanFiles
                cert_setpermisions
                if [ -n "${debugEnabled}" ]; then
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
                else
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
                fi
                cert_verifyRemotedcertificates "${base_path}/wazuh-certificates"
            else
                common_logger -e "Manager node not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${clb}" ]]; then
            if [ ${#lb_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateLoadbalancercertificates
                common_logger "Load balancer certificates created."
                cert_cleanFiles
                cert_setpermisions
                if [ -n "${debugEnabled}" ]; then
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
                else
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
                fi
                cert_verifyLoadbalancercertificates "${base_path}/wazuh-certificates"
            else
                common_logger -e "Load balancer section not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${cdashboard}" ]]; then
            if [ ${#dashboard_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateDashboardcertificates
                common_logger "Wazuh dashboard certificates created."
                cert_cleanFiles
                cert_setpermisions
                if [ -n "${debugEnabled}" ]; then
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
                else
                    mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
                fi
            else
                common_logger -e "Dashboard node not present in config.yml."
                exit 1
            fi
        fi

    else
        getHelp
    fi

}
# ------------ certVariables.sh ------------ 

function common_checkAptLock() {

    attempt=0
    seconds=30
    max_attempts=10

    while fuser "${apt_lockfile}" >/dev/null 2>&1 && [ "${attempt}" -lt "${max_attempts}" ]; do
        attempt=$((attempt+1))
        common_logger "Another process is using APT. Waiting for it to release the lock. Next retry in ${seconds} seconds (${attempt}/${max_attempts})"
        sleep "${seconds}"
    done

}
function common_logger() {

    now=$(date +'%d/%m/%Y %H:%M:%S')
    mtype="INFO:"
    debugLogger=
    nolog=
    if [ -n "${1}" ]; then
        while [ -n "${1}" ]; do
            case ${1} in
                "-e")
                    mtype="ERROR:"
                    shift 1
                    ;;
                "-w")
                    mtype="WARNING:"
                    shift 1
                    ;;
                "-d")
                    debugLogger=1
                    mtype="DEBUG:"
                    shift 1
                    ;;
                "-nl")
                    nolog=1
                    shift 1
                    ;;
                *)
                    message="${1}"
                    shift 1
                    ;;
            esac
        done
    fi

    if [ -z "${debugLogger}" ] || { [ -n "${debugLogger}" ] && [ -n "${debugEnabled}" ]; }; then
        if [ -z "${nolog}" ] && { [ "$EUID" -eq 0 ] || [[ "$(basename "$0")" =~ $cert_tool_script_name ]]; }; then
            printf "%s\n" "${now} ${mtype} ${message}" | tee -a ${logfile}
        else
            printf "%b\n" "${now} ${mtype} ${message}"
        fi
    fi

}
function common_checkRoot() {

    common_logger -d "Checking root permissions."
    if [ "$EUID" -ne 0 ]; then
        echo "This script must be run as root."
        exit 1;
    fi

}
function common_checkInstalled() {

    common_logger -d "Checking Wazuh installation."
    wazuh_installed=""
    indexer_installed=""
    dashboard_installed=""

    if [ "${sys_type}" == "yum" ]; then
        rpm -q wazuh-manager --quiet && wazuh_installed=1
    elif [ "${sys_type}" == "apt-get" ]; then
        wazuh_installed=$(apt list --installed  2>/dev/null | grep wazuh-manager)
    fi

    if [ -d "/var/wazuh-manager" ]; then
        common_logger -d "There are Wazuh remaining files."
        wazuh_remaining_files=1
    fi

    if [ "${sys_type}" == "yum" ]; then
        rpm -q wazuh-indexer --quiet && indexer_installed=1

    elif [ "${sys_type}" == "apt-get" ]; then
        indexer_installed=$(apt list --installed 2>/dev/null | grep wazuh-indexer)
    fi

    if [ -d "/var/lib/wazuh-indexer/" ] || [ -d "/usr/share/wazuh-indexer" ] || [ -d "/etc/wazuh-indexer" ] || [ -f "${base_path}/search-guard-tlstool*" ]; then
        common_logger -d "There are Wazuh indexer remaining files."
        indexer_remaining_files=1
    fi

    if [ "${sys_type}" == "yum" ]; then
        eval "rpm -q wazuh-dashboard --quiet && dashboard_installed=1"
    elif [ "${sys_type}" == "apt-get" ]; then
        dashboard_installed=$(apt list --installed  2>/dev/null | grep wazuh-dashboard)
    fi

    if [ -d "/var/lib/wazuh-dashboard/" ] || [ -d "/usr/share/wazuh-dashboard" ] || [ -d "/etc/wazuh-dashboard" ] || [ -d "/run/wazuh-dashboard/" ]; then
        common_logger -d "There are Wazuh dashboard remaining files."
        dashboard_remaining_files=1
    fi

}
function common_checkSystem() {

    if [ -n "$(command -v yum)" ]; then
        sys_type="yum"
        sep="-"
        common_logger -d "YUM package manager will be used."
    elif [ -n "$(command -v apt-get)" ]; then
        sys_type="apt-get"
        sep="="
        common_logger -d "APT package manager will be used."
    else
        common_logger -e "Couldn't find YUM or APT package manager. Try installing the one corresponding to your operating system and then, launch the installation assistant again."
        exit 1
    fi

}
function common_checkWazuhConfigYaml() {

    common_logger -d "Checking Wazuh YAML configuration file."
    filecorrect=$(cert_parseYaml "${config_file}" | grep -Ev '^#|^\s*$' | grep -Pzc "\A(\s*(nodes_indexer__name|nodes_indexer__ip|nodes_manager__name|nodes_manager__ip|nodes_manager__node_type|nodes_dashboard__name|nodes_dashboard__ip)=.*?)+\Z")
    if [[ "${filecorrect}" -ne 1 ]]; then
        common_logger -e "The configuration file ${config_file} does not have a correct format."
        exit 1
    fi

}
function common_curl() {

    if [ -n "${curl_has_connrefused}" ]; then
        eval "curl --retry-connrefused $@"
        e_code="${PIPESTATUS[0]}"
    else
        retries=0
        eval "curl $@"
        e_code="${PIPESTATUS[0]}"
        while [ "${e_code}" -eq 7 ] && [ "${retries}" -ne 12 ]; do
            retries=$((retries+1))
            sleep 5
            eval "curl $@"
            e_code="${PIPESTATUS[0]}"
        done
    fi
    return "${e_code}"

}
function common_checkYumLock() {

    attempt=0
    seconds=30
    max_attempts=10

    while [ -f "${yum_lockfile}" ] && [ "${attempt}" -lt "${max_attempts}" ]; do
        attempt=$((attempt+1))
        common_logger "Another process is using YUM. Waiting for it to release the lock. Next retry in ${seconds} seconds (${attempt}/${max_attempts})"
        sleep "${seconds}"
    done

}

main "$@"
