Monitoring HashiCorp Vault activities with Wazuh

| by | Wazuh 4.14.4
Post icon

Managing secrets in a distributed environment remains a significant challenge for many organizations. HashiCorp Vault (Vault) serves as a centralized system for storing and managing sensitive data such as API keys, tokens, passwords, and certificates. While centralization simplifies administration and access control, misconfigurations or unauthorized access can quickly lead to compromise and data loss. Therefore, it is important to gain visibility into how Vault is accessed and used.

Wazuh is an open source security platform that helps organizations meet monitoring needs by aggregating security events and analyzing system behavior. Integrating Wazuh with HashiCorp Vault enables the collection and analysis of logs to identify anomalous behavior and potential security breaches. Furthermore, Wazuh utilizes its Active Response capability to automate threat mitigation, such as sealing a Vault cluster when specific security incidents are identified.

This blog post explores how to integrate Wazuh with Vault to provide continuous monitoring of authentication events, secret access, policy changes, and system activity. Additionally, it shows how to configure the Wazuh Active Response module to automatically respond to potential threats targeting the Vault in real time.

Infrastructure

We use the following infrastructure for this integration:

  • A pre-built, ready-to-use Wazuh OVA 4.14.4, which includes the Wazuh central components (Wazuh server, Wazuh indexer, and Wazuh dashboard). Follow this guide to download and deploy the virtual machine.
  • An Ubuntu 24.04 endpoint with:
    • The Wazuh agent 4.14.4 installed and enrolled to the Wazuh server.
    • HashiCorp Vault installed following these steps.

HashiCorp Vault events

HashiCorp Vault (Vault) generates two types of logs:

  • Audit logs: These logs provide a detailed trail of every request made to the Vault and its corresponding response. They are not enabled by default, but once active, they answer the “Who, What, and When” for every secret access or policy change. Vault supports several audit device types, including Syslog, Socket, and File. In this blog post, we will use the File audit device to store our logs.
  • Operational logs: These are standard system logs that focus on the health and state of the Vault server. They record events such as service startup, sealing/unsealing, cluster leadership changes, and internal storage or network connectivity errors.

Configuration

Ubuntu endpoint

In this section, we enable Vault audit logging, then configure the Wazuh agent to collect these logs and forward them to the Wazuh server for analysis.

Note

According to HashiCorp documentation, Vault prioritizes security over availability. If an audit device is enabled but cannot be written to (for example, due to a permission denied or disk full error), the Vault will stop responding to all client requests. To maintain availability, it is recommended to enable multiple audit devices.

  1. Enable Vault service to start upon restarts:
# systemctl enable --now  vault.service
  1. Export the following variables to make it available to Vault processes launched from the current shell session:
# export VAULT_ADDR="https://<VAULT_SERVER_IP>:8200"
# export VAULT_SKIP_VERIFY=true

Where:

  • VAULT_ADDR: Defines the Vault API endpoint to query using the Vault CLI. Replace <VAULT_SERVER_IP> with the IP address of your Vault server.
  • VAULT_SKIP_VERIFY=true: Is required when Vault is configured with self-signed certificates (default setup). Without this, the Vault CLI will reject the connection because it cannot verify the certificate authority.
  1. Verify your Vault instance is running:
# vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
Total Shares       0
Threshold          0
Unseal Progress    0/0
Unseal Nonce       n/a
Version            1.21.3
Build Date         2026-02-03T14:56:30Z
Storage Type       file
HA Enabled         false
  1. Initialize the Vault instance to generate the Unseal keys and the initial root token. Vault uses a threshold-based mechanism where the master key is split into multiple shares to ensure security. You must provide a minimum number of these shares (the threshold) to reconstruct the key and unseal the Vault after a restart or seal event. 

Make sure you save these credentials securely. With the key-threshold parameter set to three, you will need to provide three Unseal keys during the unseal operation. 

# vault operator init -key-shares=5 -key-threshold=3
  1. Unseal the Vault with one of the unseal keys using the command below. You will be prompted to enter your Unseal key.
# vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       4dd71e09-6cf9-2039-3e32-bc968ce37053
Version            1.21.3
Build Date         2026-02-03T14:56:30Z
Storage Type       file
HA Enabled         false

Repeat the process with a different Unseal key till the key Sealed is false.

  1. Log in to the Vault using the initial root token saved during the initialization process.
# vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
  1. Create a file vault_audit.log in the /var/log/ directory to record Vault events for security auditing, and set the ownership permissions:
