mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 11:22:31 +00:00
Merge pull request #414 from pragepani/feature/web-app-wazuh/add-role
feat(web-app-wazuh): add Wazuh SIEM role with OIDC/LDAP RBAC
This commit is contained in:
46 files changed
+2452
-4
No files matched your search
@@ -182,6 +182,7 @@ Every invokable role in the tested lifecycle envelope, with its upstream homepag
|
||||
| [Typesense](roles/svc-db-typesense/) | beta | Central Dockerized search engine shared by multiple apps with per-consumer scoped API keys | [🌐](https://typesense.org/) [🛠️](roles/svc-db-typesense/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
|
||||
| [Unbound](roles/svc-dns-unbound/) | beta | Central recursive/forwarding DNSSEC resolver on a static IP for consumer stacks to target as their DNS | [🌐](https://nlnetlabs.nl/projects/unbound/about/) [🛠️](roles/svc-dns-unbound/README.md#quick-setup) | |
|
||||
| [Universal Logout](roles/web-svc-logout/) | beta | Central logout proxy for single-action sign-out across all OIDC-connected apps, with Nginx snippets and a live status UI | [🛠️](roles/web-svc-logout/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
|
||||
| [Wazuh](roles/web-app-wazuh/) | beta | Open-source SIEM and XDR platform for monitoring, log analysis, intrusion/vulnerability detection, and compliance | [🌐](https://wazuh.com/) [🎬](https://www.youtube.com/watch?v=peTSzcAueEc) [🛠️](roles/web-app-wazuh/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
|
||||
| [web-app-pihole](roles/web-app-pihole/) | beta | Network-wide DNS sinkhole for blocking ads and trackers across all devices, with DHCP and a web admin dashboard | [🌐](https://pi-hole.net/) [🎬](https://youtu.be/KBXTnrD_Zs4) [🛠️](roles/web-app-pihole/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
|
||||
| [WireGuard](roles/svc-net-wireguard/) | beta | Kernel WireGuard tunnels, hub-and-spoke meshes under a swarm manager and full meshes between Compose hosts | [🌐](https://www.wireguard.com/) [🛠️](roles/svc-net-wireguard/README.md#quick-setup) | |
|
||||
| [WordPress](roles/web-app-wordpress/) | beta | Content management system for building and managing websites, blogs, and multisite networks with themes and plugins | [🌐](https://wordpress.org/) [🎬](https://www.youtube.com/watch?v=QU8GFqvXRxE) [🛠️](roles/web-app-wordpress/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
|
||||
|
||||
@@ -464,11 +464,11 @@ An upstream path exists or is claimed, and something concrete stops it here. Eac
|
||||
| `web-app-penpot` | `interactive_browser_session` | The server needs an active browser tab and plugin connection, and can execute powerful design-context operations. |
|
||||
| `web-app-shopware` | `version_unverified` | The pinned 6.7.8.2 predates the experimental native `/api/_mcp` server. |
|
||||
|
||||
### adapter_candidate (38)
|
||||
### adapter_candidate (39)
|
||||
|
||||
An adapter could reach these, and none has been curated yet. The five that used to sit under "current MCP metadata requiring revalidation" are here because that revalidation found no shipped surface to revalidate.
|
||||
|
||||
`svc-db-typesense`, `web-app-akaunting`, `web-app-bigbluebutton`, `web-app-bluesky`, `web-app-bookwyrm`, `web-app-bridgy-fed`, `web-app-confluence`, `web-app-decidim`, `web-app-espocrm`, `web-app-friendica`, `web-app-funkwhale`, `web-app-jira`, `web-app-jitsi`, `web-app-joomla`, `web-app-kix`, `web-app-magento`, `web-app-mailu`, `web-app-mastodon`, `web-app-matrix`, `web-app-mediawiki`, `web-app-minio`, `web-app-mobilizon`, `web-app-opencloud`, `web-app-openproject`, `web-app-opentalk`, `web-app-peertube`, `web-app-pihole`, `web-app-pixelfed`, `web-app-postmarks`, `web-app-seaweedfs`, `web-app-semaphore`, `web-app-socialhome`, `web-app-stalwart`, `web-app-suitecrm`, `web-app-taiga`, `web-app-xwiki`, `web-app-yourls`, `web-svc-xmpp`.
|
||||
`svc-db-typesense`, `web-app-akaunting`, `web-app-bigbluebutton`, `web-app-bluesky`, `web-app-bookwyrm`, `web-app-bridgy-fed`, `web-app-confluence`, `web-app-decidim`, `web-app-espocrm`, `web-app-friendica`, `web-app-funkwhale`, `web-app-jira`, `web-app-jitsi`, `web-app-joomla`, `web-app-kix`, `web-app-magento`, `web-app-mailu`, `web-app-mastodon`, `web-app-matrix`, `web-app-mediawiki`, `web-app-minio`, `web-app-mobilizon`, `web-app-opencloud`, `web-app-openproject`, `web-app-opentalk`, `web-app-peertube`, `web-app-pihole`, `web-app-pixelfed`, `web-app-postmarks`, `web-app-seaweedfs`, `web-app-semaphore`, `web-app-socialhome`, `web-app-stalwart`, `web-app-suitecrm`, `web-app-taiga`, `web-app-wazuh`, `web-app-xwiki`, `web-app-yourls`, `web-svc-xmpp`.
|
||||
|
||||
### enabler (2)
|
||||
|
||||
|
||||
@@ -48,4 +48,9 @@ defaults_oidc:
|
||||
FAMILY_NAME: "surname"
|
||||
EMAIL: "email"
|
||||
|
||||
# Keycloak's own built-in admin realm/client (password-grant Admin REST API automation), distinct from the app's own CLIENT.REALM/CLIENT.ID above.
|
||||
ADMIN:
|
||||
REALM: "master"
|
||||
CLI_CLIENT_ID: "admin-cli"
|
||||
|
||||
OIDC: "{{ defaults_oidc | combine(OIDC_OVERRIDE | default({}, true), recursive=True) }}"
|
||||
@@ -0,0 +1,70 @@
|
||||
# Bcrypt hashing via the raw `bcrypt` package, bypassing Ansible's built-in
|
||||
# `password_hash('bcrypt')` filter.
|
||||
#
|
||||
# `password_hash('bcrypt')` routes through passlib, whose bcrypt backend runs
|
||||
# a self-test (`detect_wrap_bug`) against a fixed known-answer secret on
|
||||
# first use. In environments where the installed `bcrypt` package and
|
||||
# passlib's bundled version-detection drift apart (`bcrypt` has no
|
||||
# `__about__.__version__` attribute passlib expects), that self-test itself
|
||||
# raises "password cannot be longer than 72 bytes" — a misleading error
|
||||
# unrelated to the caller's actual input, and one that makes the filter
|
||||
# unusable for ANY input, not just long ones. The raw `bcrypt` package has no
|
||||
# such self-test and hashes correctly in the same environment.
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
import bcrypt
|
||||
|
||||
_STD_TO_BCRYPT_B64 = bytes.maketrans(
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
||||
)
|
||||
|
||||
|
||||
def _seeded_salt(seed: str, secret: bytes) -> bytes:
|
||||
"""A fixed bcrypt salt derived from the seed and the secret.
|
||||
|
||||
Args:
|
||||
seed: caller-chosen, non-empty string that separates otherwise equal secrets.
|
||||
secret: the UTF-8 encoded secret being hashed.
|
||||
|
||||
Returns:
|
||||
``$2b$12$`` plus 22 bcrypt-base64 characters encoding the first 16
|
||||
bytes of sha256(seed + secret).
|
||||
"""
|
||||
digest = hashlib.sha256(seed.encode("utf-8") + secret).digest()[:16]
|
||||
encoded = base64.b64encode(digest)[:22].translate(_STD_TO_BCRYPT_B64)
|
||||
return b"$2b$12$" + encoded
|
||||
|
||||
|
||||
class FilterModule:
|
||||
def filters(self):
|
||||
return {
|
||||
"bcrypt_hash": self.bcrypt_hash,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def bcrypt_hash(value, salt_seed=None):
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError("bcrypt_hash: value must be a non-empty string")
|
||||
|
||||
secret = value.encode("utf-8")
|
||||
if len(secret) > 72:
|
||||
raise ValueError(
|
||||
f"bcrypt_hash: secret is {len(secret)} bytes in UTF-8; bcrypt "
|
||||
"accepts at most 72 bytes. Shorten the secret instead of "
|
||||
"letting bcrypt truncate or reject it."
|
||||
)
|
||||
|
||||
if salt_seed is None:
|
||||
salt = bcrypt.gensalt()
|
||||
elif isinstance(salt_seed, str) and salt_seed:
|
||||
salt = _seeded_salt(salt_seed, secret)
|
||||
else:
|
||||
raise ValueError(
|
||||
"bcrypt_hash: salt_seed must be a non-empty string when given"
|
||||
)
|
||||
|
||||
return bcrypt.hashpw(secret, salt).decode("utf-8")
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Keycloak OIDC login helpers.
|
||||
* Keycloak OIDC login helpers, plus Keycloak Admin REST API helpers for
|
||||
* Playwright specs that need to manipulate group membership directly
|
||||
* (RBAC-tier tests across web-app-* roles).
|
||||
*
|
||||
* `performKeycloakLoginForm(target, username, password)`
|
||||
* Fills the Keycloak login form on `target` (a `Page` OR a
|
||||
@@ -23,6 +25,21 @@
|
||||
* or a 401 / 403 on the relying party after the callback).
|
||||
* Returns the resulting URL so callers can assert additional
|
||||
* details if needed.
|
||||
*
|
||||
* `keycloakAdminToken(request, keycloakBaseUrl, username, password, opts)`
|
||||
* Password-grant token fetch against Keycloak's own bootstrap realm
|
||||
* (default `master`/`admin-cli`, both overridable via `opts` since
|
||||
* they are deployment-configurable, not framework constants).
|
||||
*
|
||||
* `keycloakResolveGroupId(request, keycloakBaseUrl, realmName, accessToken, groupPath)`
|
||||
* Resolves a group's internal id from its human-readable path.
|
||||
*
|
||||
* `keycloakAdminAddUserToGroup(request, keycloakBaseUrl, realmName, targetGroupPath, username, adminUsername, adminPassword, opts)`
|
||||
* Idempotent group join; returns whether this call performed it.
|
||||
*
|
||||
* `keycloakRemoveUserFromGroupViaRest(request, keycloakBaseUrl, realmName, adminUsername, adminPassword, groupPath, username, opts)`
|
||||
* Best-effort group removal (throws on lookup failure, no-ops if the
|
||||
* user/group is already gone).
|
||||
*/
|
||||
|
||||
const { expect } = require("@playwright/test");
|
||||
@@ -178,10 +195,206 @@ async function performKeycloakLoginExpectingDenial(page, username, password, can
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
async function keycloakAdminToken(
|
||||
request,
|
||||
keycloakBaseUrl,
|
||||
username,
|
||||
password,
|
||||
{ adminRealm = "master", adminClientId = "admin-cli" } = {},
|
||||
) {
|
||||
const tokenResp = await request.post(
|
||||
`${keycloakBaseUrl}/realms/${encodeURIComponent(adminRealm)}/protocol/openid-connect/token`,
|
||||
{
|
||||
form: {
|
||||
client_id: adminClientId,
|
||||
grant_type: "password",
|
||||
username,
|
||||
password,
|
||||
},
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
},
|
||||
);
|
||||
if (!tokenResp.ok()) {
|
||||
throw new Error(
|
||||
`Keycloak admin token request failed: ${tokenResp.status()} ${await tokenResp.text()}`,
|
||||
);
|
||||
}
|
||||
const json = await tokenResp.json();
|
||||
if (!json.access_token) {
|
||||
throw new Error("Keycloak admin token response missing access_token");
|
||||
}
|
||||
return json.access_token;
|
||||
}
|
||||
|
||||
// Resolves a group's internal Keycloak id from its human-readable path
|
||||
// (e.g. "/roles/web-app-wazuh/administrator"). Tries the direct
|
||||
// group-by-path admin endpoint first, since it is a single request; some
|
||||
// Keycloak versions 404 that endpoint for paths containing certain
|
||||
// characters, so on failure this falls back to walking the group tree one
|
||||
// path segment at a time (list top-level groups filtered by name, then
|
||||
// each subsequent segment's children), matching by exact name at each
|
||||
// depth, and returns the id of the final matched segment.
|
||||
async function keycloakResolveGroupId(request, keycloakBaseUrl, realmName, accessToken, groupPath) {
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
const trimmed = groupPath.replace(/^\//, "");
|
||||
const byPath = await request.get(
|
||||
`${keycloakBaseUrl}/admin/realms/${encodeURIComponent(realmName)}/group-by-path/${trimmed}`,
|
||||
{ headers },
|
||||
);
|
||||
if (byPath.ok()) {
|
||||
const group = await byPath.json();
|
||||
if (group?.id) return group.id;
|
||||
}
|
||||
const segments = trimmed.split("/").filter((s) => s !== "");
|
||||
if (segments.length === 0) {
|
||||
throw new Error(`Empty Keycloak group path: ${groupPath}`);
|
||||
}
|
||||
let parentId = null;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const wanted = segments[i];
|
||||
const url =
|
||||
parentId === null
|
||||
? `${keycloakBaseUrl}/admin/realms/${realmName}/groups?max=500&search=${encodeURIComponent(wanted)}`
|
||||
: `${keycloakBaseUrl}/admin/realms/${realmName}/groups/${parentId}/children?max=500`;
|
||||
const resp = await request.get(url, { headers });
|
||||
if (!resp.ok()) {
|
||||
throw new Error(
|
||||
`Keycloak groups lookup failed at segment ${i} (${wanted}): ${resp.status()} ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
const items = await resp.json();
|
||||
let match;
|
||||
if (parentId === null) {
|
||||
// The top-level search endpoint returns full subtrees, not just
|
||||
// direct matches, so walk each returned tree looking for `wanted`
|
||||
// at depth 0 only - a same-named descendant deeper in the tree
|
||||
// MUST NOT be mistaken for the top-level segment.
|
||||
const walk = (nodes, depth) => {
|
||||
for (const n of nodes) {
|
||||
if (depth === 0 && n.name === wanted) return n;
|
||||
if (n.subGroups && n.subGroups.length) {
|
||||
const r = walk(n.subGroups, depth - 1);
|
||||
if (r) return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
match = walk(items, 0);
|
||||
} else {
|
||||
match = items.find((n) => n.name === wanted) || null;
|
||||
}
|
||||
if (!match) {
|
||||
throw new Error(`Keycloak group "${groupPath}" not found while resolving segment "${wanted}"`);
|
||||
}
|
||||
parentId = match.id;
|
||||
}
|
||||
return parentId;
|
||||
}
|
||||
|
||||
// Adds `username` to the group at `targetGroupPath`, fetching its own
|
||||
// admin token internally. Returns true when this call performed the join
|
||||
// (caller MUST tear down), false when the user was already a member
|
||||
// (caller MUST NOT remove it, since removal would then affect membership
|
||||
// this call did not grant).
|
||||
async function keycloakAdminAddUserToGroup(
|
||||
request,
|
||||
keycloakBaseUrl,
|
||||
realmName,
|
||||
targetGroupPath,
|
||||
username,
|
||||
adminUsername,
|
||||
adminPassword,
|
||||
opts = {},
|
||||
) {
|
||||
const accessToken = await keycloakAdminToken(request, keycloakBaseUrl, adminUsername, adminPassword, opts);
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
|
||||
const userResp = await request.get(
|
||||
`${keycloakBaseUrl}/admin/realms/${realmName}/users?username=${encodeURIComponent(username)}&exact=true`,
|
||||
{ headers },
|
||||
);
|
||||
if (!userResp.ok()) {
|
||||
throw new Error(`Keycloak user lookup failed: ${userResp.status()} ${await userResp.text()}`);
|
||||
}
|
||||
const users = await userResp.json();
|
||||
const user = users.find((u) => u.username === username);
|
||||
if (!user) {
|
||||
throw new Error(`Keycloak user "${username}" not found`);
|
||||
}
|
||||
|
||||
const groupId = await keycloakResolveGroupId(request, keycloakBaseUrl, realmName, accessToken, targetGroupPath);
|
||||
|
||||
const memberResp = await request.get(
|
||||
`${keycloakBaseUrl}/admin/realms/${realmName}/users/${user.id}/groups?max=500`,
|
||||
{ headers },
|
||||
);
|
||||
if (!memberResp.ok()) {
|
||||
throw new Error(`Keycloak user-groups lookup failed: ${memberResp.status()} ${await memberResp.text()}`);
|
||||
}
|
||||
const currentGroups = await memberResp.json();
|
||||
if (currentGroups.some((g) => g.id === groupId || g.path === targetGroupPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const joinResp = await request.put(
|
||||
`${keycloakBaseUrl}/admin/realms/${realmName}/users/${user.id}/groups/${groupId}`,
|
||||
{ headers },
|
||||
);
|
||||
if (!joinResp.ok()) {
|
||||
throw new Error(
|
||||
`Keycloak join-group failed (user=${username}, group=${targetGroupPath}): ${joinResp.status()} ${await joinResp.text()}`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function keycloakRemoveUserFromGroupViaRest(
|
||||
request,
|
||||
keycloakBaseUrl,
|
||||
realmName,
|
||||
adminUsername,
|
||||
adminPassword,
|
||||
groupPath,
|
||||
username,
|
||||
opts = {},
|
||||
) {
|
||||
const accessToken = await keycloakAdminToken(request, keycloakBaseUrl, adminUsername, adminPassword, opts);
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
|
||||
const userResp = await request.get(
|
||||
`${keycloakBaseUrl}/admin/realms/${realmName}/users?username=${encodeURIComponent(username)}&exact=true`,
|
||||
{ headers },
|
||||
);
|
||||
if (!userResp.ok()) {
|
||||
throw new Error(`Keycloak user lookup failed: ${userResp.status()} ${await userResp.text()}`);
|
||||
}
|
||||
const users = await userResp.json();
|
||||
const user = users.find((u) => u.username === username);
|
||||
if (!user) {
|
||||
throw new Error(`Keycloak user "${username}" not found`);
|
||||
}
|
||||
|
||||
const groupId = await keycloakResolveGroupId(request, keycloakBaseUrl, realmName, accessToken, groupPath);
|
||||
|
||||
const leaveResp = await request.delete(
|
||||
`${keycloakBaseUrl}/admin/realms/${realmName}/users/${user.id}/groups/${groupId}`,
|
||||
{ headers },
|
||||
);
|
||||
if (!leaveResp.ok()) {
|
||||
throw new Error(
|
||||
`Keycloak leave-group failed (user=${username}, group=${groupPath}): ${leaveResp.status()} ${await leaveResp.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OIDC_LOGIN_READY_FLAG,
|
||||
performKeycloakLoginForm,
|
||||
performKeycloakLogin,
|
||||
clickOidcLoginLink,
|
||||
performKeycloakLoginExpectingDenial,
|
||||
keycloakAdminToken,
|
||||
keycloakResolveGroupId,
|
||||
keycloakAdminAddUserToGroup,
|
||||
keycloakRemoveUserFromGroupViaRest,
|
||||
};
|
||||
@@ -82,7 +82,11 @@ function menuTriggerCandidatesOn(scope) {
|
||||
scope.getByRole("button", { name: ACCOUNT_MENU_NAME_RE }),
|
||||
scope.getByRole("link", { name: ACCOUNT_MENU_NAME_RE }),
|
||||
scope.locator(
|
||||
"[data-bs-toggle='dropdown'], .dropdown-toggle, [aria-haspopup='menu'], [aria-haspopup='true'], [data-region='user-menu-toggle'], .user-menu-toggle, .usermenu, [aria-label*='user menu' i], [aria-label*='account' i], [data-testid*='user' i]:not(input):not(textarea):not(select)",
|
||||
|
||||
// #user-icon-btn: OpenSearch/Kibana Dashboards' user-avatar trigger has no accessible name or aria-haspopup, so it needs an explicit id selector.
|
||||
"[data-bs-toggle='dropdown'], .dropdown-toggle, [aria-haspopup='menu'], [aria-haspopup='true'], [data-region='user-menu-toggle'], .user-menu-toggle, .usermenu, [aria-label*='user menu' i], [aria-label*='account' i], [data-testid*='user' i]:not(input):not(textarea):not(select), #user-icon-btn",
|
||||
|
||||
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Wazuh
|
||||
|
||||
## Description
|
||||
|
||||
[Wazuh](https://wazuh.com/) is an open-source SIEM and XDR platform providing security monitoring, log analysis, intrusion and vulnerability detection, and compliance support.
|
||||
|
||||
## Overview
|
||||
|
||||
This role deploys the single-node Wazuh stack (`wazuh.manager`, `wazuh.indexer`, `wazuh.dashboard`) with Docker Compose or Docker Swarm.
|
||||
A one-shot `wazuh-certs-generator` job creates the internal TLS material before the manager and the indexer start.
|
||||
The indexer is the stack's own datastore and holds the OpenSearch security configuration, so the role needs no shared database.
|
||||
Agent enrollment is not part of this role: the agent ports are declared in [services.yml](./meta/services.yml) but not published.
|
||||
The manager's vulnerability detection data (`/var/ossec/queue/vd`, volume `wazuh_manager_vd`) is deliberately not backed up.
|
||||
The manager downloads it again after a restore, which can take a long time.
|
||||
This matters once agents are enrolled.
|
||||
|
||||
## Cosmos
|
||||
|
||||
The diagram places Wazuh in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph deps [Dependencies]
|
||||
dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"]
|
||||
dep_svc_db_openldap["svc-db-openldap 🐳🐝"]
|
||||
dep_svc_net_tor["svc-net-tor 🐳🐝"]
|
||||
dep_web_app_dashboard["web-app-dashboard 🐳🐝"]
|
||||
dep_web_app_keycloak["web-app-keycloak 🐳🐝"]
|
||||
dep_web_app_matomo["web-app-matomo 🐳🐝"]
|
||||
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
|
||||
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
|
||||
dep_web_svc_css["web-svc-css 💻"]
|
||||
dep_web_svc_logout["web-svc-logout 🐳🐝"]
|
||||
end
|
||||
subgraph role [web-app-wazuh 🐳🐝]
|
||||
svc_email["email ❌"]
|
||||
svc_sso["sso"]
|
||||
svc_ldap["ldap"]
|
||||
svc_logout["logout"]
|
||||
svc_dashboard["dashboard"]
|
||||
svc_matomo["matomo"]
|
||||
svc_prometheus["prometheus"]
|
||||
svc_css["css"]
|
||||
svc_container_backup["container_backup"]
|
||||
svc_wazuh["wazuh"]
|
||||
svc_certs_generator["certs_generator"]
|
||||
svc_manager["manager"]
|
||||
svc_indexer["indexer"]
|
||||
svc_console["console"]
|
||||
svc_tor["tor"]
|
||||
end
|
||||
dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup
|
||||
dep_svc_db_openldap -. "0..1" .-> svc_ldap
|
||||
dep_svc_net_tor -. "0..1" .-> svc_tor
|
||||
dep_web_app_dashboard -. "0..1" .-> svc_dashboard
|
||||
dep_web_app_keycloak -. "0..1" .-> svc_sso
|
||||
dep_web_app_matomo -. "0..1" .-> svc_matomo
|
||||
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
|
||||
dep_web_app_stalwart -- "0..0" --> svc_email
|
||||
dep_web_svc_css -. "0..1" .-> svc_css
|
||||
dep_web_svc_logout -. "0..1" .-> svc_logout
|
||||
linkStyle 7 stroke:red;
|
||||
```
|
||||
|
||||
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single-node Wazuh stack:** Manager, indexer, and dashboard at the versions pinned in [services.yml](./meta/services.yml).
|
||||
- **Native OIDC login:** The dashboard signs users in against Keycloak's discovery endpoint, without an oauth2-proxy sidecar.
|
||||
- **LDAP authorization:** LDAP group membership adds `backend_roles` to an already authenticated user and is never used to verify a password.
|
||||
- **Three-tier RBAC:** Administrator, Security Analyst, and Read-only Auditor, declared in [rbac.yml](./meta/rbac.yml) and mapped onto OpenSearch security roles.
|
||||
- **Generated credentials:** Every service account password and the manager cluster key come from [secrets.yml](./meta/secrets.yml).
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### Development
|
||||
|
||||
Clone, set up the workstation, and deploy Wazuh onto the local stack:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/infinito-nexus/core.git
|
||||
cd core
|
||||
make onboard
|
||||
make compose-deploy mode=reinstall apps=web-app-wazuh full_cycle=false
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
Run the published image to provision the inventory and deploy Wazuh to a managed server (the mounted volume persists the inventory):
|
||||
|
||||
```bash
|
||||
APP=web-app-wazuh
|
||||
HOST=<your-server>
|
||||
DOMAIN=<your-domain>
|
||||
TLS_MODE=self_signed
|
||||
SSH_PUBLIC_KEY="<your-ssh-public-key>"
|
||||
|
||||
docker run --rm -it \
|
||||
-v "$PWD/inventories:/etc/infinito.nexus/inventories" \
|
||||
-e APP="$APP" -e HOST="$HOST" -e DOMAIN="$DOMAIN" -e TLS_MODE="$TLS_MODE" -e SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
|
||||
ghcr.io/infinito-nexus/core/debian bash -c '
|
||||
INVENTORY=/etc/infinito.nexus/inventories/production
|
||||
infinito administration inventory provision "$INVENTORY" \
|
||||
--inventory-file "$INVENTORY/devices.yml" \
|
||||
--host "$HOST" \
|
||||
--include "$APP" \
|
||||
--vars "{\"TLS_MODE\": \"$TLS_MODE\", \"DOMAIN_PRIMARY\": \"$DOMAIN\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}" &&
|
||||
infinito administration deploy dedicated "$INVENTORY/devices.yml" \
|
||||
--password-file "$INVENTORY/.password" \
|
||||
--diff -vv'
|
||||
```
|
||||
|
||||
## Auth flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as Browser (biber)
|
||||
participant D as wazuh.dashboard
|
||||
participant I as wazuh.indexer (OpenSearch Security)
|
||||
participant K as Keycloak
|
||||
participant L as LDAP (authz only)
|
||||
|
||||
U->>D: GET / (no session)
|
||||
D->>I: forward request
|
||||
Note over I: basic_internal_auth_domain, order 0<br/>no Authorization header, falls through
|
||||
Note over I: openid_auth_domain, order 1, challenge true
|
||||
I-->>D: 401, redirect to Keycloak
|
||||
D-->>U: redirect to Keycloak login
|
||||
U->>K: submit credentials
|
||||
K-->>U: redirect back with auth code
|
||||
U->>D: callback with auth code
|
||||
D->>K: exchange code for tokens
|
||||
K-->>D: id_token, groups claim, full.path=true
|
||||
D->>I: authenticated request, bearer token
|
||||
Note over I: subject_key=preferred_username<br/>roles_key=groups maps to backend_roles
|
||||
I->>L: enrich backend_roles via ldap_authz_domain<br/>CN lookup, skips admin and kibanaserver
|
||||
L-->>I: additional backend_roles, if matched
|
||||
Note over I: roles_mapping.yml maps backend_roles<br/>to all_access, security_analyst, or readonly_auditor
|
||||
I-->>D: authorized response
|
||||
D-->>U: authenticated dashboard, RBAC-scoped
|
||||
```
|
||||
|
||||
The internal `admin` and `kibanaserver` service accounts authenticate with HTTP basic auth against `internal_users.yml` and bypass OIDC and LDAP.
|
||||
With `sso` disabled, these two accounts are the only way to sign in.
|
||||
|
||||
## RBAC
|
||||
|
||||
| Role | OpenSearch role | `backend_roles` (OIDC) | `backend_roles` (LDAP) |
|
||||
|---|---|---|---|
|
||||
| Administrator | `all_access` | `/roles/web-app-wazuh/administrator` | `web-app-wazuh-administrator` |
|
||||
| Security Analyst | `security_analyst` | `/roles/web-app-wazuh/security-analyst` | `web-app-wazuh-security-analyst` |
|
||||
| Read-only Auditor | `readonly_auditor`, `kibana_read_only` | `/roles/web-app-wazuh/readonly-auditor` | `web-app-wazuh-readonly-auditor` |
|
||||
|
||||
- Grant a role by adding the user to the matching Keycloak group or LDAP group.
|
||||
- A group change applies on the user's next request, because the indexer's auth cache is disabled.
|
||||
- Only Administrators reach the OpenSearch Dashboards Security app.
|
||||
- Every role lands on `/app/home` after login.
|
||||
|
||||
## Developer Notes
|
||||
|
||||
Variant matrix: [variants.yml](./meta/variants.yml). Service flags and image pins: [services.yml](./meta/services.yml). Credentials: [secrets.yml](./meta/secrets.yml). RBAC roles: [rbac.yml](./meta/rbac.yml).
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [Wazuh Official Website](https://wazuh.com/)
|
||||
- [Wazuh Documentation](https://documentation.wazuh.com/current/)
|
||||
- [wazuh-docker GitHub](https://github.com/wazuh/wazuh-docker)
|
||||
|
||||
## Credits
|
||||
|
||||
Implemented by **[Prageeth Panicker](https://github.com/pragepani)**.
|
||||
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
|
||||
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).
|
||||
@@ -0,0 +1 @@
|
||||
# nocheck: role-files-layout - Playwright discovers this suite by path; the runner copies the tree as it stands and the specs import their helpers relatively. Sorting by extension would break discovery.
|
||||
@@ -0,0 +1,126 @@
|
||||
const { expect } = require("@playwright/test");
|
||||
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("./personas");
|
||||
const { resolveTimeout } = require("./timeouts");
|
||||
const keycloakAdmin = require("./personas/utils/keycloak");
|
||||
|
||||
const env = {
|
||||
appBaseUrl: normalizeBaseUrl(process.env.APP_BASE_URL || ""),
|
||||
keycloakBaseUrl: normalizeBaseUrl(process.env.KEYCLOAK_BASE_URL || ""),
|
||||
realmName: decodeDotenvQuotedValue(process.env.KEYCLOAK_REALM_NAME),
|
||||
superAdminUsername: decodeDotenvQuotedValue(process.env.SUPER_ADMIN_USERNAME),
|
||||
superAdminPassword: decodeDotenvQuotedValue(process.env.SUPER_ADMIN_PASSWORD),
|
||||
adminUsername: decodeDotenvQuotedValue(process.env.ADMIN_USERNAME),
|
||||
adminPassword: decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD),
|
||||
biberUsername: decodeDotenvQuotedValue(process.env.BIBER_USERNAME),
|
||||
biberPassword: decodeDotenvQuotedValue(process.env.BIBER_PASSWORD),
|
||||
keycloakAdminRealm: decodeDotenvQuotedValue(process.env.KEYCLOAK_ADMIN_REALM),
|
||||
keycloakAdminCliClientId: decodeDotenvQuotedValue(process.env.KEYCLOAK_ADMIN_CLI_CLIENT_ID),
|
||||
rbacGroupPathPrefix: decodeDotenvQuotedValue(process.env.RBAC_GROUP_PATH_PREFIX),
|
||||
};
|
||||
|
||||
function attachDiagnostics(page) {
|
||||
const consoleErrors = [];
|
||||
const pageErrors = [];
|
||||
const cspRelated = [];
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") consoleErrors.push(message.text());
|
||||
if (/content security policy|csp/i.test(message.text())) {
|
||||
cspRelated.push({ source: "console", text: message.text() });
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
const text = String(error);
|
||||
pageErrors.push(text);
|
||||
if (/content security policy|csp/i.test(text)) {
|
||||
cspRelated.push({ source: "pageerror", text });
|
||||
}
|
||||
});
|
||||
return { consoleErrors, pageErrors, cspRelated };
|
||||
}
|
||||
|
||||
async function wazuhLoginViaOidc(page, appBaseUrl, username, password) {
|
||||
await gotoOnion(page, `${appBaseUrl}/`, { waitUntil: "domcontentloaded" });
|
||||
|
||||
if (!page.url().includes("openid-connect/auth")) {
|
||||
const strictLink = page
|
||||
.getByRole("link", { name: /^\s*(log\s*in|sign\s*in|login|sso)\s*$/i })
|
||||
.or(page.getByRole("button", { name: /^\s*(log\s*in|sign\s*in|login|sso)\s*$/i }))
|
||||
.first();
|
||||
const looseLink = page
|
||||
.getByRole("link", { name: /log\s*in|sign\s*in|sso/i })
|
||||
.or(page.getByRole("button", { name: /log\s*in|sign\s*in|sso/i }))
|
||||
.first();
|
||||
await keycloakAdmin.clickOidcLoginLink(page, strictLink, looseLink);
|
||||
}
|
||||
|
||||
if (page.url().includes("openid-connect/auth")) {
|
||||
await keycloakAdmin.performKeycloakLoginForm(page, username, password);
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() => page.url(), {
|
||||
timeout: resolveTimeout(60_000),
|
||||
message: `Expected redirect back to ${appBaseUrl} after OIDC login`,
|
||||
})
|
||||
.toContain(new URL(appBaseUrl).host);
|
||||
await expect
|
||||
.poll(async () => (await page.context().cookies()).length, {
|
||||
timeout: resolveTimeout(30_000),
|
||||
message:
|
||||
`Expected a real session cookie after OIDC login to ${appBaseUrl} ` +
|
||||
`(got 0 - the URL matched but no session was actually established)`,
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
async function withBiberInGroup(browser, groupPath, fn) {
|
||||
const adminOpts = { adminRealm: env.keycloakAdminRealm, adminClientId: env.keycloakAdminCliClientId };
|
||||
const adminCtx = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
let biberAdded = false;
|
||||
let bodyFailed = false;
|
||||
try {
|
||||
biberAdded = await keycloakAdmin.keycloakAdminAddUserToGroup(
|
||||
adminCtx.request,
|
||||
env.keycloakBaseUrl,
|
||||
env.realmName,
|
||||
groupPath,
|
||||
env.biberUsername,
|
||||
env.superAdminUsername,
|
||||
env.superAdminPassword,
|
||||
adminOpts,
|
||||
);
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
bodyFailed = true;
|
||||
throw err;
|
||||
} finally {
|
||||
try {
|
||||
if (biberAdded) {
|
||||
await keycloakAdmin
|
||||
.keycloakRemoveUserFromGroupViaRest(
|
||||
adminCtx.request,
|
||||
env.keycloakBaseUrl,
|
||||
env.realmName,
|
||||
env.superAdminUsername,
|
||||
env.superAdminPassword,
|
||||
groupPath,
|
||||
env.biberUsername,
|
||||
adminOpts,
|
||||
)
|
||||
.catch((err) => {
|
||||
if (!bodyFailed) throw err;
|
||||
console.warn(`Cleanup removal of biber from ${groupPath} failed: ${err}`);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await adminCtx.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
env,
|
||||
attachDiagnostics,
|
||||
wazuhLoginViaOidc,
|
||||
withBiberInGroup,
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
const { test } = require("@playwright/test");
|
||||
|
||||
const testBaseline = require("./test-baseline");
|
||||
const testRbacRoles = require("./test-rbac-roles");
|
||||
require("./test-csp");
|
||||
|
||||
test.describe.serial("wazuh: biber Keycloak group membership (single-owner)", () => {
|
||||
testBaseline.registerBiberBaseline();
|
||||
testRbacRoles.register();
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { runGuestFlow, runBiberFlow, runAdminFlow, gotoOnion } = require("./personas");
|
||||
const { skipUnlessServiceEnabled } = require("./service-gating");
|
||||
const { resolveTimeout } = require("./timeouts");
|
||||
const shared = require("./_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
test("guest: public-landing → auth chain → never authenticated", async ({ page }) => {
|
||||
await runGuestFlow(page);
|
||||
});
|
||||
|
||||
function registerBiberBaseline() {
|
||||
test("biber: dashboard → keycloak → view alerts → logout", async ({ page, browser }) => {
|
||||
skipUnlessServiceEnabled("sso");
|
||||
await shared.withBiberInGroup(browser, `${shared.env.rbacGroupPathPrefix}readonly-auditor`, () =>
|
||||
runBiberFlow(page, {
|
||||
biberInteraction: async (p) => {
|
||||
await expect(p.locator("body")).toContainText(/home|manage|dev tools/i, {
|
||||
timeout: resolveTimeout(30_000),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test("administrator: app → keycloak → admin action → logout", async ({ page }) => {
|
||||
await runAdminFlow(page, {
|
||||
adminInteraction: async (p) => {
|
||||
await gotoOnion(p, `${shared.env.appBaseUrl}/app/security-dashboards-plugin#/roles`);
|
||||
await expect(p.locator("body")).toContainText(/security|roles|users|management/i, {
|
||||
timeout: resolveTimeout(30_000),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = { registerBiberBaseline };
|
||||
@@ -0,0 +1,76 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const {
|
||||
installCspViolationObserver,
|
||||
assertCspResponseHeader,
|
||||
assertCspMetaParity,
|
||||
expectNoCspViolations,
|
||||
gotoOnion,
|
||||
} = require("./personas");
|
||||
const { skipUnlessServiceEnabled } = require("./service-gating");
|
||||
const { resolveTimeout } = require("./timeouts");
|
||||
const shared = require("./_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
const CONSOLE_EDITOR_SELECTORS = [
|
||||
".ace_text-input",
|
||||
".monaco-editor textarea",
|
||||
"[data-test-subj='console-textarea'] textarea",
|
||||
".ace_editor textarea",
|
||||
];
|
||||
|
||||
async function typeIntoDevToolsConsole(page) {
|
||||
const response = await gotoOnion(page, `${shared.env.appBaseUrl}/app/dev_tools#/console`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForLoadState("networkidle").catch(() => {});
|
||||
if (!response || response.status() >= 400) {
|
||||
throw new Error(`dev tools console answered ${response ? response.status() : "no response"}`);
|
||||
}
|
||||
const editor = page.locator(CONSOLE_EDITOR_SELECTORS.join(", ")).first();
|
||||
await editor.waitFor({ state: "visible", timeout: resolveTimeout(15_000) });
|
||||
await page
|
||||
.getByRole("button", { name: /dismiss|close this dialog/i })
|
||||
.first()
|
||||
.click({ timeout: resolveTimeout(5_000) })
|
||||
.catch(() => {});
|
||||
await editor.focus({ timeout: resolveTimeout(10_000) });
|
||||
await page.keyboard.type("GET _cluster/health", { delay: 50 });
|
||||
await page.waitForTimeout(resolveTimeout(3_000));
|
||||
}
|
||||
|
||||
test("wazuh dashboard: script-src-elem unsafe-inline and worker-src blob: are declared and exercised without violation", async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessServiceEnabled("sso");
|
||||
|
||||
const diagnostics = shared.attachDiagnostics(page);
|
||||
await installCspViolationObserver(page);
|
||||
|
||||
await shared.wazuhLoginViaOidc(
|
||||
page,
|
||||
shared.env.appBaseUrl,
|
||||
shared.env.adminUsername,
|
||||
shared.env.adminPassword,
|
||||
);
|
||||
|
||||
const response = await gotoOnion(page, `${shared.env.appBaseUrl}/app/home`);
|
||||
expect(response, "Expected Wazuh dashboard home response").toBeTruthy();
|
||||
expect(
|
||||
response.status(),
|
||||
"Expected Wazuh dashboard home response to be successful",
|
||||
).toBeLessThan(400);
|
||||
|
||||
const directives = assertCspResponseHeader(response, "wazuh dashboard home");
|
||||
await assertCspMetaParity(page, directives, "wazuh dashboard home");
|
||||
|
||||
expect(
|
||||
directives["script-src-elem"] || [],
|
||||
"Expected script-src-elem to declare 'unsafe-inline'",
|
||||
).toContain("'unsafe-inline'");
|
||||
expect(directives["worker-src"] || [], "Expected worker-src to grant blob:").toContain("blob:");
|
||||
|
||||
await typeIntoDevToolsConsole(page);
|
||||
|
||||
await expectNoCspViolations(page, diagnostics, "wazuh dashboard (home + dev tools console)");
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { installCspViolationObserver, gotoOnion } = require("./personas");
|
||||
const { skipUnlessServiceEnabled } = require("./service-gating");
|
||||
const { resolveTimeout } = require("./timeouts");
|
||||
const shared = require("./_shared");
|
||||
|
||||
const RBAC_TIERS = [
|
||||
{ role: "administrator", expectSecurityUi: true },
|
||||
{ role: "security-analyst", expectSecurityUi: false },
|
||||
{ role: "readonly-auditor", expectSecurityUi: false },
|
||||
];
|
||||
|
||||
async function seenBy(page) {
|
||||
const body = await page
|
||||
.locator("body")
|
||||
.innerText({ timeout: resolveTimeout(5_000) })
|
||||
.catch((err) => `<body text unavailable: ${err.message}>`);
|
||||
return `page: ${page.url()}, body: ${body.slice(0, 1500)}`;
|
||||
}
|
||||
|
||||
exports.register = function () {
|
||||
for (const tier of RBAC_TIERS) {
|
||||
test(`rbac: membership in ${tier.role} group grants the expected Wazuh UI surface`, async ({
|
||||
browser,
|
||||
}) => {
|
||||
skipUnlessServiceEnabled("sso");
|
||||
await shared.withBiberInGroup(browser, `${shared.env.rbacGroupPathPrefix}${tier.role}`, async () => {
|
||||
const biberCtx = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
try {
|
||||
const page = await biberCtx.newPage();
|
||||
await installCspViolationObserver(page);
|
||||
await shared.wazuhLoginViaOidc(
|
||||
page,
|
||||
shared.env.appBaseUrl,
|
||||
shared.env.biberUsername,
|
||||
shared.env.biberPassword,
|
||||
);
|
||||
|
||||
const nav = await gotoOnion(page, `${shared.env.appBaseUrl}/app/security-dashboards-plugin#/roles`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
expect(
|
||||
nav ? nav.status() : 0,
|
||||
`${tier.role} navigation to the Security management UI failed (${await seenBy(page)})`,
|
||||
).toBeLessThan(400);
|
||||
const roleTable = page.locator("[data-test-subj='role-list']");
|
||||
|
||||
if (tier.expectSecurityUi) {
|
||||
await roleTable
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: resolveTimeout(30_000) })
|
||||
.catch(async (err) => {
|
||||
throw new Error(
|
||||
`${tier.role} MUST reach the Security management UI (${await seenBy(page)}): ${err.message}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await page.waitForTimeout(resolveTimeout(10_000));
|
||||
const seen = await seenBy(page);
|
||||
// Exception: the explicit denial text is not verified yet; these two checks stand in for it until CI shows what a denied user sees.
|
||||
expect(
|
||||
page.url().startsWith(shared.env.appBaseUrl),
|
||||
`${tier.role} MUST be denied inside the Wazuh app, not redirected away (${seen})`,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(await page.locator("body").innerText({ timeout: resolveTimeout(5_000) })).trim(),
|
||||
`${tier.role} MUST see a denial or empty-permission page, not a blank page (${seen})`,
|
||||
).not.toBe("");
|
||||
await expect(
|
||||
roleTable,
|
||||
`${tier.role} MUST NOT reach the Security management UI (role table rendered, ${seen})`,
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.locator("[data-test-subj='create-role']"),
|
||||
`${tier.role} MUST NOT reach the Security management UI (create-role button rendered, ${seen})`,
|
||||
).toHaveCount(0);
|
||||
}
|
||||
} finally {
|
||||
await biberCtx.close().catch(() => {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
_meta:
|
||||
type: "roles"
|
||||
config_version: 2
|
||||
|
||||
security_analyst:
|
||||
reserved: false
|
||||
description: "Security Analyst: read alerts/logs/dashboards, no security-config or RBAC-management access."
|
||||
cluster_permissions:
|
||||
- "cluster_composite_ops_ro"
|
||||
index_permissions:
|
||||
- index_patterns:
|
||||
- "wazuh-alerts-*"
|
||||
- "wazuh-archives-*"
|
||||
- "wazuh-monitoring-*"
|
||||
- "wazuh-statistics-*"
|
||||
allowed_actions:
|
||||
- "read"
|
||||
- "indices:admin/mappings/get"
|
||||
- "indices:admin/get"
|
||||
tenant_permissions:
|
||||
- tenant_patterns:
|
||||
- "global_tenant"
|
||||
allowed_actions:
|
||||
- "kibana_all_read"
|
||||
|
||||
readonly_auditor:
|
||||
reserved: false
|
||||
description: "Read-only Auditor: read-only access to dashboards and alerts across all Wazuh indices, no agent/rule/configuration actions."
|
||||
cluster_permissions:
|
||||
- "cluster_composite_ops_ro"
|
||||
index_permissions:
|
||||
- index_patterns:
|
||||
- "wazuh-*"
|
||||
allowed_actions:
|
||||
- "read"
|
||||
tenant_permissions:
|
||||
- tenant_patterns:
|
||||
- "global_tenant"
|
||||
allowed_actions:
|
||||
- "kibana_all_read"
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
unset CURL_CA_BUNDLE SSL_CERT_FILE REQUESTS_CA_BUNDLE NODE_EXTRA_CA_CERTS
|
||||
|
||||
: "${CERT_TOOL_VERSION:?CERT_TOOL_VERSION must name the wazuh-certs-tool release}"
|
||||
: "${WAZUH_MANAGER_NODE:?WAZUH_MANAGER_NODE must name the manager node in /config/certs.yml}"
|
||||
|
||||
config_src="/config/certs.yml"
|
||||
out_dir="/certificates"
|
||||
tool_url="https://packages.wazuh.com/${CERT_TOOL_VERSION}/wazuh-certs-tool.sh"
|
||||
|
||||
work_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${work_dir}"' EXIT
|
||||
|
||||
if ! curl --connect-timeout 10 --max-time 120 --fail --silent --show-error --location \
|
||||
--retry 5 --retry-delay 5 --retry-all-errors \
|
||||
--output "${work_dir}/wazuh-certs-tool.sh" "${tool_url}"; then
|
||||
echo "ERROR: downloading ${tool_url} failed; no certificates were generated" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "${config_src}" "${work_dir}/config.yml"
|
||||
bash "${work_dir}/wazuh-certs-tool.sh" --all
|
||||
|
||||
cp "${work_dir}"/wazuh-certificates/* "${out_dir}/"
|
||||
cp "${out_dir}/root-ca.pem" "${out_dir}/root-ca-manager.pem"
|
||||
cp "${out_dir}/root-ca.key" "${out_dir}/root-ca-manager.key"
|
||||
|
||||
chmod 0400 "${out_dir}"/*
|
||||
chown 1000:1000 "${out_dir}"/*
|
||||
chown 999:999 \
|
||||
"${out_dir}/root-ca-manager.pem" \
|
||||
"${out_dir}/root-ca-manager.key" \
|
||||
"${out_dir}/${WAZUH_MANAGER_NODE}.pem" \
|
||||
"${out_dir}/${WAZUH_MANAGER_NODE}-key.pem"
|
||||
chmod 0500 "${out_dir}"
|
||||
|
||||
echo "Wazuh certificates written to ${out_dir}"
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "${CA_TRUST_CERT:-}" ] && [ -r "${CA_TRUST_CERT}" ]; then
|
||||
CACERTS="/usr/share/wazuh-indexer/jdk/lib/security/cacerts"
|
||||
ALIAS="${CA_TRUST_NAME:?CA_TRUST_NAME must accompany CA_TRUST_CERT}"
|
||||
KEYTOOL="/usr/share/wazuh-indexer/jdk/bin/keytool"
|
||||
if ! "$KEYTOOL" -list -keystore "$CACERTS" -storepass changeit -alias "$ALIAS" >/dev/null 2>&1; then
|
||||
"$KEYTOOL" -importcert -noprompt -alias "$ALIAS" -file "$CA_TRUST_CERT" -keystore "$CACERTS" -storepass changeit
|
||||
fi
|
||||
fi
|
||||
|
||||
exec /entrypoint.sh "$@"
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true # nocheck: csp-unsafe Reason: OpenSearch Dashboards (Kibana-derived) bootstraps via inline <script> tags injected by its plugin loader; blocking inline scripts breaks the dashboard shell before it can render.
|
||||
unsafe-eval: true # nocheck: csp-unsafe Reason: OpenSearch Dashboards' bundled AngularJS runtime and Vega/Timelion visualization expressions evaluate code dynamically at runtime; blocking eval breaks those code paths, same as every other Kibana/OpenSearch-Dashboards-family role in this repo (e.g. web-app-checkmk).
|
||||
whitelist:
|
||||
worker-src:
|
||||
- "blob:"
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
canonical:
|
||||
- wazuh.{{ DOMAIN_PRIMARY }}
|
||||
aliases: []
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
logo:
|
||||
class: fa-solid fa-magnifying-glass-chart
|
||||
homepage: https://wazuh.com/
|
||||
video: https://www.youtube.com/watch?v=peTSzcAueEc
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: Prageeth Panicker
|
||||
description: Open-source SIEM and XDR platform for monitoring, log analysis, intrusion/vulnerability detection, and compliance
|
||||
license: Infinito.Nexus Community License (Non-Commercial)
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- wazuh
|
||||
- siem
|
||||
- xdr
|
||||
- security
|
||||
- monitoring
|
||||
- opensearch
|
||||
- self-hosted
|
||||
- docker
|
||||
issue_tracker_url: https://s.infinito.nexus/issues
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: ArchLinux
|
||||
versions:
|
||||
- all
|
||||
- name: Debian
|
||||
versions:
|
||||
- all
|
||||
- name: EL
|
||||
versions:
|
||||
- all
|
||||
- name: Fedora
|
||||
versions:
|
||||
- all
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- all
|
||||
dependencies: []
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
local:
|
||||
subnet: 192.168.81.0/24
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
roles:
|
||||
administrator:
|
||||
description: "Full administrative access: agents, rules, and security configuration."
|
||||
security-analyst:
|
||||
description: "Reads alerts, logs, and dashboards. No security-configuration or RBAC-management access."
|
||||
readonly-auditor:
|
||||
description: "Read-only access to dashboards and alerts. No agent, rule, or configuration actions."
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
credentials:
|
||||
indexer_admin_password:
|
||||
description: "Password for the wazuh-indexer's internal 'admin' service account (OpenSearch security plugin). Consumed by manager/dashboard as INDEXER_PASSWORD,
|
||||
by Ansible health checks and securityadmin.sh, and hashed into internal_users.yml."
|
||||
algorithm: alphanumeric
|
||||
validation: "^[A-Za-z0-9]{32,}$"
|
||||
length: 40
|
||||
dashboard_service_password:
|
||||
description: "Password for the wazuh-indexer's internal 'kibanaserver' service account. Consumed by wazuh.dashboard as DASHBOARD_PASSWORD and hashed into internal_users.yml."
|
||||
algorithm: alphanumeric
|
||||
validation: "^[A-Za-z0-9]{32,}$"
|
||||
length: 40
|
||||
wazuh_wui_password:
|
||||
description: "Password for the 'wazuh-wui' Wazuh API account. Consumed by wazuh.manager (API_PASSWORD, creates the account) and wazuh.dashboard (WAZUH API connection
|
||||
in wazuh.yml)."
|
||||
algorithm: alphanumeric
|
||||
validation: "^[A-Za-z0-9]{32,}$"
|
||||
length: 40
|
||||
manager_cluster_key:
|
||||
description: "wazuh-manager cluster key (ossec.conf <cluster><key>). Clustering is disabled in this single-node deployment, but the key MUST NOT ship as the well-known
|
||||
upstream demo value in case clustering is enabled later."
|
||||
algorithm: random_hex_16
|
||||
validation: "^[a-f0-9]{32}$"
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
status_codes:
|
||||
default:
|
||||
- 200
|
||||
- 302
|
||||
- 401
|
||||
- 403
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
# nocheck: email, dynamic-flag, playwright-service-flag
|
||||
email:
|
||||
enabled: false
|
||||
shared: false
|
||||
sso:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-keycloak' in group_names }}"
|
||||
shared: "{{ 'web-app-keycloak' in group_names }}"
|
||||
flavor: oidc
|
||||
# nocheck: playwright-service-flag authorization backend of the indexer's OpenSearch security plugin, no LDAP UI surface to probe.
|
||||
ldap:
|
||||
bond: 1
|
||||
enabled: "{{ 'svc-db-openldap' in group_names }}"
|
||||
shared: "{{ 'svc-db-openldap' in group_names }}"
|
||||
logout:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-svc-logout' in group_names }}"
|
||||
shared: "{{ 'web-svc-logout' in group_names }}"
|
||||
# nocheck: playwright-service-flag dashboard-tile reachability is owned by web-app-dashboard/files/playwright/playwright.spec.js, not consumer roles.
|
||||
dashboard:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-dashboard' in group_names }}"
|
||||
shared: "{{ 'web-app-dashboard' in group_names }}"
|
||||
matomo:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-matomo' in group_names }}"
|
||||
shared: "{{ 'web-app-matomo' in group_names }}"
|
||||
# nocheck: playwright-service-flag Wazuh exposes no native /metrics endpoint; nothing for a consumer-role spec to assert here.
|
||||
prometheus:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-prometheus' in group_names }}"
|
||||
shared: "{{ 'web-app-prometheus' in group_names }}"
|
||||
# nocheck: playwright-service-flag css consumer, no Playwright surface (provider: web-svc-css)
|
||||
css:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-svc-css' in group_names }}"
|
||||
shared: "{{ 'web-svc-css' in group_names }}"
|
||||
# nocheck: playwright-service-flag, dynamic-flag infrastructure backup consumer
|
||||
container_backup: # nocheck: bond-baseline backup host joins outside the variant topology
|
||||
bond: 1
|
||||
enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}"
|
||||
shared: true
|
||||
wazuh:
|
||||
modes:
|
||||
compose:
|
||||
enabled: true
|
||||
swarm:
|
||||
enabled: true
|
||||
# nocheck: default-placement-manager certs_generator writes the TLS material into a host bind directory that manager, indexer and dashboard mount, so all of them have to run on that node.
|
||||
placement: manager
|
||||
min_storage: 1GB
|
||||
cpus: "0.2"
|
||||
mem_reservation: 512m
|
||||
mem_limit: 1g
|
||||
pids_limit: 256
|
||||
run_after:
|
||||
- web-app-keycloak
|
||||
- svc-db-openldap
|
||||
lifecycle: beta
|
||||
certs_generator:
|
||||
image: wazuh/wazuh-certs-generator
|
||||
version: "0.0.4"
|
||||
name: wazuh-certs-generator
|
||||
cpus: "0.2"
|
||||
mem_reservation: 64m
|
||||
mem_limit: 256m
|
||||
pids_limit: 128
|
||||
manager:
|
||||
image: wazuh/wazuh-manager
|
||||
version: "4.14.6"
|
||||
name: wazuh.manager
|
||||
# nocheck: min-storage-headroom Reason: budgets room for wazuh-manager's own operational data (var-ossec logs, alerts, agent queue/state) to grow well beyond the base image over the service's lifetime, not just the image itself.
|
||||
min_storage: 10GB
|
||||
cpus: "1.0"
|
||||
mem_reservation: 512m
|
||||
mem_limit: 2g
|
||||
pids_limit: 1024
|
||||
nofile: 655360
|
||||
backup:
|
||||
no_stop_required: false
|
||||
ports:
|
||||
internal:
|
||||
agent_events: 1514
|
||||
agent_registration: 1515
|
||||
api: 55000
|
||||
indexer:
|
||||
healthcheck:
|
||||
test:
|
||||
- "CMD-SHELL"
|
||||
- "curl -s -o /dev/null -w '%{http_code}' -k https://localhost:{{ lookup('config', 'web-app-wazuh', 'services.indexer.ports.internal.http') }}/_cluster/health
|
||||
| grep -Eq '^(200|401)$$'"
|
||||
# nocheck: healthcheck-timing-override manager and dashboard depend_on indexer with condition: service_healthy (see compose.yml.j2); the curl flavor's 2m cadence would leave both blocked for minutes past the point the indexer is actually ready.
|
||||
interval: 30s
|
||||
# nocheck: healthcheck-timing-override the -k https probe against the security plugin can take longer than the 5s default while the TLS handshake completes during startup.
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
# nocheck: healthcheck-timing-override covers the indexer's real boot without waiting out the curl flavor's 10m default, which would otherwise stall manager/dashboard's startup order far longer than the indexer actually needs.
|
||||
start_period: 90s
|
||||
image: wazuh/wazuh-indexer
|
||||
version: "4.14.6"
|
||||
name: wazuh.indexer
|
||||
# nocheck: min-storage-headroom Reason: budgets room for the OpenSearch index data (security events/alerts Wazuh continuously ingests) to grow well beyond the base image over the service's lifetime, not just the image itself.
|
||||
min_storage: 20GB
|
||||
cpus: "2.0"
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
pids_limit: 2048
|
||||
nofile: 65536
|
||||
backup:
|
||||
no_stop_required: false
|
||||
ports:
|
||||
internal:
|
||||
http: 9200
|
||||
local:
|
||||
http: 8097
|
||||
console:
|
||||
image: wazuh/wazuh-dashboard
|
||||
version: "4.14.6"
|
||||
name: wazuh.dashboard
|
||||
# nocheck: min-storage-headroom Reason: budgets room for the dashboard's own runtime state (OpenSearch Dashboards bundle/build cache rebuilt on first boot, plus exported reports and saved-object data) to grow beyond the base image, not just the image itself.
|
||||
min_storage: 2GB
|
||||
cpus: "1.0"
|
||||
mem_reservation: 512m
|
||||
mem_limit: 1g
|
||||
pids_limit: 512
|
||||
ports:
|
||||
internal:
|
||||
http: 5601
|
||||
local:
|
||||
http: 8096
|
||||
|
||||
# nocheck: playwright-service-flag
|
||||
# nocheck: variants-coverage — sso-variant-tor requires the onion on in every variant, so the false polarity has no variant left to live in
|
||||
tor:
|
||||
bond: 0.5
|
||||
enabled: "{{ 'svc-net-tor' in group_names }}"
|
||||
shared: true # nocheck: dynamic-flag
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
admin: {}
|
||||
web-app-wazuh-dashboard:
|
||||
username: kibanaserver
|
||||
accounts: []
|
||||
description: Service account wazuh.dashboard uses for its backend calls to wazuh.indexer
|
||||
web-app-wazuh-api:
|
||||
username: wazuh-wui
|
||||
accounts: []
|
||||
description: Wazuh API account the dashboard connects with; created by wazuh.manager
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
- services:
|
||||
container_backup:
|
||||
enabled: true
|
||||
sso:
|
||||
enabled: true
|
||||
shared: true
|
||||
ldap:
|
||||
enabled: true
|
||||
shared: true
|
||||
logout:
|
||||
enabled: true
|
||||
shared: true
|
||||
dashboard:
|
||||
enabled: true
|
||||
shared: true
|
||||
matomo:
|
||||
enabled: true
|
||||
shared: true
|
||||
prometheus:
|
||||
enabled: true
|
||||
shared: true
|
||||
css:
|
||||
enabled: true
|
||||
shared: true
|
||||
tor:
|
||||
enabled: true
|
||||
|
||||
- services:
|
||||
container_backup:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
sso:
|
||||
enabled: false
|
||||
shared: false
|
||||
ldap:
|
||||
enabled: false
|
||||
shared: false
|
||||
logout:
|
||||
enabled: false
|
||||
shared: false
|
||||
dashboard:
|
||||
enabled: false
|
||||
shared: false
|
||||
matomo:
|
||||
enabled: false
|
||||
shared: false
|
||||
prometheus:
|
||||
enabled: false
|
||||
shared: false
|
||||
css:
|
||||
enabled: false
|
||||
shared: false
|
||||
tor:
|
||||
enabled: true
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
manager_api_configuration:
|
||||
type: volume
|
||||
name: wazuh_manager_api_configuration
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/api/configuration
|
||||
manager_etc:
|
||||
type: volume
|
||||
name: wazuh_manager_etc
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/etc
|
||||
manager_logs:
|
||||
type: volume
|
||||
name: wazuh_manager_logs
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/logs
|
||||
manager_queue:
|
||||
type: volume
|
||||
name: wazuh_manager_queue
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/queue
|
||||
manager_vd:
|
||||
type: volume
|
||||
name: wazuh_manager_vd
|
||||
backup: false
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/queue/vd
|
||||
manager_var_multigroups:
|
||||
type: volume
|
||||
name: wazuh_manager_var_multigroups
|
||||
backup: false
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/var/multigroups
|
||||
manager_integrations:
|
||||
type: volume
|
||||
name: wazuh_manager_integrations
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/integrations
|
||||
manager_active_response:
|
||||
type: volume
|
||||
name: wazuh_manager_active_response
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/active-response/bin
|
||||
manager_agentless:
|
||||
type: volume
|
||||
name: wazuh_manager_agentless
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/agentless
|
||||
manager_wodles:
|
||||
type: volume
|
||||
name: wazuh_manager_wodles
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/ossec/wodles
|
||||
manager_filebeat_etc:
|
||||
type: volume
|
||||
name: wazuh_manager_filebeat_etc
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /etc/filebeat
|
||||
manager_filebeat_var:
|
||||
type: volume
|
||||
name: wazuh_manager_filebeat_var
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /var/lib/filebeat
|
||||
indexer_data:
|
||||
type: volume
|
||||
name: wazuh_indexer_data
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /var/lib/wazuh-indexer
|
||||
dashboard_config:
|
||||
type: volume
|
||||
name: wazuh_dashboard_config
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /usr/share/wazuh-dashboard/data/wazuh/config
|
||||
dashboard_custom:
|
||||
type: volume
|
||||
name: wazuh_dashboard_custom
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /usr/share/wazuh-dashboard/plugins/wazuh/public/assets/custom
|
||||
|
||||
certs_generator_config:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: certs_generator
|
||||
target: /config/certs.yml
|
||||
certs_generator_entrypoint:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs-entrypoint.sh' ] | path_join }}"
|
||||
mode: "0555"
|
||||
mounts:
|
||||
- service: certs_generator
|
||||
target: &entrypoint_target /infinito-entrypoint.sh
|
||||
indexer_entrypoint:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'indexer-entrypoint.sh' ] | path_join }}"
|
||||
mode: "0555"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: *entrypoint_target
|
||||
wazuh_manager_conf:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'wazuh_manager.conf' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /wazuh-config-mount/etc/ossec.conf
|
||||
wazuh_indexer_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'wazuh.indexer.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/opensearch.yml
|
||||
internal_users_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'internal_users.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/opensearch-security/internal_users.yml
|
||||
config_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'config.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/opensearch-security/config.yml
|
||||
indexer_roles_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'roles.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/opensearch-security/roles.yml
|
||||
roles_mapping_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'roles_mapping.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/opensearch-security/roles_mapping.yml
|
||||
opensearch_dashboards_yml:
|
||||
type: secret
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'opensearch_dashboards.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /usr/share/wazuh-dashboard/config/opensearch_dashboards.yml
|
||||
dashboard_wazuh_yml:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'wazuh.yml' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml
|
||||
|
||||
wazuh_certs_dir:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs' ] | path_join }}"
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: certs_generator
|
||||
target: /certificates/
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
root_ca_manager_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', 'root-ca-manager.pem' ] | path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /etc/ssl/root-ca.pem
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
manager_filebeat_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', lookup('config', 'web-app-wazuh', 'services.manager.name') ~ '.pem' ] | path_join
|
||||
}}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /etc/ssl/filebeat.pem
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
manager_filebeat_key_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', lookup('config', 'web-app-wazuh', 'services.manager.name') ~ '-key.pem' ] |
|
||||
path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: manager
|
||||
target: /etc/ssl/filebeat.key
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
root_ca_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', 'root-ca.pem' ] | path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/certs/root-ca.pem
|
||||
- service: dashboard
|
||||
target: /usr/share/wazuh-dashboard/certs/root-ca.pem
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
indexer_key_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', lookup('config', 'web-app-wazuh', 'services.indexer.name') ~ '-key.pem' ] |
|
||||
path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
indexer_cert_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', lookup('config', 'web-app-wazuh', 'services.indexer.name') ~ '.pem' ] | path_join
|
||||
}}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
admin_cert_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', 'admin.pem' ] | path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/certs/admin.pem
|
||||
# nocheck: meta-volume-file-bind-should-be-config Reason: generated at deploy time by certs_generator in this same stack; placement: manager guarantees the bind source exists on the scheduled node.
|
||||
admin_key_pem:
|
||||
type: bind
|
||||
source: "{{ [ lookup('container', 'web-app-wazuh', 'directories.instance'), 'certs', 'admin-key.pem' ] | path_join }}"
|
||||
read_only: true
|
||||
swarm_safe: false
|
||||
mounts:
|
||||
- service: indexer
|
||||
target: /usr/share/wazuh-indexer/config/certs/admin-key.pem
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
- include_tasks: utils/once/flag.yml
|
||||
|
||||
- name: "🧩 load docker for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-backend
|
||||
vars:
|
||||
compose_handlers_flush: false
|
||||
|
||||
- name: "📁 Ensure {{ WAZUH_CERTS_DIR_HOST }} exists for the cert-generator bind mount"
|
||||
ansible.builtin.file:
|
||||
path: "{{ WAZUH_CERTS_DIR_HOST }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: "📄 Render node topology for wazuh-certs-generator"
|
||||
stack_host_template:
|
||||
src: certs.yml.j2
|
||||
dest: "{{ WAZUH_CERTS_CONFIG_HOST }}"
|
||||
mode: "0644"
|
||||
notify: compose-up
|
||||
|
||||
- name: "📄 Render Wazuh config files bind-mounted into manager/indexer/dashboard"
|
||||
stack_host_template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "{{ [ WAZUH_INSTANCE_DIR, item ] | path_join }}"
|
||||
mode: "0644"
|
||||
loop:
|
||||
- wazuh_manager.conf
|
||||
- wazuh.indexer.yml
|
||||
- internal_users.yml
|
||||
- config.yml
|
||||
- roles_mapping.yml
|
||||
- opensearch_dashboards.yml
|
||||
- wazuh.yml
|
||||
notify: compose-up
|
||||
|
||||
- name: "📄 Render per-service env files for the Wazuh manager and dashboard"
|
||||
stack_host_template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "0640"
|
||||
force: true
|
||||
loop:
|
||||
- { src: manager.env.j2, dest: "{{ WAZUH_MANAGER_ENV_FILE }}" }
|
||||
- { src: dashboard.env.j2, dest: "{{ WAZUH_DASHBOARD_ENV_FILE }}" }
|
||||
loop_control:
|
||||
label: "{{ item.src }}"
|
||||
notify: compose-up
|
||||
|
||||
- name: "📋 Copy static Wazuh indexer roles.yml"
|
||||
ansible.builtin.copy: # nocheck: stack-host-consumer (already gated by `when: IS_STACK_HOST | bool` below)
|
||||
src: roles.yml
|
||||
dest: "{{ [ WAZUH_INSTANCE_DIR, 'roles.yml' ] | path_join }}"
|
||||
mode: "0644"
|
||||
when: IS_STACK_HOST | bool
|
||||
notify: compose-up
|
||||
|
||||
- name: "📋 Copy static Wazuh entrypoint scripts"
|
||||
ansible.builtin.copy: # nocheck: stack-host-consumer (already gated by `when: IS_STACK_HOST | bool` below)
|
||||
src: "shell/{{ item }}"
|
||||
dest: "{{ [ WAZUH_INSTANCE_DIR, item ] | path_join }}"
|
||||
mode: "0555"
|
||||
loop:
|
||||
- certs-entrypoint.sh
|
||||
- indexer-entrypoint.sh
|
||||
when: IS_STACK_HOST | bool
|
||||
notify: compose-up
|
||||
|
||||
- name: "🔐 Pre-generate the Wazuh certs before the swarm stack deploys"
|
||||
include_tasks: certs/swarm.yml
|
||||
when:
|
||||
- IS_STACK_HOST | bool
|
||||
- DEPLOYMENT_MODE == 'swarm'
|
||||
|
||||
- name: "🔧 Set vm.max_map_count for the Wazuh indexer (OpenSearch requirement)"
|
||||
ansible.posix.sysctl:
|
||||
name: vm.max_map_count
|
||||
value: "262144"
|
||||
state: present
|
||||
reload: true
|
||||
sysctl_file: "{{ WAZUH_SYSCTL_CONF }}"
|
||||
when: not (DOCKER_IN_CONTAINER | bool)
|
||||
|
||||
- name: "🔄 Flush compose-up before front-proxy can shadow application_id"
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: "📂 Include front proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-front-proxy
|
||||
vars:
|
||||
http_port: "{{ WAZUH_DASHBOARD_HTTP_PORT }}"
|
||||
webserver_proxy_service_key: "{{ 'dashboard' if DEPLOYMENT_MODE == 'swarm' else (application_id | get_entity_name) }}"
|
||||
proxy_internal_port: "{{ WAZUH_DASHBOARD_INTERNAL_PORT if DEPLOYMENT_MODE == 'swarm' else '' }}"
|
||||
|
||||
- name: "⏳ Wait for Wazuh indexer to answer HTTPS requests (security plugin loaded)"
|
||||
uri_retry:
|
||||
url: "{{ WAZUH_INDEXER_INTERNAL_API_URL }}/_cluster/health"
|
||||
method: GET
|
||||
validate_certs: false
|
||||
status_code: [200, 401]
|
||||
register: wazuh_indexer_ready
|
||||
until: wazuh_indexer_ready is succeeded
|
||||
retries: 60
|
||||
delay: 5
|
||||
|
||||
- name: "🔄 Apply Wazuh indexer security config via securityadmin.sh"
|
||||
include_tasks: 01_securityadmin.yml
|
||||
|
||||
- name: "✅ Confirm indexer admin credentials are live post-securityadmin.sh"
|
||||
uri_retry:
|
||||
url: "{{ WAZUH_INDEXER_INTERNAL_API_URL }}/_cluster/health"
|
||||
method: GET
|
||||
user: "{{ WAZUH_INDEXER_ADMIN_USERNAME }}"
|
||||
password: "{{ WAZUH_INDEXER_ADMIN_PASSWORD }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: 200
|
||||
return_content: true
|
||||
register: wazuh_indexer_health
|
||||
until: >-
|
||||
wazuh_indexer_health.json is defined and
|
||||
wazuh_indexer_health.json.status in ['green', 'yellow']
|
||||
retries: 30
|
||||
delay: 5
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
- name: "▶️ Run securityadmin.sh inside the indexer container"
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
container exec -e JAVA_HOME=/usr/share/wazuh-indexer/jdk {{ WAZUH_INDEXER_CONTAINER_ADDRESS }}
|
||||
/usr/share/wazuh-indexer/plugins/opensearch-security/tools/securityadmin.sh
|
||||
-cd {{ WAZUH_INDEXER_SECURITY_DIR_CONTAINER }}/
|
||||
-icl
|
||||
-key {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/admin-key.pem
|
||||
-cert {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/admin.pem
|
||||
-cacert {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/root-ca.pem
|
||||
-nhnv
|
||||
chdir: "{{ WAZUH_INSTANCE_DIR }}"
|
||||
register: wazuh_securityadmin
|
||||
changed_when: "'Done with success' in wazuh_securityadmin.stdout"
|
||||
failed_when: wazuh_securityadmin.rc != 0 or 'Done with success' not in wazuh_securityadmin.stdout
|
||||
retries: 10
|
||||
delay: 10
|
||||
until: wazuh_securityadmin.rc == 0
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
- name: "🔍 Read the Wazuh certs completion marker"
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ WAZUH_CERTS_MARKER_HOST }}"
|
||||
register: wazuh_certs_marker
|
||||
failed_when: false
|
||||
|
||||
- name: "🔍 Checksum the rendered Wazuh certs.yml"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ WAZUH_CERTS_CONFIG_HOST }}"
|
||||
checksum_algorithm: sha256
|
||||
register: wazuh_certs_config
|
||||
|
||||
- name: "🔍 Stat every Wazuh cert file the stack bind-mounts"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop: "{{ WAZUH_CERTS_REQUIRED_FILES }}"
|
||||
register: wazuh_certs_files
|
||||
|
||||
- name: "🔍 List the services of a running Wazuh stack"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- service
|
||||
- ls
|
||||
- --quiet
|
||||
- --filter
|
||||
- "label=com.docker.stack.namespace={{ application_id | get_entity_name }}"
|
||||
register: wazuh_certs_stack
|
||||
changed_when: false
|
||||
|
||||
- name: "🧮 Decide whether the Wazuh certs are current"
|
||||
vars:
|
||||
expected: "{{ wazuh_certs_config.stat.checksum }} {{ WAZUH_CERTS_TOOL_VERSION }}"
|
||||
recorded: "{{ wazuh_certs_marker.content | default('') | b64decode | trim }}"
|
||||
present: >-
|
||||
{{ wazuh_certs_files.results | selectattr('stat.size', 'defined') | selectattr('stat.size', 'gt', 0) | list | length }}
|
||||
ansible.builtin.set_fact:
|
||||
wazuh_certs_expected: "{{ expected }}"
|
||||
wazuh_certs_recorded: "{{ recorded }}"
|
||||
wazuh_certs_present: "{{ present | int }}"
|
||||
wazuh_certs_current: "{{ present | int == WAZUH_CERTS_REQUIRED_FILES | length and recorded == expected }}"
|
||||
|
||||
- name: "⛔ Refuse to rotate the Wazuh CA under a running stack"
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Wazuh TLS certs on {{ inventory_hostname }} are not current, and the stack
|
||||
'{{ application_id | get_entity_name }}' is running, so they were NOT regenerated: that would
|
||||
swap the CA under the live services.
|
||||
Marker {{ WAZUH_CERTS_MARKER_HOST }} reads '{{ wazuh_certs_recorded or 'missing' }}',
|
||||
expected '{{ wazuh_certs_expected }}' (sha256 of certs.yml, then tool version);
|
||||
{{ wazuh_certs_present }}/{{ WAZUH_CERTS_REQUIRED_FILES | length }} cert files in
|
||||
{{ WAZUH_CERTS_DIR_HOST }} exist and are non-empty.
|
||||
To regenerate: run 'docker stack rm {{ application_id | get_entity_name }}', then redeploy.
|
||||
To keep certs you know are good (a host deployed before this marker existed): write
|
||||
'{{ wazuh_certs_expected }}' to {{ WAZUH_CERTS_MARKER_HOST }}, then redeploy.
|
||||
when:
|
||||
- not (wazuh_certs_current | bool)
|
||||
- wazuh_certs_stack.stdout | trim | length > 0
|
||||
|
||||
- name: "⬇️ Pull the Wazuh certs generator image on the stack host"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- pull
|
||||
- "{{ WAZUH_CERTS_GENERATOR_IMAGE }}"
|
||||
register: wazuh_certs_pull
|
||||
retries: 10
|
||||
delay: 6
|
||||
until: wazuh_certs_pull.rc == 0
|
||||
changed_when: false
|
||||
when: not (wazuh_certs_current | bool)
|
||||
|
||||
- name: "🔐 Run the Wazuh certs generator once on the stack host"
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
container ps -aq --filter {{ ('name=^' ~ WAZUH_CERTS_GENERATOR_CONTAINER ~ '$') | quote }} | xargs -r container rm -f
|
||||
timeout -k 30 {{ lookup('timeout', 600) | int }} container run --rm \
|
||||
--name {{ WAZUH_CERTS_GENERATOR_CONTAINER | quote }} \
|
||||
--entrypoint {{ WAZUH_CERTS_ENTRYPOINT_TARGET | quote }} \
|
||||
--env CERT_TOOL_VERSION={{ WAZUH_CERTS_TOOL_VERSION | quote }} \
|
||||
--env WAZUH_MANAGER_NODE={{ WAZUH_MANAGER_CONTAINER | quote }} \
|
||||
{% for mount in WAZUH_CERTS_GENERATOR_MOUNTS %}--mount {{ mount | quote }} {% endfor %}\
|
||||
{{ WAZUH_CERTS_GENERATOR_IMAGE | quote }}
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: wazuh_certs_generate
|
||||
retries: 3
|
||||
delay: 10
|
||||
until: wazuh_certs_generate.rc == 0
|
||||
when: not (wazuh_certs_current | bool)
|
||||
|
||||
- name: "✅ Verify the generated Wazuh certs and record the marker"
|
||||
when: wazuh_certs_generate is not skipped
|
||||
block:
|
||||
- name: "🔍 Stat the Wazuh cert files the generator wrote"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop: "{{ WAZUH_CERTS_REQUIRED_FILES }}"
|
||||
register: wazuh_certs_written
|
||||
|
||||
- name: "✅ Assert every Wazuh cert file exists and is non-empty"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
wazuh_certs_written.results | selectattr('stat.size', 'defined') | selectattr('stat.size', 'gt', 0)
|
||||
| list | length == WAZUH_CERTS_REQUIRED_FILES | length
|
||||
fail_msg: "The Wazuh certs generator exited 0 but left a required cert file missing or empty."
|
||||
|
||||
- name: "📝 Record the Wazuh certs completion marker"
|
||||
ansible.builtin.copy:
|
||||
content: "{{ wazuh_certs_expected }}\n"
|
||||
dest: "{{ WAZUH_CERTS_MARKER_HOST }}"
|
||||
mode: "0644"
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
- include_tasks: 00_core.yml
|
||||
when: run_once_web_app_wazuh is not defined
|
||||
@@ -0,0 +1,10 @@
|
||||
nodes:
|
||||
indexer:
|
||||
- name: {{ WAZUH_INDEXER_CONTAINER }}
|
||||
ip: {{ WAZUH_INDEXER_CONTAINER }}
|
||||
server:
|
||||
- name: {{ WAZUH_MANAGER_CONTAINER }}
|
||||
ip: {{ WAZUH_MANAGER_CONTAINER }}
|
||||
dashboard:
|
||||
- name: {{ WAZUH_DASHBOARD_CONTAINER }}
|
||||
ip: {{ WAZUH_DASHBOARD_CONTAINER }}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}
|
||||
{# Exception: swarm ignores depends_on, so in swarm tasks/certs/swarm.yml generates the certs before the stack deploys. #}
|
||||
{% set certs_generator_dependency = {'certs_generator': {'condition': 'service_completed_successfully'}} if DEPLOYMENT_MODE == 'compose' else {} %}
|
||||
{% if DEPLOYMENT_MODE == 'compose' %}
|
||||
{% set service_name = 'certs_generator' %}
|
||||
{% set docker_restart_policy = 'no' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'certs_generator') }}
|
||||
{{ lookup('compose_only', 'container_name', WAZUH_CERTS_GENERATOR_CONTAINER) }}
|
||||
entrypoint: ["{{ WAZUH_CERTS_ENTRYPOINT_TARGET }}"]
|
||||
environment:
|
||||
- CERT_TOOL_VERSION={{ WAZUH_CERTS_TOOL_VERSION }}
|
||||
- WAZUH_MANAGER_NODE={{ WAZUH_MANAGER_CONTAINER }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
|
||||
{% endif %}
|
||||
{% set service_name = 'manager' %}
|
||||
{% set docker_restart_policy = DOCKER_RESTART_POLICY %}
|
||||
{% set docker_compose_env = WAZUH_MANAGER_ENV_FILE %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'manager') }}
|
||||
{{ lookup('compose_only', 'container_name', WAZUH_MANAGER_CONTAINER) }}
|
||||
hostname: "{{ WAZUH_MANAGER_CONTAINER }}"
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: {{ lookup('config', application_id, 'services.manager.nofile') }}
|
||||
hard: {{ lookup('config', application_id, 'services.manager.nofile') }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
{{ lookup('container_depends_on', application_id, extra=certs_generator_dependency) | indent(4) }}
|
||||
|
||||
{% set service_name = 'indexer' %}
|
||||
{% set docker_compose_env = '' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'indexer') }}
|
||||
{{ lookup('compose_only', 'container_name', WAZUH_INDEXER_CONTAINER) }}
|
||||
hostname: "{{ WAZUH_INDEXER_CONTAINER }}"
|
||||
entrypoint: ["{{ WAZUH_INDEXER_ENTRYPOINT_TARGET }}"]
|
||||
command: ["opensearchwrapper"]
|
||||
{{ lookup('container_ports', ['indexer', 'http', DOCKER_BIND_HOST]) | indent(4) }}
|
||||
environment:
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms{{ WAZUH_INDEXER_HEAP }} -Xmx{{ WAZUH_INDEXER_HEAP }}"
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: {{ lookup('config', application_id, 'services.indexer.nofile') }}
|
||||
hard: {{ lookup('config', application_id, 'services.indexer.nofile') }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
{{ lookup('container_depends_on', application_id, extra=certs_generator_dependency) | indent(4) }}
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
|
||||
{% set service_name = 'dashboard' %}
|
||||
{% set docker_compose_env = WAZUH_DASHBOARD_ENV_FILE %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'console') }}
|
||||
{{ lookup('compose_only', 'container_name', WAZUH_DASHBOARD_CONTAINER) }}
|
||||
hostname: "{{ WAZUH_DASHBOARD_CONTAINER }}"
|
||||
{{ lookup('container_ports', ['console', 'http', DOCKER_BIND_HOST]) | indent(4) }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
{{ lookup('container_depends_on', application_id, extra={
|
||||
'indexer': {'condition': 'service_healthy'},
|
||||
'manager': {'condition': 'service_started'},
|
||||
}) | indent(4) }}
|
||||
|
||||
{{ lookup('compose_volumes') }}
|
||||
|
||||
{{ lookup('compose_networks') }}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
_meta:
|
||||
type: "config"
|
||||
config_version: 2
|
||||
|
||||
config:
|
||||
dynamic:
|
||||
http:
|
||||
anonymous_auth_enabled: false
|
||||
authc:
|
||||
basic_internal_auth_domain:
|
||||
description: "HTTP basic auth against internal_users.yml (service accounts only)."
|
||||
http_enabled: true
|
||||
transport_enabled: true
|
||||
order: 0
|
||||
http_authenticator:
|
||||
type: basic
|
||||
challenge: false
|
||||
authentication_backend:
|
||||
type: internal
|
||||
{% if WAZUH_OIDC_ENABLED %}
|
||||
openid_auth_domain:
|
||||
description: "Native OIDC against Keycloak's discovery endpoint (no oauth2-proxy sidecar)."
|
||||
http_enabled: true
|
||||
transport_enabled: true
|
||||
order: 1
|
||||
http_authenticator:
|
||||
type: openid
|
||||
challenge: true
|
||||
config:
|
||||
subject_key: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
roles_key: groups
|
||||
openid_connect_url: "{{ OIDC.CLIENT.DISCOVERY_DOCUMENT }}"
|
||||
authentication_backend:
|
||||
type: noop
|
||||
{% endif %}
|
||||
{% if WAZUH_LDAP_ENABLED %}
|
||||
authz:
|
||||
ldap_authz_domain:
|
||||
description: "Authorization only: enrich the authenticated user with backend_roles from LDAP group membership. Never used to verify a password."
|
||||
http_enabled: true
|
||||
transport_enabled: true
|
||||
authorization_backend:
|
||||
type: ldap
|
||||
config:
|
||||
hosts:
|
||||
- "{{ LDAP.SERVER.URI | regex_replace('^ldaps?://', '') }}"
|
||||
enable_ssl: "{{ LDAP.SERVER.SECURITY == 'ldaps' }}"
|
||||
enable_start_tls: "{{ LDAP.SERVER.SECURITY == 'starttls' }}"
|
||||
verify_hostnames: true
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
password: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
userbase: "{{ LDAP.DN.OU.USERS }}"
|
||||
usersearch: "({{ LDAP.USER.ATTRIBUTES.ID }}={0})"
|
||||
username_attribute: "{{ LDAP.USER.ATTRIBUTES.ID }}"
|
||||
rolebase: "{{ LDAP.DN.OU.ROLES }}"
|
||||
rolesearch: "(member={0})"
|
||||
userroleattribute: null
|
||||
userrolename: "disabled"
|
||||
rolename: "cn"
|
||||
resolve_nested_roles: true
|
||||
skip_users:
|
||||
- "{{ WAZUH_INDEXER_ADMIN_USERNAME }}"
|
||||
- "{{ WAZUH_DASHBOARD_SERVICE_USERNAME }}"
|
||||
{% endif %}
|
||||
@@ -0,0 +1,7 @@
|
||||
API_PASSWORD={{ WAZUH_WUI_PASSWORD | dotenv_quote }}
|
||||
API_USERNAME={{ WAZUH_WUI_USERNAME }}
|
||||
DASHBOARD_PASSWORD={{ WAZUH_DASHBOARD_SERVICE_PASSWORD | dotenv_quote }}
|
||||
DASHBOARD_USERNAME={{ WAZUH_DASHBOARD_SERVICE_USERNAME }}
|
||||
INDEXER_PASSWORD={{ WAZUH_INDEXER_ADMIN_PASSWORD | dotenv_quote }}
|
||||
INDEXER_USERNAME={{ WAZUH_INDEXER_ADMIN_USERNAME }}
|
||||
WAZUH_API_URL=https://{{ WAZUH_MANAGER_CONTAINER }}
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
_meta:
|
||||
type: "internalusers"
|
||||
config_version: 2
|
||||
|
||||
"{{ WAZUH_INDEXER_ADMIN_USERNAME }}":
|
||||
hash: "{{ WAZUH_INDEXER_ADMIN_HASH }}"
|
||||
reserved: true
|
||||
backend_roles:
|
||||
- "admin"
|
||||
description: "Bootstrap/service account used by securityadmin.sh, health checks, and manager/dashboard INDEXER_PASSWORD."
|
||||
|
||||
"{{ WAZUH_DASHBOARD_SERVICE_USERNAME }}":
|
||||
hash: "{{ WAZUH_DASHBOARD_SERVICE_HASH }}"
|
||||
reserved: true
|
||||
description: "Service account wazuh.dashboard uses for its own backend calls to wazuh.indexer."
|
||||
@@ -0,0 +1,9 @@
|
||||
API_PASSWORD={{ WAZUH_WUI_PASSWORD | dotenv_quote }}
|
||||
API_USERNAME={{ WAZUH_WUI_USERNAME }}
|
||||
FILEBEAT_SSL_VERIFICATION_MODE=full
|
||||
INDEXER_PASSWORD={{ WAZUH_INDEXER_ADMIN_PASSWORD | dotenv_quote }}
|
||||
INDEXER_URL={{ WAZUH_INDEXER_URL }}
|
||||
INDEXER_USERNAME={{ WAZUH_INDEXER_ADMIN_USERNAME }}
|
||||
SSL_CERTIFICATE={{ lookup('volume', application_id, 'manager_filebeat_pem').target }}
|
||||
SSL_CERTIFICATE_AUTHORITIES={{ lookup('volume', application_id, 'root_ca_manager_pem').target }}
|
||||
SSL_KEY={{ lookup('volume', application_id, 'manager_filebeat_key_pem').target }}
|
||||
@@ -0,0 +1,22 @@
|
||||
server.host: 0.0.0.0
|
||||
server.port: {{ WAZUH_DASHBOARD_INTERNAL_PORT }}
|
||||
server.ssl.enabled: false
|
||||
opensearch.hosts: {{ WAZUH_INDEXER_URL }}
|
||||
opensearch.ssl.verificationMode: certificate
|
||||
opensearch.ssl.certificateAuthorities: ["/usr/share/wazuh-dashboard/certs/root-ca.pem"]
|
||||
opensearch.requestHeadersWhitelist: ["securitytenant", "Authorization"]
|
||||
opensearch_security.multitenancy.enabled: false
|
||||
opensearch_security.readonly_mode.roles: ["kibana_read_only"]
|
||||
uiSettings.overrides.defaultRoute: /app/home
|
||||
opensearch_security.cookie.ttl: 900000
|
||||
opensearch_security.session.ttl: 900000
|
||||
opensearch_security.session.keepalive: true
|
||||
{% if WAZUH_OIDC_ENABLED %}
|
||||
opensearch_security.auth.type: "openid"
|
||||
opensearch_security.openid.connect_url: "{{ OIDC.CLIENT.DISCOVERY_DOCUMENT }}"
|
||||
opensearch_security.openid.client_id: "{{ OIDC.CLIENT.ID }}"
|
||||
opensearch_security.openid.client_secret: "{{ OIDC.CLIENT.SECRET }}"
|
||||
opensearch_security.openid.base_redirect_url: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
opensearch_security.openid.scope: "openid profile email groups"
|
||||
opensearch_security.openid.logout_url: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
{% endif %}
|
||||
@@ -0,0 +1,36 @@
|
||||
ADMIN_PASSWORD={{ lookup('users', 'administrator').password | dotenv_quote }}
|
||||
|
||||
ADMIN_USERNAME={{ lookup('users', 'administrator').username | dotenv_quote }}
|
||||
|
||||
APP_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }}
|
||||
|
||||
BIBER_PASSWORD={{ lookup('users', 'biber').password | dotenv_quote }}
|
||||
|
||||
BIBER_USERNAME={{ lookup('users', 'biber').username | dotenv_quote }}
|
||||
|
||||
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
|
||||
|
||||
KEYCLOAK_ADMIN_CLI_CLIENT_ID={{ OIDC.ADMIN.CLI_CLIENT_ID | dotenv_quote }}
|
||||
|
||||
KEYCLOAK_ADMIN_REALM={{ OIDC.ADMIN.REALM | dotenv_quote }}
|
||||
|
||||
KEYCLOAK_BASE_URL={{ OIDC.URL | dotenv_quote }}
|
||||
|
||||
KEYCLOAK_REALM_NAME={{ OIDC.CLIENT.REALM | dotenv_quote }}
|
||||
|
||||
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
|
||||
|
||||
MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }}
|
||||
|
||||
OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL | dotenv_quote }}
|
||||
|
||||
# nocheck: persona-block-rationale with sso off the dashboard only accepts the internal admin/kibanaserver HTTP basic accounts, and the persona helper drives Keycloak login only.
|
||||
PERSONA_ADMINISTRATOR_BLOCKED={{ (not (WAZUH_OIDC_ENABLED | bool)) | string | lower }}
|
||||
|
||||
RBAC_GROUP_PATH_PREFIX={{ ('/' ~ (RBAC.GROUP.NAME | trim('/')) ~ '/' ~ application_id ~ '/') | dotenv_quote }}
|
||||
|
||||
SSO_SERVICE_ENABLED={{ WAZUH_OIDC_ENABLED | string | lower }}
|
||||
|
||||
SUPER_ADMIN_PASSWORD={{ (lookup('config', 'web-app-keycloak', 'secrets.credentials.administrator_password') | dotenv_quote) if (WAZUH_OIDC_ENABLED | bool) else '""' }}
|
||||
|
||||
SUPER_ADMIN_USERNAME={{ (lookup('config', 'web-app-keycloak', 'services.keycloak.accounts.system.username') | dotenv_quote) if (WAZUH_OIDC_ENABLED | bool) else '""' }}
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
_meta:
|
||||
type: "rolesmapping"
|
||||
config_version: 2
|
||||
|
||||
all_access:
|
||||
reserved: false
|
||||
backend_roles:
|
||||
- "{{ WAZUH_RBAC_ADMIN_GROUP }}"
|
||||
- "{{ WAZUH_RBAC_ADMIN_LDAP_CN }}"
|
||||
- "admin"
|
||||
|
||||
security_analyst:
|
||||
reserved: false
|
||||
backend_roles:
|
||||
- "{{ WAZUH_RBAC_ANALYST_GROUP }}"
|
||||
- "{{ WAZUH_RBAC_ANALYST_LDAP_CN }}"
|
||||
|
||||
readonly_auditor:
|
||||
reserved: false
|
||||
backend_roles:
|
||||
- "{{ WAZUH_RBAC_READONLY_GROUP }}"
|
||||
- "{{ WAZUH_RBAC_READONLY_LDAP_CN }}"
|
||||
|
||||
kibana_read_only:
|
||||
reserved: false
|
||||
backend_roles:
|
||||
- "{{ WAZUH_RBAC_READONLY_GROUP }}"
|
||||
- "{{ WAZUH_RBAC_READONLY_LDAP_CN }}"
|
||||
|
||||
kibana_server:
|
||||
reserved: false
|
||||
users:
|
||||
- "{{ WAZUH_DASHBOARD_SERVICE_USERNAME }}"
|
||||
@@ -0,0 +1,37 @@
|
||||
network.host: "0.0.0.0"
|
||||
node.name: "{{ WAZUH_INDEXER_CONTAINER }}"
|
||||
cluster.name: "wazuh-cluster"
|
||||
path.data: {{ lookup('volume', application_id, 'indexer_data').target }}
|
||||
path.logs: /var/log/wazuh-indexer
|
||||
discovery.type: single-node
|
||||
compatibility.override_main_response_version: true
|
||||
plugins.security.ssl.http.pemcert_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/wazuh.indexer.pem
|
||||
plugins.security.ssl.http.pemkey_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/wazuh.indexer.key
|
||||
plugins.security.ssl.http.pemtrustedcas_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/root-ca.pem
|
||||
plugins.security.ssl.transport.pemcert_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/wazuh.indexer.pem
|
||||
plugins.security.ssl.transport.pemkey_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/wazuh.indexer.key
|
||||
plugins.security.ssl.transport.pemtrustedcas_filepath: {{ WAZUH_INDEXER_CERTS_DIR_CONTAINER }}/root-ca.pem
|
||||
plugins.security.ssl.http.enabled: true
|
||||
plugins.security.ssl.transport.enforce_hostname_verification: false
|
||||
plugins.security.ssl.transport.resolve_hostname: false
|
||||
plugins.security.ssl.http.enabled_ciphers:
|
||||
- "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
- "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
|
||||
- "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
|
||||
- "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
|
||||
plugins.security.ssl.http.enabled_protocols:
|
||||
- "TLSv1.2"
|
||||
plugins.security.authcz.admin_dn:
|
||||
- "CN=admin,OU=Wazuh,O=Wazuh,L=California,C=US"
|
||||
plugins.security.check_snapshot_restore_write_privileges: true
|
||||
plugins.security.enable_snapshot_restore_privilege: true
|
||||
plugins.security.nodes_dn:
|
||||
- "CN={{ WAZUH_INDEXER_CONTAINER }},OU=Wazuh,O=Wazuh,L=California,C=US"
|
||||
plugins.security.restapi.roles_enabled:
|
||||
- "all_access"
|
||||
- "security_rest_api_access"
|
||||
plugins.security.system_indices.enabled: true
|
||||
plugins.security.system_indices.indices: [".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opendistro-notifications-*", ".opendistro-notebooks", ".opensearch-observability", ".opendistro-asynchronous-search-response*", ".replication-metadata-store"]
|
||||
plugins.security.allow_default_init_securityindex: true
|
||||
plugins.security.cache.ttl_minutes: 0
|
||||
cluster.routing.allocation.disk.threshold_enabled: false
|
||||
@@ -0,0 +1,7 @@
|
||||
hosts:
|
||||
- default:
|
||||
url: "https://{{ WAZUH_MANAGER_CONTAINER }}"
|
||||
port: {{ lookup('config', application_id, 'services.manager.ports.internal.api') }}
|
||||
username: "{{ WAZUH_WUI_USERNAME }}"
|
||||
password: "{{ WAZUH_WUI_PASSWORD }}"
|
||||
run_as: true
|
||||
@@ -0,0 +1,251 @@
|
||||
<ossec_config>
|
||||
<global>
|
||||
<jsonout_output>yes</jsonout_output>
|
||||
<alerts_log>yes</alerts_log>
|
||||
<logall>no</logall>
|
||||
<logall_json>no</logall_json>
|
||||
<email_notification>no</email_notification>
|
||||
<agents_disconnection_time>10m</agents_disconnection_time>
|
||||
<agents_disconnection_alert_time>0</agents_disconnection_alert_time>
|
||||
</global>
|
||||
|
||||
<alerts>
|
||||
<log_alert_level>3</log_alert_level>
|
||||
<email_alert_level>12</email_alert_level>
|
||||
</alerts>
|
||||
|
||||
<logging>
|
||||
<log_format>plain</log_format>
|
||||
</logging>
|
||||
|
||||
<remote>
|
||||
<connection>secure</connection>
|
||||
<port>{{ lookup('config', application_id, 'services.manager.ports.internal.agent_events') }}</port>
|
||||
<protocol>tcp</protocol>
|
||||
<queue_size>131072</queue_size>
|
||||
</remote>
|
||||
|
||||
<rootcheck>
|
||||
<disabled>no</disabled>
|
||||
<check_files>yes</check_files>
|
||||
<check_trojans>yes</check_trojans>
|
||||
<check_dev>yes</check_dev>
|
||||
<check_sys>yes</check_sys>
|
||||
<check_pids>yes</check_pids>
|
||||
<check_ports>yes</check_ports>
|
||||
<check_if>yes</check_if>
|
||||
<frequency>43200</frequency>
|
||||
<rootkit_files>etc/rootcheck/rootkit_files.txt</rootkit_files>
|
||||
<rootkit_trojans>etc/rootcheck/rootkit_trojans.txt</rootkit_trojans>
|
||||
<skip_nfs>yes</skip_nfs>
|
||||
</rootcheck>
|
||||
|
||||
<wodle name="cis-cat">
|
||||
<disabled>yes</disabled>
|
||||
<timeout>1800</timeout>
|
||||
<interval>1d</interval>
|
||||
<scan-on-start>yes</scan-on-start>
|
||||
<java_path>wodles/java</java_path>
|
||||
<ciscat_path>wodles/ciscat</ciscat_path>
|
||||
</wodle>
|
||||
|
||||
<wodle name="osquery">
|
||||
<disabled>yes</disabled>
|
||||
<run_daemon>yes</run_daemon>
|
||||
<log_path>/var/log/osquery/osqueryd.results.log</log_path>
|
||||
<config_path>/etc/osquery/osquery.conf</config_path>
|
||||
<add_labels>yes</add_labels>
|
||||
</wodle>
|
||||
|
||||
<wodle name="syscollector">
|
||||
<disabled>no</disabled>
|
||||
<interval>1h</interval>
|
||||
<scan_on_start>yes</scan_on_start>
|
||||
<hardware>yes</hardware>
|
||||
<os>yes</os>
|
||||
<network>yes</network>
|
||||
<packages>yes</packages>
|
||||
<ports all="yes">yes</ports>
|
||||
<processes>yes</processes>
|
||||
<synchronization>
|
||||
<max_eps>10</max_eps>
|
||||
</synchronization>
|
||||
</wodle>
|
||||
|
||||
<sca>
|
||||
<enabled>yes</enabled>
|
||||
<scan_on_start>yes</scan_on_start>
|
||||
<interval>12h</interval>
|
||||
<skip_nfs>yes</skip_nfs>
|
||||
</sca>
|
||||
|
||||
<vulnerability-detection>
|
||||
<enabled>yes</enabled>
|
||||
<index-status>yes</index-status>
|
||||
<feed-update-interval>60m</feed-update-interval>
|
||||
</vulnerability-detection>
|
||||
|
||||
<indexer>
|
||||
<enabled>yes</enabled>
|
||||
<hosts>
|
||||
<host>{{ WAZUH_INDEXER_URL }}</host>
|
||||
</hosts>
|
||||
<ssl>
|
||||
<certificate_authorities>
|
||||
<ca>{{ lookup('volume', application_id, 'root_ca_manager_pem').target }}</ca>
|
||||
</certificate_authorities>
|
||||
<certificate>{{ lookup('volume', application_id, 'manager_filebeat_pem').target }}</certificate>
|
||||
<key>{{ lookup('volume', application_id, 'manager_filebeat_key_pem').target }}</key>
|
||||
</ssl>
|
||||
</indexer>
|
||||
|
||||
<syscheck>
|
||||
<disabled>no</disabled>
|
||||
<frequency>43200</frequency>
|
||||
<scan_on_start>yes</scan_on_start>
|
||||
<alert_new_files>yes</alert_new_files>
|
||||
<auto_ignore frequency="10" timeframe="3600">no</auto_ignore>
|
||||
<directories>/etc,/usr/bin,/usr/sbin</directories>
|
||||
<directories>/bin,/sbin,/boot</directories>
|
||||
<ignore>/etc/mtab</ignore>
|
||||
<ignore>/etc/hosts.deny</ignore>
|
||||
<ignore>/etc/mail/statistics</ignore>
|
||||
<ignore>/etc/random-seed</ignore>
|
||||
<ignore>/etc/random.seed</ignore>
|
||||
<ignore>/etc/adjtime</ignore>
|
||||
<ignore>/etc/httpd/logs</ignore>
|
||||
<ignore>/etc/utmpx</ignore>
|
||||
<ignore>/etc/wtmpx</ignore>
|
||||
<ignore>/etc/cups/certs</ignore>
|
||||
<ignore>/etc/dumpdates</ignore>
|
||||
<ignore>/etc/svc/volatile</ignore>
|
||||
<ignore type="sregex">.log$|.swp$</ignore>
|
||||
<nodiff>/etc/ssl/private.key</nodiff>
|
||||
<skip_nfs>yes</skip_nfs>
|
||||
<skip_dev>yes</skip_dev>
|
||||
<skip_proc>yes</skip_proc>
|
||||
<skip_sys>yes</skip_sys>
|
||||
<process_priority>10</process_priority>
|
||||
<max_eps>100</max_eps>
|
||||
<synchronization>
|
||||
<enabled>yes</enabled>
|
||||
<interval>5m</interval>
|
||||
<max_interval>1h</max_interval>
|
||||
<max_eps>10</max_eps>
|
||||
</synchronization>
|
||||
</syscheck>
|
||||
|
||||
<global>
|
||||
<white_list>127.0.0.1</white_list>
|
||||
<white_list>^localhost.localdomain$</white_list>
|
||||
</global>
|
||||
|
||||
<command>
|
||||
<name>disable-account</name>
|
||||
<executable>disable-account</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>restart-wazuh</name>
|
||||
<executable>restart-wazuh</executable>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>firewall-drop</name>
|
||||
<executable>firewall-drop</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>host-deny</name>
|
||||
<executable>host-deny</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>route-null</name>
|
||||
<executable>route-null</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>win_route-null</name>
|
||||
<executable>route-null.exe</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<command>
|
||||
<name>netsh</name>
|
||||
<executable>netsh.exe</executable>
|
||||
<timeout_allowed>yes</timeout_allowed>
|
||||
</command>
|
||||
|
||||
<localfile>
|
||||
<log_format>command</log_format>
|
||||
<command>df -P</command>
|
||||
<frequency>360</frequency>
|
||||
</localfile>
|
||||
|
||||
<localfile>
|
||||
<log_format>full_command</log_format>
|
||||
<command>last -n 20</command>
|
||||
<frequency>360</frequency>
|
||||
</localfile>
|
||||
|
||||
<ruleset>
|
||||
<decoder_dir>ruleset/decoders</decoder_dir>
|
||||
<rule_dir>ruleset/rules</rule_dir>
|
||||
<rule_exclude>0215-policy_rules.xml</rule_exclude>
|
||||
<list>etc/lists/audit-keys</list>
|
||||
<list>etc/lists/amazon/aws-eventnames</list>
|
||||
<list>etc/lists/security-eventchannel</list>
|
||||
<list>etc/lists/malicious-ioc/malicious-ip</list>
|
||||
<list>etc/lists/malicious-ioc/malicious-domains</list>
|
||||
<list>etc/lists/malicious-ioc/malware-hashes</list>
|
||||
<decoder_dir>etc/decoders</decoder_dir>
|
||||
<rule_dir>etc/rules</rule_dir>
|
||||
</ruleset>
|
||||
|
||||
<rule_test>
|
||||
<enabled>yes</enabled>
|
||||
<threads>1</threads>
|
||||
<max_sessions>64</max_sessions>
|
||||
<session_timeout>15m</session_timeout>
|
||||
</rule_test>
|
||||
|
||||
<auth>
|
||||
<disabled>no</disabled>
|
||||
<port>{{ lookup('config', application_id, 'services.manager.ports.internal.agent_registration') }}</port>
|
||||
<use_source_ip>no</use_source_ip>
|
||||
<purge>yes</purge>
|
||||
<use_password>no</use_password>
|
||||
<ciphers>HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH</ciphers>
|
||||
<ssl_verify_host>no</ssl_verify_host>
|
||||
<ssl_manager_cert>etc/sslmanager.cert</ssl_manager_cert>
|
||||
<ssl_manager_key>etc/sslmanager.key</ssl_manager_key>
|
||||
<ssl_auto_negotiate>no</ssl_auto_negotiate>
|
||||
</auth>
|
||||
|
||||
<cluster>
|
||||
<name>wazuh</name>
|
||||
<node_name>node01</node_name>
|
||||
<node_type>master</node_type>
|
||||
<key>{{ WAZUH_CLUSTER_KEY }}</key>
|
||||
<port>1516</port>
|
||||
<bind_addr>0.0.0.0</bind_addr>
|
||||
<nodes>
|
||||
<node>{{ WAZUH_MANAGER_CONTAINER }}</node>
|
||||
</nodes>
|
||||
<hidden>no</hidden>
|
||||
<disabled>yes</disabled>
|
||||
</cluster>
|
||||
|
||||
</ossec_config>
|
||||
|
||||
<ossec_config>
|
||||
<localfile>
|
||||
<log_format>syslog</log_format>
|
||||
<location>/var/ossec/logs/active-responses.log</location>
|
||||
</localfile>
|
||||
</ossec_config>
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
application_id: "web-app-wazuh"
|
||||
|
||||
WAZUH_MANAGER_CONTAINER: "{{ lookup('config', application_id, 'services.manager.name') }}"
|
||||
|
||||
WAZUH_INDEXER_CONTAINER: "{{ lookup('config', application_id, 'services.indexer.name') }}"
|
||||
WAZUH_INDEXER_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, 'indexer') }}"
|
||||
WAZUH_INDEXER_HTTP_PORT: "{{ lookup('config', application_id, 'services.indexer.ports.local.http') }}"
|
||||
WAZUH_INDEXER_INTERNAL_PORT: "{{ lookup('config', application_id, 'services.indexer.ports.internal.http') }}"
|
||||
WAZUH_INDEXER_URL: "https://{{ WAZUH_INDEXER_CONTAINER }}:{{ WAZUH_INDEXER_INTERNAL_PORT }}"
|
||||
WAZUH_INDEXER_HEAP: "{{ lookup('service_memory_mb', application_id, 'indexer', 0.5) }}m"
|
||||
|
||||
WAZUH_DASHBOARD_CONTAINER: "{{ lookup('config', application_id, 'services.console.name') }}"
|
||||
WAZUH_DASHBOARD_HTTP_PORT: "{{ lookup('config', application_id, 'services.console.ports.local.http') }}"
|
||||
WAZUH_DASHBOARD_INTERNAL_PORT: "{{ lookup('config', application_id, 'services.console.ports.internal.http') }}"
|
||||
|
||||
WAZUH_CERTS_GENERATOR_CONTAINER: "{{ lookup('config', application_id, 'services.certs_generator.name') }}"
|
||||
WAZUH_CERTS_GENERATOR_IMAGE: "{{ lookup('image', application_id, 'certs_generator', tag_only=True) }}"
|
||||
WAZUH_CERTS_GENERATOR_MOUNTS:
|
||||
- "type=bind,source={{ WAZUH_CERTS_CONFIG_HOST }},target={{ lookup('volume', application_id, 'certs_generator_config').target }},readonly"
|
||||
- "type=bind,source={{ WAZUH_CERTS_ENTRYPOINT_HOST }},target={{ WAZUH_CERTS_ENTRYPOINT_TARGET }},readonly"
|
||||
- "type=bind,source={{ WAZUH_CERTS_DIR_HOST }},target={{ lookup('volume', application_id, 'wazuh_certs_dir').target }}"
|
||||
WAZUH_CERTS_TOOL_VERSION: "{{ lookup('config', application_id, 'services.manager.version') | regex_replace('^(\\d+\\.\\d+).*$', '\\1') }}"
|
||||
|
||||
WAZUH_INSTANCE_DIR: "{{ lookup('container', application_id, 'directories.instance') }}"
|
||||
WAZUH_MANAGER_ENV_FILE: "{{ [lookup('container', application_id, 'directories.env'), 'manager.env'] | path_join }}"
|
||||
WAZUH_DASHBOARD_ENV_FILE: "{{ [lookup('container', application_id, 'directories.env'), 'dashboard.env'] | path_join }}"
|
||||
WAZUH_CERTS_DIR_HOST: "{{ [ WAZUH_INSTANCE_DIR, 'certs' ] | path_join }}"
|
||||
WAZUH_CERTS_CONFIG_HOST: "{{ [ WAZUH_INSTANCE_DIR, 'certs.yml' ] | path_join }}"
|
||||
WAZUH_CERTS_ENTRYPOINT_HOST: "{{ [ WAZUH_INSTANCE_DIR, 'certs-entrypoint.sh' ] | path_join }}"
|
||||
WAZUH_CERTS_ENTRYPOINT_TARGET: "{{ lookup('volume', application_id, 'certs_generator_entrypoint').target }}"
|
||||
WAZUH_INDEXER_ENTRYPOINT_TARGET: "{{ lookup('volume', application_id, 'indexer_entrypoint').target }}"
|
||||
WAZUH_CERTS_MARKER_HOST: "{{ [ WAZUH_INSTANCE_DIR, 'certs.complete' ] | path_join }}"
|
||||
WAZUH_CERTS_REQUIRED_FILES:
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, 'root-ca.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, 'root-ca-manager.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, 'admin.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, 'admin-key.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, WAZUH_MANAGER_CONTAINER ~ '.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, WAZUH_MANAGER_CONTAINER ~ '-key.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, WAZUH_INDEXER_CONTAINER ~ '.pem' ] | path_join }}"
|
||||
- "{{ [ WAZUH_CERTS_DIR_HOST, WAZUH_INDEXER_CONTAINER ~ '-key.pem' ] | path_join }}"
|
||||
|
||||
WAZUH_INDEXER_CERTS_DIR_CONTAINER: "/usr/share/wazuh-indexer/config/certs" # nocheck: container-var-name-lookup in-container path, not a container name
|
||||
WAZUH_INDEXER_SECURITY_DIR_CONTAINER: "/usr/share/wazuh-indexer/config/opensearch-security" # nocheck: container-var-name-lookup in-container path, not a container name
|
||||
|
||||
WAZUH_SYSCTL_CONF: "/etc/sysctl.d/99-wazuh-indexer.conf"
|
||||
|
||||
WAZUH_INDEXER_INTERNAL_API_URL: "https://{{ DOCKER_REACH_HOST }}:{{ WAZUH_INDEXER_HTTP_PORT }}"
|
||||
|
||||
WAZUH_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') | bool }}"
|
||||
WAZUH_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') | bool }}"
|
||||
|
||||
WAZUH_INDEXER_ADMIN_USERNAME: "{{ lookup('users', 'admin').username }}"
|
||||
WAZUH_INDEXER_ADMIN_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.indexer_admin_password') }}"
|
||||
WAZUH_INDEXER_ADMIN_HASH: "{{ WAZUH_INDEXER_ADMIN_PASSWORD | bcrypt_hash(application_id ~ ':indexer-admin') }}"
|
||||
|
||||
WAZUH_DASHBOARD_SERVICE_USERNAME: "{{ lookup('users', 'web-app-wazuh-dashboard').username }}"
|
||||
WAZUH_DASHBOARD_SERVICE_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.dashboard_service_password') }}"
|
||||
WAZUH_DASHBOARD_SERVICE_HASH: "{{ WAZUH_DASHBOARD_SERVICE_PASSWORD | bcrypt_hash(application_id ~ ':dashboard-service') }}"
|
||||
|
||||
WAZUH_WUI_USERNAME: "{{ lookup('users', 'web-app-wazuh-api').username }}"
|
||||
WAZUH_WUI_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.wazuh_wui_password') }}"
|
||||
|
||||
WAZUH_CLUSTER_KEY: "{{ lookup('config', application_id, 'secrets.credentials.manager_cluster_key') }}"
|
||||
|
||||
WAZUH_RBAC_ADMIN_GROUP: "/{{ lookup('rbac_group_path', application_id=application_id, role='administrator') }}"
|
||||
WAZUH_RBAC_ANALYST_GROUP: "/{{ lookup('rbac_group_path', application_id=application_id, role='security-analyst') }}"
|
||||
WAZUH_RBAC_READONLY_GROUP: "/{{ lookup('rbac_group_path', application_id=application_id, role='readonly-auditor') }}"
|
||||
|
||||
WAZUH_RBAC_ADMIN_LDAP_CN: "{{ application_id }}-administrator"
|
||||
WAZUH_RBAC_ANALYST_LDAP_CN: "{{ application_id }}-security-analyst"
|
||||
WAZUH_RBAC_READONLY_LDAP_CN: "{{ application_id }}-readonly-auditor"
|
||||
@@ -0,0 +1,98 @@
|
||||
import unittest
|
||||
|
||||
import bcrypt
|
||||
|
||||
from plugins.filter.bcrypt_hash import FilterModule
|
||||
|
||||
|
||||
class TestBcryptHash(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.f = FilterModule().filters()["bcrypt_hash"]
|
||||
|
||||
def test_hash_verifies_against_the_original_password(self):
|
||||
password = "correct horse battery staple"
|
||||
hashed = self.f(password)
|
||||
self.assertTrue(
|
||||
bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||
)
|
||||
|
||||
def test_hash_does_not_verify_against_a_different_password(self):
|
||||
hashed = self.f("password-one")
|
||||
self.assertFalse(bcrypt.checkpw(b"password-two", hashed.encode("utf-8")))
|
||||
|
||||
def test_hash_is_randomized_salt_each_call(self):
|
||||
password = "same-password"
|
||||
self.assertNotEqual(self.f(password), self.f(password))
|
||||
|
||||
def test_hash_has_bcrypt_prefix(self):
|
||||
hashed = self.f("some-password")
|
||||
self.assertTrue(hashed.startswith(("$2b$", "$2a$")))
|
||||
|
||||
def test_64_character_password_hashes_successfully(self):
|
||||
password = "A" * 64
|
||||
hashed = self.f(password)
|
||||
self.assertTrue(
|
||||
bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||
)
|
||||
|
||||
def test_72_byte_password_hashes_successfully(self):
|
||||
password = "A" * 72
|
||||
hashed = self.f(password)
|
||||
self.assertTrue(
|
||||
bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||
)
|
||||
|
||||
def test_73_byte_password_raises(self):
|
||||
with self.assertRaisesRegex(ValueError, "73 bytes.*at most 72 bytes"):
|
||||
self.f("A" * 73)
|
||||
|
||||
def test_multibyte_password_over_72_bytes_raises(self):
|
||||
password = "ä" * 37
|
||||
self.assertEqual(len(password), 37)
|
||||
with self.assertRaisesRegex(ValueError, "74 bytes"):
|
||||
self.f(password)
|
||||
|
||||
def test_same_secret_and_seed_give_the_same_hash(self):
|
||||
self.assertEqual(
|
||||
self.f("same-password", "app:indexer-admin"),
|
||||
self.f("same-password", "app:indexer-admin"),
|
||||
)
|
||||
|
||||
def test_different_seed_gives_a_different_hash(self):
|
||||
self.assertNotEqual(
|
||||
self.f("same-password", "app:indexer-admin"),
|
||||
self.f("same-password", "app:dashboard-service"),
|
||||
)
|
||||
|
||||
def test_different_secret_gives_a_different_hash(self):
|
||||
self.assertNotEqual(
|
||||
self.f("password-one", "app:indexer-admin"),
|
||||
self.f("password-two", "app:indexer-admin"),
|
||||
)
|
||||
|
||||
def test_seeded_hash_verifies_against_the_original_password(self):
|
||||
password = "correct horse battery staple"
|
||||
hashed = self.f(password, "app:indexer-admin")
|
||||
self.assertTrue(
|
||||
bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||
)
|
||||
self.assertTrue(hashed.startswith("$2b$12$"))
|
||||
|
||||
def test_no_seed_stays_randomized(self):
|
||||
self.assertNotEqual(self.f("same-password"), self.f("same-password"))
|
||||
|
||||
def test_empty_seed_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.f("some-password", "")
|
||||
|
||||
def test_none_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.f(None)
|
||||
|
||||
def test_empty_string_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.f("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user