# Wazuh ## But Il y a 2 sources de logs qui nous intéressent : - les logs des conteneurs des pods ; - l'audit de l'apiserver. Il n'est pas possible de récupérer les logs des pods à travers l'audit de l'apiserver (à part peut-être de façon bancale en interrogeant l'endpoint /logs mais ce n'est pas viable). La centralisation des logs de conteneurs de pods a plusieurs objectifs : - l'aide au débogage des applications tournant dans le cluster (ex: portail) - l'aide au débogage des applications "système" tournant dans le cluster (ex: coredns) - la rétention d'informations légales (tentatives de connexion par exemple) L'audit de l'apiserveur a plusieurs objectifs : - la détection de comportement suspicieux (accès/modifications/tentatives d'accès aux ressources) - l'aide au débogage en cas de grave disfonctionnement du cluster ## Audit de l'apiserver ### Solutions explorées 2 solutions ont été explorées : 1. Installer sur chaque noeud le client wazuh et créer un objet k8s de type audit policy ; 2. Installer sur le serveur wazuh un listener et créer un objet k8s de type audit policy. La solution 1 n'est pas envisageable, les noeuds étant immutables. Après recherches et discussion sur le slack de Wazuh, il semblerait qu'aucun agent wazuh ne supporte un environnement k8s et que la solution 2 soit la meilleure solution. ### Lab local Pour un test local, la manière la plus simple est de suivre les docs suivantes : - https://documentation.wazuh.com/4.11/deployment-options/docker/wazuh-container.html#single-node-deployment - https://wazuh.com/blog/auditing-kubernetes-with-wazuh/ avec quelques modifications cependant pour ajouter un listener sur le port 1580 dans le conteneur de wazuh afin d'envoyer les logs à la socket unix de wazuh. Attention : cette procédure est un POC largement améliorable. Cela reste avant tout une piste de démarrage pour travailler sur le sujet. #### Installation du serveur wazuh Suivre le tutoriel https://documentation.wazuh.com/4.11/deployment-options/docker/wazuh-container.html#single-node-deployment pour la génération des certificats. Ajouter localement les fichiers suivants : - custom-webhook.py ``` #!/var/ossec/framework/python/bin/python3 import json from socket import socket, AF_UNIX, SOCK_DGRAM from flask import Flask, request # CONFIG PORT = 1580 CERT = '/var/ossec/api/configuration/ssl/server.crt' CERT_KEY = '/var/ossec/api/configuration/ssl/server.key' # Analysisd socket address socket_addr = '/var/ossec/queue/sockets/queue' def send_event(msg): string = '1:k8s:{0}'.format(json.dumps(msg)) sock = socket(AF_UNIX, SOCK_DGRAM) sock.connect(socket_addr) sock.send(string.encode()) sock.close() return True app = Flask(__name__) context = (CERT, CERT_KEY) @app.route('/', methods=['POST']) def webhook(): if request.method == 'POST': if send_event(request.json): print("Request sent to Wazuh") else: print("Failed to send request to Wazuh") return "Webhook received!" if __name__ == '__main__': app.run(host='0.0.0.0', port=PORT, ssl_context=context) ``` - local_rules.xml ``` k8s audit.k8s.io/v1 Kubernetes audit log. 110002 requestURI\":.+", \"verb\": \"create Kubernetes request to create resource 110002 requestURI\":.+", \"verb\": \"delete Kubernetes request to delete resource ``` Dans le docker-compose.yml, ajouter : - le mapping de port "1580:1580" au wazuh.manager - le mapping ./custom-webhook.py:/var/ossec/integrations/custom-webhook.py - le mapping ./local_rules.xml:/var/ossec/etc/rules/local_rules.xml Ce qui donne : ``` # Wazuh App Copyright (C) 2017, Wazuh Inc. (License GPLv2) version: '3.7' services: wazuh.manager: image: wazuh/wazuh-manager:4.11.1 hostname: wazuh.manager restart: always ulimits: memlock: soft: -1 hard: -1 nofile: soft: 655360 hard: 655360 ports: - "1514:1514" - "1515:1515" - "1580:1580" - "514:514/udp" - "55000:55000" environment: - INDEXER_URL=https://wazuh.indexer:9200 - INDEXER_USERNAME=admin - INDEXER_PASSWORD=SecretPassword - FILEBEAT_SSL_VERIFICATION_MODE=full - SSL_CERTIFICATE_AUTHORITIES=/etc/ssl/root-ca.pem - SSL_CERTIFICATE=/etc/ssl/filebeat.pem - SSL_KEY=/etc/ssl/filebeat.key - API_USERNAME=wazuh-wui - API_PASSWORD=MyS3cr37P450r.*- volumes: - wazuh_api_configuration:/var/ossec/api/configuration - wazuh_etc:/var/ossec/etc - wazuh_logs:/var/ossec/logs - wazuh_queue:/var/ossec/queue - wazuh_var_multigroups:/var/ossec/var/multigroups - wazuh_integrations:/var/ossec/integrations - wazuh_active_response:/var/ossec/active-response/bin - wazuh_agentless:/var/ossec/agentless - wazuh_wodles:/var/ossec/wodles - filebeat_etc:/etc/filebeat - filebeat_var:/var/lib/filebeat - ./config/wazuh_indexer_ssl_certs/root-ca-manager.pem:/etc/ssl/root-ca.pem - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key - ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf - ./custom-webhook.py:/var/ossec/integrations/custom-webhook.py - ./wazuh-webhook.service:/lib/systemd/system/wazuh-webhook.service - ./local_rules.xml:/var/ossec/etc/rules/local_rules.xml wazuh.indexer: image: wazuh/wazuh-indexer:4.11.1 hostname: wazuh.indexer restart: always ports: - "9200:9200" environment: - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g" ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - wazuh-indexer-data:/var/lib/wazuh-indexer - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/certs/root-ca.pem - ./config/wazuh_indexer_ssl_certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.key - ./config/wazuh_indexer_ssl_certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.pem - ./config/wazuh_indexer_ssl_certs/admin.pem:/usr/share/wazuh-indexer/certs/admin.pem - ./config/wazuh_indexer_ssl_certs/admin-key.pem:/usr/share/wazuh-indexer/certs/admin-key.pem - ./config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/opensearch.yml - ./config/wazuh_indexer/internal_users.yml:/usr/share/wazuh-indexer/opensearch-security/internal_users.yml wazuh.dashboard: image: wazuh/wazuh-dashboard:4.11.1 hostname: wazuh.dashboard restart: always ports: - 443:5601 environment: - INDEXER_USERNAME=admin - INDEXER_PASSWORD=SecretPassword - WAZUH_API_URL=https://wazuh.manager - DASHBOARD_USERNAME=kibanaserver - DASHBOARD_PASSWORD=kibanaserver - API_USERNAME=wazuh-wui - API_PASSWORD=MyS3cr37P450r.*- volumes: - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem - ./config/wazuh_dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml - ./config/wazuh_dashboard/wazuh.yml:/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml - wazuh-dashboard-config:/usr/share/wazuh-dashboard/data/wazuh/config - wazuh-dashboard-custom:/usr/share/wazuh-dashboard/plugins/wazuh/public/assets/custom depends_on: - wazuh.indexer links: - wazuh.indexer:wazuh.indexer - wazuh.manager:wazuh.manager volumes: wazuh_api_configuration: wazuh_etc: wazuh_logs: wazuh_queue: wazuh_var_multigroups: wazuh_integrations: wazuh_active_response: wazuh_agentless: wazuh_wodles: filebeat_etc: filebeat_var: wazuh-indexer-data: wazuh-dashboard-config: wazuh-dashboard-custom: ``` Ensuite, lancer le serveur avec ``` docker-compose up ``` Se connecter au conteneur pour installer flask (pour notre listener) : ``` docker exec single-node-wazuh.manager-1 /var/ossec/framework/python/bin/pip3 install flask ``` puis démarrer le serveur webhook : ``` docker exec single-node-wazuh.manager-1 /var/ossec/framework/python/bin/python3 /var/ossec/integrations/custom-webhook.py ``` A ce moment là, le serveur est prêt à recevoir des logs d'audit k8s. Le serveur devrait être accessible sur https://127.0.0.1/app/login (admin/SecretPassword). Note : il reste un problème avec les dashboard qui ne s'affichent pas dans l'interface web. Ce n'est pas grave, les logs reste accessibles dans l'exploration de données. #### Lien entre le cluster et le serveur wazuh Pour lier le cluster et le serveur wazuh en réseau, sans s'embêter avec les configurations réseau, on peut faire un tunnel (remplacer l'ip par l'ip du contenur wazuh-server) : ``` ssh -L 0.0.0.0:1581:172.21.0.3:1580 127.0.0.1 ``` #### Création du cluster k8s "kind" On va créer un cluster kind nommé "wazuh" avec 1 CP et 1 Worker. On ajoute 2 fichiers de conf pour kubeapi-server : audit-policy.yaml et audit-webhook.yaml. wazuh/audit-policy.yaml: ``` apiVersion: audit.k8s.io/v1 kind: Policy rules: # Don’t log requests to the following API endpoints - level: None nonResourceURLs: - '/healthz*' - '/metrics' - '/swagger*' - '/version' # Limit requests containing tokens to Metadata level so the token is not included in the log #- level: Metadata # omitStages: # - RequestReceived # resources: # - group: authentication.k8s.io # resources: # - tokenreviews # Extended audit of auth delegation #- level: RequestResponse # omitStages: # - RequestReceived # resources: # - group: authorization.k8s.io # resources: # - subjectaccessreviews # Log changes to pods at RequestResponse level - level: RequestResponse omitStages: - RequestReceived resources: # core API group; add third-party API services and your API services if needed - group: '' resources: ['pods'] verbs: ['create', 'patch', 'update', 'delete'] # Log everything else at Metadata level - level: Metadata omitStages: - RequestReceived - level: RequestResponse nonResourceURLs: - '/logs' ``` wazuh/audit-webhook.yaml (attention, changer MON_IP par 192.168.10.XX): ``` apiVersion: v1 kind: Config preferences: {} clusters: - name: wazuh-webhook cluster: insecure-skip-tls-verify: true server: https://MON_IP:1581 # kubeconfig files require a context. Provide one for the API server. current-context: webhook contexts: - context: cluster: wazuh-webhook user: kube-apiserver # Replace with name of API server if it’s different name: webhook ``` wazuh.yaml: ``` kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: wazuh nodes: - role: control-plane image: reg.cadoles.com/dh/kindest/node:v1.28.7 kubeadmConfigPatches: - | kind: ClusterConfiguration apiServer: extraArgs: audit-webhook-batch-max-size: "1" audit-webhook-config-file: "/etc/kubernetes/audit-webhook.yaml" audit-policy-file: "/etc/kubernetes/policies/audit-policy.yaml" extraVolumes: - name: audit-policies hostPath: "/etc/kubernetes/policies" mountPath: "/etc/kubernetes/policies" readOnly: true pathType: "DirectoryOrCreate" - name: audit-webhook hostPath: "/etc/kubernetes/audit-webhook.yaml" mountPath: "/etc/kubernetes/audit-webhook.yaml" readOnly: true - | kind: InitConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "ingress-ready=true" extraMounts: - hostPath: "./wazuh/audit-policy.yaml" containerPath: "/etc/kubernetes/policies/audit-policy.yaml" readOnly: true - hostPath: "./wazuh/audit-webhook.yaml" containerPath: "/etc/kubernetes/audit-webhook.yaml" readOnly: true extraPortMappings: - containerPort: 31000 hostPort: 31001 listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0" - containerPort: 80 hostPort: 8081 listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0" labels: ingress-ready: true - role: worker image: reg.cadoles.com/dh/kindest/node:v1.28.7 kubeadmConfigPatches: - | kind: JoinConfiguration nodeRegistration: kubeletExtraArgs: system-reserved: memory=2Gi ``` ``` kind create cluster --config wazuh.yaml ``` À ce moment là, on doit pouvoir voir des logs coté de custom-webhook.py qui reçoit des logs du cluster. Ensuite, dans l'interface web de wazuh, on doit pouvoir retrouver les logs du cluster dans l'exploration de données. Un filtre sur "data.apiVersion=audit.k8s.io/v1" devrait retourner uniquement les logs serveur. ### Analyse Comme indiqué en introduction, cette méthode permet de recevoir dans Wazuh toutes les activités passant par l'apiserver. Cependant : - il faut prendre en compte une augmentation de la consommation mémoire sur les controle-plane ; - il faut customizer audit-policy.yaml afin de filtrer les appels à l'apiserver réellement important et éviter de se retrouver avec trop de logs ; - il faut trouver un moyen de filtrer par cluster (prod et pp) > The audit logging feature increases the memory consumption of the API server because some context required for auditing is stored for each request. Memory consumption depends on the audit logging configuration. ## Journalisation au niveau cluster ### Solutions explorées D'après la documentation [kubernetes](https://kubernetes.io/docs/concepts/cluster-administration/logging/#cluster-level-logging-architectures), la meilleure solution dans notre cas est d'installer un daemonset dans le cluster ayant accès au dossier /var/log/pods. Reste à déterminer quel logiciel faire tourner dans le pod. ### DaemonSet Après discussion sur le slack de Wazuh, il nous ait conseillé par un employé de Wazuh d'utiliser un DaemonSet géré par OpenNix https://opennix.org/en/opensource/. OpenNix offre 2 possibilités : utiliser le DaemonSet ou utiliser un opérateur k8s. L'opérateur n'ayant été pull que peu de fois, et après analyse du dépôt source (https://github.com/pyToshka/wazuh-agent-kubernetes-operator), il semblerait qu'il ne soit utile que pour produire un DaemonSet et un Secret avec les données passées en paramètre. L'ajout d'un DaemonSet et d'un Secret directement semble plus adapté. OpenNix est une organization russe. Il a été décidé de réimplémenter une solution pour intégrer l'agent Wazuh dans kubernetes. Cela se présente sous la forme de projets maintenus par Cadoles : - wazuh-agent-k8s-autoadd : programme go pour enregistrer puis démarrer un agent Wazuh + Dockerfile - wazuh-agent-kustom : kustomization d'un DaemonSet, d'une ConfigMap et d'un Secret - wazuh-agent-container : Dockerfile pour créer un conteneur wazuh-agent DaemonSet: 2 images, l'une register et produit le fichier de conf ossec, l'autre démarre ensuite avec l'agent wazuh. Health endpoint? Demander : - version du wazuh manager (la version de l'agent doit être inférieure ou égale) - certificats ? - nom/addresse du manager wazuh ou endpoint