# touch /var/log/vault_audit.log
# chown vault:vault /var/log/vault_audit.log
  1. Enable audit prefixing in the Vault configuration and restart the service to apply the configuration changes:
# echo "allow_audit_log_prefixing = true" >> /etc/vault.d/vault.hcl
# systemctl restart vault.service
  1. Unseal the Vault cluster as performed in step 5 above. This is because Vault has been sealed during the service restart operation.
# vault operator unseal
  1. Enable the file-based audit device on your Vault server:
# vault audit enable  -description "Auditing Vault" file file_path='/var/log/vault_audit.log' mode="644" elide_list_responses=true prefix="[vault audit]"

Note

Vault operational logs are enabled by default with the info log level, but you can adjust the verbosity to trace, debug, warn, or error using the log_level parameter in the Vault configuration file or the VAULT_LOG_LEVEL environment variable.

  1. Append the configuration below to the Wazuh agent file /var/ossec/etc/ossec.conf to monitor and forward Vault events to the Wazuh server for analysis:
<ossec_config>
  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/vault_audit.log</location>
  </localfile>
</ossec_config>
  1. Restart the Wazuh agent to apply the changes:
# systemctl restart wazuh-agent

Wazuh dashboard

Perform the following steps on the Wazuh dashboard to create custom decoders and rules to trigger alerts for relevant Vault events:

  1. Navigate to Server management > Decoders, click + Add new decoders file.
  2. Copy and paste the decoders below, name the file vault_decoder.xml, and click Save.
<decoder name="vault-audit">
    <prematch>^[vault audit]</prematch>
    <plugin_decoder offset="after_prematch">JSON_Decoder</plugin_decoder>
</decoder>

<decoder name="vault">
  <program_name>^vault$</program_name>
</decoder>

<decoder name="vault-fields">
  <parent>vault</parent>
  <regex type="pcre2">\[(\w+)\]\s+([\w\.-]+):\s+(.*)</regex>
  <order>vault_level, vault_component, vault_message</order>
</decoder>

<decoder name="vault-fields">
  <parent>vault</parent>
  <regex type="pcre2">from ([\d\.]+):(\d+)</regex>
  <order>srcip, srcport</order>
</decoder>
  1. Navigate to Server management > Rules and click + Add new rules file.
  2. Copy and paste the rules below and name the file vault_audit.xml. Click Save, then Reload to apply the changes.
