Add ELK stack for Suricata log visualisation

Elasticsearch + Kibana + Filebeat in Docker, bridged via an elk network.
Filebeat uses the Suricata module to parse eve.json and auto-installs
Kibana dashboards on first run. ES heap capped at 1g; Kibana Node heap
at 512m — total stack ~2-2.5 GB RAM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ediblerope 2026-04-06 21:25:29 +01:00
parent 43ce6b046f
commit 699bbd9f9a
2 changed files with 101 additions and 0 deletions

View file

@ -31,6 +31,7 @@
./services/cloudflare-ddns.nix
./services/fail2ban.nix
./services/suricata.nix
./services/elk.nix
];
### Make build time quicker

100
services/elk.nix Normal file
View file

@ -0,0 +1,100 @@
{ config, lib, pkgs, ... }:
let
# Pin all three to the same version — mismatches between ES/Kibana/Filebeat cause errors
# Check https://www.docker.elastic.co for latest 8.x tag
elasticVersion = "8.17.0";
filebeatConfig = pkgs.writeText "filebeat.yml" ''
filebeat.modules:
- module: suricata
eve:
enabled: true
var.paths: ["/var/log/suricata/eve.json"]
# Auto-install Kibana dashboards on first run
setup.dashboards.enabled: true
setup.kibana:
host: "kibana:5601"
output.elasticsearch:
hosts: ["elasticsearch:9200"]
# Reduce logging noise
logging.level: warning
'';
in
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
virtualisation.oci-containers.containers = {
elasticsearch = {
image = "docker.elastic.co/elasticsearch/elasticsearch:${elasticVersion}";
environment = {
# Single-node cluster — no replica shards needed
"discovery.type" = "single-node";
# Security disabled — ES is not exposed externally
"xpack.security.enabled" = "false";
# Keep heap at 1g; ES default is 50% of RAM which is excessive here
"ES_JAVA_OPTS" = "-Xms1g -Xmx1g";
};
volumes = [ "elasticsearch-data:/usr/share/elasticsearch/data" ];
extraOptions = [ "--network=elk" ];
};
kibana = {
image = "docker.elastic.co/kibana/kibana:${elasticVersion}";
environment = {
"ELASTICSEARCH_HOSTS" = "http://elasticsearch:9200";
# Cap Node.js heap — default is uncapped
"NODE_OPTIONS" = "--max-old-space-size=512";
};
ports = [ "5601:5601" ];
extraOptions = [ "--network=elk" ];
dependsOn = [ "elasticsearch" ];
};
filebeat = {
image = "docker.elastic.co/beats/filebeat:${elasticVersion}";
extraOptions = [
"--network=elk"
# root so it can read /var/log/suricata owned by the suricata user
"--user=root"
# Filebeat refuses to start if config file is group/world writable
"--security-opt=no-new-privileges:true"
];
volumes = [
"/var/log/suricata:/var/log/suricata:ro"
"${filebeatConfig}:/usr/share/filebeat/filebeat.yml:ro"
];
dependsOn = [ "elasticsearch" "kibana" ];
};
};
# Create the elk bridge network before any container starts
systemd.services.init-elk-docker-network = {
description = "Create elk Docker network";
after = [ "docker.service" ];
requires = [ "docker.service" ];
before = [
"docker-elasticsearch.service"
"docker-kibana.service"
"docker-filebeat.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
${pkgs.docker}/bin/docker network inspect elk >/dev/null 2>&1 \
|| ${pkgs.docker}/bin/docker network create elk
'';
};
# Kibana accessible on the LAN
networking.firewall.allowedTCPPorts = [ 5601 ];
};
}