Authoritative per-field reference for TenantConfig (src/wazuh_mcp/tenancy/config.py). Every field, its type, default, validator, and operational semantics.
TenantConfig is a strict Pydantic model — model_config = ConfigDict(extra="forbid", frozen=True). Unknown fields fail config validation loudly at startup; instances are immutable post-construction.
| Type | str |
| Pattern | ^[a-z0-9][a-z0-9_-]{0,62}$ |
| Validator | Pydantic regex (1..63 chars, lowercase + _-, must start alphanumeric) |
Stable identifier the operator picks per tenant. Used as the routing key everywhere — pool acquisition, RBAC resolution, rate-limit bucket key, audit-sink fan-out, secret-store path component. Renaming a tenant requires re-issuing tokens and re-keying secrets.
| Type | HttpUrl |
| Validator | Pydantic HttpUrl (must parse, scheme + host required) |
Wazuh Indexer base URL (typically https://wazuh.<tenant>.example:9200). The Server API URL is derived from this by substituting port 9200 → 55000, unless server_api_url overrides it.
| Type | HttpUrl | None |
| Default | None |
Explicit Wazuh Server API base URL. When unset, the pool derives it from indexer_url (port 9200 → 55000 substitution). M5b added this so multi-manager fixtures can point distinct tenants at distinct manager clusters without colliding on the derived URL.
Set this when the tenant's manager cluster lives on a non-default port, on a different host than its indexer, or when running federation-style deployments where multiple tenants must each target a distinct manager.
| Type | bool |
| Default | True |
TLS-verify upstream Wazuh calls. Set False only for local dev with self-signed certs.
| Type | Path | None |
| Default | None |
Path to a CA bundle the indexer + Server API pools use for TLS verification. Required when verify_tls=True and the upstream uses a private CA. Owned by the MCP service user, mode 0644.
| Type | str |
| Validator | None — free-form string |
Role assigned when the OAuth bearer carries no claim from oauth.rbac_claims. Typical values: analyst, responder, readonly, admin. Must match a key in the effective role_tool_allowlist (or one of the shipped defaults).
| Type | dict[str, list[str]] | None |
| Default | None (use shipped defaults) |
Per-role tool pattern allowlist. Patterns use the same glob shape as the shipped defaults:
role_tool_allowlist:
admin: ["*"]
responder: ["alerts.*", "agents.*", "fim.*", "write.isolate_agent"]
analyst: ["alerts.*", "agents.*", "vulnerabilities.*", "mitre.*", "hunt.*", "fim.*", "cluster.*"]
readonly: ["alerts.*", "agents.get_agent", "agents.list_agents", "vulnerabilities.*", "mitre.*", "fim.*", "cluster.status"]Absent roles fall through to the shipped defaults. None means use defaults entirely.
| Type | HttpUrl | None |
| Default | None (inherit server.yaml's oauth.issuer) |
OAuth issuer URL for tokens scoped to this tenant. Used by IssuerIndex to map verified iss claims back to tenants. Multiple tenants MAY share an issuer URL (multi-tenant Keycloak realms distinguished only by tenant_id claim) — see oauth.md for the IssuerIndex semantics.
| Type | str | None |
| Default | None (inherit server.yaml's oauth.audience) |
Expected aud claim on tokens for this tenant.
| Type | str |
| Default | "wazuh_user" |
Name of the OAuth claim that carries the Wazuh user identity for run_as attribution. When the claim is present in a verified bearer, Session.wazuh_user is populated and Server API calls pass ?run_as=<value>. When absent, calls run as the tenant's service account with no per-operator attribution.
| Type | str | None |
| Default | None (use the secret driver's default wazuh-mcp/) |
Override the default secret-path prefix for this tenant. Resolves to {secret_prefix}{tenant_id}/{key} in the backing store. Use this when one tenant lives in an existing secret namespace (acquired customer, separate AWS account using a shared MCP deploy via cross-account IAM) and the rest follow the default.
See secrets.md for driver-specific path semantics.
| Type | RateLimitConfig |
| Default | tenant: capacity=250, refill_per_sec=4.17 / session: capacity=60, refill_per_sec=1.0 |
Two token buckets per tenant:
rate_limit:
tenant:
capacity: 100
refill_per_sec: 10.0
session:
capacity: 10
refill_per_sec: 1.0BucketConfig:
capacity:intin(0, 100_000]. Bucket size — burst budget.refill_per_sec:floatin(0.0, 1000.0]. Refill rate.
The tenant bucket is shared across every session for the tenant; the session bucket is per-session. A call must successfully acquire from BOTH buckets. Denials surface as WazuhError("rate_limited", ..., scope="rate_limit:tenant" | "rate_limit:session") and increment mcp_rate_limit_drops_total{tenant, scope} (see observability.md).
The defaults are tuned for ~50 tenants on one MCP deploy with light query volume. Heavy-traffic tenants should override upward; locked-down tenants (e.g. readonly MSP customers) downward.
| Type | list[AuditSinkConfig] |
| Default | [] (the global stderr sink covers; no per-tenant sinks) |
Discriminated-union list. Each entry's kind field picks the variant:
kind: stderr— no fields.kind: stdout— no fields. HTTP-only; under stdio it corrupts the JSON-RPC wire.kind: file—path(Path, required),rotate_size_mb(default 100, 1..10000),keep(default 5, 0..100).kind: http—url(HttpUrl, required),batch(default 50),flush_ms(default 500),max_attempts(default 5).kind: wazuh_indexer—index_prefix(defaultwazuh-mcp-audit),batch(default 100),flush_ms(default 1000),max_attempts(default 5).
See observability.md for sink semantics, the MultiSinkAuditEmitter dual-track model, and Wazuh Dashboards setup.
| Type | list[str] | None |
| Default | None (no filter — all writes callable) |
| Validator | _validate_write_allowlist_entry — must start with write., must be in the known-tool set |
Three meaningful states:
None— no filter. All 9 writes callable. RBAC still applies on top.[]— empty list. All 9 writes register but every call denies withforbidden. M4c-shifted from "tools hidden inlist_tools" to "tools shown but uniformly denied".["write.isolate_agent", ...]— only listed names callable. Unknown names fail config validation loudly at startup.
Valid names: write.isolate_agent, write.restart_agent, write.add_agent_to_group, write.remove_agent_from_group, write.create_rule, write.update_rule, write.run_active_response, write.run_active_response_on_group, write.restart_manager.
See writes.md for the full two-layer allowlist model.
| Type | list[str] |
| Default | [] (deny-all) |
| Validator | _validate_ar_command_name — must be non-empty after strip |
Allowed command_name values for write.run_active_response and write.run_active_response_on_group. Default is empty — every AR command denies until the operator lists commands explicitly.
The MCP allowlist does not teach Wazuh anything. Commands listed here MUST also be declared in the Wazuh manager's ossec.conf under <command> and bound to an <active-response> block. If the MCP allowlist names a command Wazuh does not know, the Server API call reaches Wazuh and Wazuh fails it upstream.
| Type | list[str] |
| Default | [] (deny-all) |
| Validator | _validate_ar_group_name — pattern ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$, length 1..128 |
| Max length | 50 entries |
Allowed group_name values for write.run_active_response_on_group. Mirrors the active_response_allowlist precedent: empty by default, list explicit group names per tenant.
Both gates apply for group-target AR calls:
write.run_active_response_on_groupmust be inwrite_allowlist(orwrite_allowlist=null).- The session's role must allow
write.run_active_response_on_groupperrole_tool_allowlist. group_namearg must be inagent_group_allowlist.command_namearg must be inactive_response_allowlist.
See writes.md for the full group-target AR setup.
Multi-tenant config with one strict + one open tenant:
tenants:
- tenant_id: acme
indexer_url: https://wazuh.acme.internal:9200
server_api_url: https://wazuh-api.acme.internal:55000 # M5b — explicit override
verify_tls: true
ca_bundle_path: /etc/wazuh-mcp/ca/acme.pem
default_rbac_role: analyst
oauth_issuer: https://idp.example.com/realms/msp
oauth_audience: wazuh-mcp-api
wazuh_user_claim: wazuh_user
secret_prefix: "prod/soc/wazuh-mcp/"
role_tool_allowlist:
admin: ["*"]
responder: ["alerts.*", "agents.*", "fim.*", "write.isolate_agent", "write.run_active_response_on_group"]
analyst: ["alerts.*", "agents.*", "vulnerabilities.*", "mitre.*", "hunt.*", "fim.*", "cluster.*"]
rate_limit:
tenant: { capacity: 500, refill_per_sec: 25.0 }
session: { capacity: 50, refill_per_sec: 5.0 }
audit_sinks:
- kind: wazuh_indexer
index_prefix: acme-audit
- kind: file
path: /var/log/wazuh-mcp/acme.jsonl
write_allowlist:
- write.isolate_agent
- write.restart_agent
- write.run_active_response
- write.run_active_response_on_group
- write.create_rule
- write.update_rule
- write.restart_manager
active_response_allowlist:
- isolate
- firewall-drop
- disable-account
agent_group_allowlist:
- linux-prod
- windows-prod
- dmz
- tenant_id: contoso
indexer_url: https://wazuh.contoso.example:9200
default_rbac_role: readonly
rate_limit:
tenant: { capacity: 25, refill_per_sec: 1.0 }
session: { capacity: 5, refill_per_sec: 0.2 }
audit_sinks:
- kind: wazuh_indexer
index_prefix: contoso-audit
# No write_allowlist override — defaults to None (all writes callable, but
# readonly role's allowlist denies them all anyway).
# No active_response_allowlist / agent_group_allowlist — empty defaults
# mean any AR call denies even if RBAC permitted it.install.md— where this file lives + the surroundingserver.yamlandsecrets.yaml.oauth.md—oauth_issuer+oauth_audience+wazuh_user_claimsemantics.writes.md—write_allowlist+active_response_allowlist+agent_group_allowlistoperational model.multi-tenant.md— per-tenant resolver model + structural isolation guarantees.observability.md—rate_limit+audit_sinksruntime behavior.secrets.md—secret_prefix+ driver-specific path resolution.