<group name="vault,">

  <!-- Vault operational base rule-->
  <rule id="100600" level="0">
    <decoded_as>vault</decoded_as>
    <description>Vault operational events.</description>
  </rule>

  <!-- Trace and debug logs -->
  <rule id="100601" level="3">
    <if_sid>100600</if_sid>
    <field name="vault_level">^TRACE$|^DEBUG$</field>
    <description>[Vault operational] Debug log.</description>
  </rule>

  <!-- Informational log, useful for audit trail -->
  <rule id="100602" level="3">
    <if_sid>100600</if_sid>
    <field name="vault_level">^INFO$</field>
    <description>[Vault operational] Informational log from $(vault_component).</description>
  </rule>

  <!-- Warning - may indicate misconfiguration issues -->
  <rule id="100603" level="5">
    <if_sid>100600</if_sid>
    <field name="vault_level">^WARN$|^WARNING$</field>
    <description>[Vault operational] Warning: $(vault_message)</description>
  </rule>

  <!-- Error - requires investigation -->
  <rule id="100604" level="7">
    <if_sid>100600</if_sid>
    <field name="vault_level">^ERROR$</field>
    <description>[Vault operational] Error: $(vault_message)</description>
  </rule>

  <!-- Critical: Vault sealed - all secret access is now blocked. -->
  <rule id="100605" level="12">
    <if_sid>100600</if_sid>
    <match>vault is sealed|marked as sealed</match>
    <description>[Vault operational] Critical: Vault has been sealed.</description>
    <group>service_availability,</group>
  </rule>

  <!-- Info: Vault unsealed - the service is now accessible. -->
  <rule id="100606" level="3">
    <if_sid>100600</if_sid>
    <match>vault is unsealed</match>
    <description>[Vault operational] Info: Vault has been unsealed.</description>
    <group>service_availability,</group>
  </rule>

  <!-- Critical: Audit device failure - this might result in a service unavailability -->
  <rule id="100607" level="10">
    <if_sid>100604</if_sid>
    <match>audit mount failed|</match>
    <description>[Vault operational] Critical: Audit failure - $(vault_message).</description>
  </rule>

  <!-- Vault audit base rule-->
  <rule id="100610" level="0">
    <decoded_as>vault-audit</decoded_as>
    <description>Vault audit events.</description>
  </rule>

  <!-- Catches every API request Vault receives. -->
  <rule id="100611" level="3">
    <if_sid>100610</if_sid>
    <field name="type">request</field>
    <description>[Vault audit] $(request.operation) request performed by $(auth.display_name) on $(request.path).</description>
  </rule>

  <!-- Catches successful response to a request. -->
  <rule id="100612" level="3">
    <if_sid>100610</if_sid>
    <field name="type">response</field>
    <field name="data.error" negate="yes">.+</field>
    <description>[Vault audit] Successful response on path ($(request.path)) by $(auth.display_name).</description>
  </rule>

  <!-- Catches any response containing an error -->
  <rule id="100613" level="7">
    <if_sid>100610</if_sid>
    <field name="type">response</field>
    <field name="data.error">.+</field>
    <description>[Vault audit] Request on path ($(request.path)) by $(auth.display_name) failed.</description>
  </rule>

  <!-- Root token usage -->
  <rule id="100614" level="9">
    <if_sid>100611</if_sid>
    <field name="auth.display_name">^root$</field>
    <description>[Vault audit] Root token used for $(request.operation) on $(request.path).</description>
    <group>pci_dss_10.2.2,</group>
  </rule>

  <!-- Succesfull deletion -->
  <rule id="100615" level="12">
    <if_sid>100612</if_sid>
    <field name="request.operation">^delete$</field>
    <description>[Vault audit] Critical: $(request.path) deleted by $(auth.display_name). Potential footprint cleaning.</description>
    <mitre>
      <id>T1562.001</id>
    </mitre>
  </rule>

  <rule id="100616" level="7">
    <if_sid>100612</if_sid>
    <field name="request.operation">^update$</field>
    <description>[Vault audit] $(request.path) updated by $(auth.display_name).</description>
    <mitre>
      <id>T1562.001</id>
    </mitre>
  </rule>

  <!-- Exfiltration by any user: 10+ reads in 60s -->
  <rule id="100617" level="14" frequency="10" timeframe="60">
    <if_matched_sid>100611</if_matched_sid>
    <field name="request.operation">^read$</field>
    <same_field>auth.accessor</same_field>
    <description>[Vault audit] Potential data exfiltration: $(auth.display_name) performed 10+ reads in 1 minute.</description>
    <mitre>
      <id>T1537</id>
    </mitre>
  </rule>

  <!-- Escalated: root token performs 10+ reads in 60s -->
  <rule id="100618" level="15" frequency="10" timeframe="60">
    <if_matched_sid>100611</if_matched_sid>
    <field name="request.operation">^delete$</field>
    <same_field>auth.accessor</same_field>
    <description>[Vault audit] Potential data destruction: $(auth.display_name) performed 10+ deletes in 1 minute.</description>
    <mitre>
      <id>T1537</id>
    </mitre>
  </rule>

</group>

Where:

  • Rule ID 100600 is the parent rule to identify any events successfully decoded as Vault operational logs.
  • Rule ID 100601 captures TRACE and DEBUG level logs.
  • Rule ID 100602 detects operational events at INFO severity level, such as service startups or component refreshes.
  • Rule ID 100603 detects Vault events with WARN or WARNING severity level, indicating non-fatal issues such as TLS handshake errors.
  • Rule ID 100604 detects Vault events at ERROR severity level, indicating operational failures that require administrator attention.
  • Rule ID 100605 escalates alerts to level 12 when logs indicate the Vault has been sealed, resulting in a complete service outage for secret retrieval.
  • Rule ID 100606 detects Vault unseal events, indicating that the service is now accessible for secret retrieval.
  • Rule ID 100607 escalates “audit mount failed” events to level 12, as this might result in service unavailability.
  • Rule ID 100610 is the parent rule to identify audit events specifically prefixed with [vault audit], distinguishing them from operational logs.
  • Rule ID 100611 captures the initiation of a request, identifying who performed what and on which secret path.
  • Rule ID 100612 captures the system’s response without the data.error field, indicating a successful operation.
  • Rule ID 100613 captures the system’s response with the data.error field, indicating a failed operation.
  • Rule ID 100614 identifies any operation performed by the root user.
  • Rule ID 100615 identifies a successful delete operation.
  • Rule ID 100616 identifies a successful update operation.
  • Rule ID 100617 identifies a potential data exfiltration.
  • Rule ID 100618 identifies a potential data destruction.

Responding to threats

The Wazuh Active Response module allows you to run commands on a monitored endpoint in response to an incident. In this section, we create an active response script to seal the Vault when a suspicious action is detected. It uses the operator seal command to instantly secure the Vault. Once sealed, data becomes inaccessible to all users until the Vault is manually unlocked using the unseal keys. This allows for damage control during a detected intrusion.

Configuration

Ubuntu endpoint

  1. Create a restricted policy that can only seal the Vault:
# vault policy write vault-sealer - <<EOF
path "sys/seal" {
  capabilities = ["sudo", "update"]
}
EOF
  1. Generate a new Vault token attached to the restricted policy. The token value will be saved in /etc/vault.d/vault-seal.token and will serve for seal operations only:
# vault token create -policy="vault-sealer" -period=720h -format=json | jq -r .auth.client_token > /etc/vault.d/vault-seal.token
  1. Restrict access to the /etc/vault.d/vault-seal.token token file:
# chmod 600 /etc/vault.d/vault-seal.token
# chown wazuh:wazuh /etc/vault.d/vault-seal.token
  1. Create a Wazuh active response script, vault-seal.sh, in the /var/ossec/active-response/bin directory with the content below:
Warning: This script is a proof of concept (PoC). Review and validate it to ensure it meets the operational and security requirements of your environment.

This script authenticates to the local Vault instance using the initial root Vault token, then it executes the operator seal command. The script logs the outcome of the seal action to /var/ossec/logs/active-responses.log.

#!/bin/bash
# Wazuh - Vault Seal Active Response

LOG_FILE="/var/ossec/logs/active-responses.log"
VAULT_EXE="/usr/bin/vault"
TOKEN_FILE="/etc/vault.d/vault-seal.token"

#------------------------- Read Wazuh input -----------#
read INPUT_JSON
COMMAND=$(echo "$INPUT_JSON" | grep -o '"command":"[^"]*"' | cut -d'"' -f4)

if [ "$COMMAND" != "add" ]; then
    echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: INFO - Ignoring command '$COMMAND'." >> "$LOG_FILE"
    exit 0
fi

#------------------------- Environment setup ----------------------#
export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_SKIP_VERIFY=true #When using self-signed certificates (Vault default)

# Read token from protected file
if [ ! -f "$TOKEN_FILE" ]; then
    echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: ERROR - Token file not found." >> "$LOG_FILE"
    exit 1
fi
export VAULT_TOKEN=$(cat "$TOKEN_FILE")

#------------------------- Main workflow --------------------------#
$VAULT_EXE status > /dev/null 2>&1
case $? in
    2) echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: INFO - Already sealed." >> "$LOG_FILE"; exit 0 ;;
    1) echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: ERROR - Vault unreachable." >> "$LOG_FILE"; exit 1 ;;
esac

OUTPUT=$($VAULT_EXE operator seal 2>&1)
if [ $? -eq 0 ]; then
    echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: SUCCESS - Vault sealed. INVESTIGATE IMMEDIATELY." >> "$LOG_FILE"
else
    echo "$(date "+%b %d %H:%M:%S") $(hostname) vault-seal: ERROR - Seal failed: $OUTPUT" >> "$LOG_FILE"
    exit 1
fi
  1. Change the Wazuh Active Response script ownership and permissions as shown below:
# chmod 750 /var/ossec/active-response/bin/vault-seal.sh
# chown root:wazuh /var/ossec/active-response/bin/vault-seal.sh
  1. Restart the Wazuh agent to apply the changes:
# systemctl restart wazuh-agent

Wazuh dashboard

Perform the following steps on the Wazuh dashboard to configure active response:

  1. Click the upper left menu and navigate to Server management > Settings.
  2. Click Edit configuration to edit the ossec.conf of the Wazuh manager.
  3. Append the following configuration to the editor to trigger an active response action to rule ID 100617 or 100618. Click Save, then Restart Manager to apply the changes:
<ossec_config>
  <command>
    <name>vault-seal</name>
    <executable>vault-seal.sh</executable>
    <timeout_allowed>no</timeout_allowed>
  </command>

  <active-response>
    <disabled>no</disabled>
    <command>vault-seal</command>
    <location>local</location>
    <rules_id>100617,100618</rules_id>
  </active-response>
</ossec_config>
  1. Navigate to Server management > Decoders, click Manage decoders files, then Custom decoders.
  2. Click the edit button next to the vault_decoder.xml file.
Editing the decoder file.
Figure 1: Editing the decoder file.
  1. Append the decoders below to the file, then click Save:
<decoder name="vault-seal">
  <program_name>^vault-seal$</program_name>
</decoder>

<decoder name="vault-seal-fields">
  <parent>vault-seal</parent>
  <regex type="pcre2">(\w+)\s+-\s+(\w.+)</regex>
  <order>vault_seal_level, vault_seal_message</order>
</decoder>
  1. Navigate to Server management > Rules, click Manage rules files, then Custom rules.
  2. Click the edit button next to the  vault_audit.xml file.
Editing the rules file.
Figure 2: Editing the rules file.
  1. Add the rules below within the <group> block of the file. Click Save, then Reload to apply the changes:
 <!-- Vault seal base rule-->
  <rule id="100620" level="0">
    <decoded_as>vault-seal</decoded_as>
    <description>Vault Active Response logs.</description>
  </rule>

  <!-- Active response confirmation: vault-seal.sh logged SUCCESS.-->
  <rule id="100621" level="12">
    <if_sid>100620</if_sid>
    <field name="vault_seal_level">SUCCESS</field>
    <description>Vault sealed by active response. Immediate investigation required.</description>
    <group>service_availability,active_response,</group>
    <mitre>
      <id>T1537</id>
    </mitre>
  </rule>

  <!-- Active response failure: vault-seal.sh could not seal Vault. -->
  <rule id="100622" level="14">
    <if_sid>100620</if_sid>
    <field name="vault_seal_level">ERROR</field>
    <description>Active response failed to seal Vault. Manual intervention required.</description>
    <group>service_availability,active_response,</group>
  </rule>

  <rule id="100623" level="3">
    <if_sid>100620</if_sid>
    <field name="vault_seal_level">INFO</field>
    <description>Vault Active Response: $(vault_seal_message)</description>
    <group>service_availability,active_response,</group>
  </rule>

Where:

  • Rule ID 100620 is the parent rule for all Vault active response logs.
  • Rule ID 100621 is triggered when a successful automated sealing operation is executed.
  • Rule ID 100622 is triggered when there is a failure in the active response script.
  • Rule ID 100623 identifies informational events.

Testing the integration

We perform the following actions on the Ubuntu endpoint to trigger alerts:

  1. Enable the key-value secret engine at the path secret:
# vault secrets enable -path=secret kv-v2
  1. Create a secret with some data:
# vault kv put secret/myapp/config \
    db_password="supersecret123" \
    api_key="sk-prod-abc123xyz" \
    db_host="prod-db.internal"
  1. Create a policy with read-only permissions on the secret secret/myapp/config:
# vault policy write limited-user - <<EOF
path "secret/data/myapp/config" {
  capabilities = ["read"]
}
EOF
  1. Create a user Bob and attach the limited-user policy:
# vault auth enable userpass
# vault write auth/userpass/users/bob \
    password="password123" \
    policies="limited-user"
  1. Login to the Vault as Bob:
# BOB_TOKEN=$(vault login -method=userpass \
    username=bob \
    password=password123 \
    -format=json | grep -o '"client_token":"[^"]*"' | cut -d'"' -f4)

# export VAULT_TOKEN=$BOB_TOKEN
  1. Try forbidden actions to the Vault:
# vault kv list secret/
# vault kv get secret/otherapp/config
  1. Simulate data exfiltration:
# for i in {1..12}; do
    vault kv get secret/myapp/config > /dev/null 2>&1
    echo "Read $i done"
done

Visualizing alerts on the Wazuh dashboard

Perform the following steps to visualize the alerts generated on the Wazuh dashboard:

  1. Navigate to Threat intelligence > Threat Hunting > Events.
  2. In the search bar, type rule.groups:vault, and click Update.
Vault events.
Figure 3: Vault events.

Click Inspect document details on any alert to have a detailed view. The figure below shows a Vault seal operation corresponding to event ID 100605

Detailed Vault events.
Figure 4: Detailed Vault events.

Conclusion

Monitoring HashiCorp Vault is essential for maintaining visibility, detecting threats, and ensuring the integrity of sensitive secrets management. Using Wazuh, organizations can analyze Vault operational and audit logs to monitor system health and track access patterns. With Wazuh custom rules and the Active Response module, administrators can detect suspicious activities such as data exfiltration and trigger immediate actions such as automated sealing. This integrated approach improves security oversight and supports compliance with security standards.

Discover more about Wazuh by exploring our other blog posts and becoming part of our growing community.

References