GuíasGuides · Nivel 3Level 3

Elasticsearch, Logstash y Kibana: centralizar los logs de los servidores y buscar un errorElasticsearch, Logstash and Kibana: centralising server logs and finding an error

Un nodo de Elasticsearch, Filebeat en los servidores, el pipeline de Logstash que da forma a los eventos y la búsqueda en Kibana que sustituye a veinte sesiones SSH.One Elasticsearch node, Filebeat on the servers, the Logstash pipeline that shapes events, and the Kibana search that replaces twenty SSH sessions.

Qué vas a conseguir

Que los logs de todos los servidores Linux (y los de Windows, con el mismo agente) lleguen a un único sitio donde buscar "permission denied" o un ID de correlación en segundos, con gráficos de cuántos errores hubo y cuándo. Al terminar tendrás la pila mínima en Docker y dos servidores enviando.

Antes de empezar

  • El servidor de herramientas de la guía de Docker, con 8 GB de RAM y 100 GB de disco para los índices.
  • Puertos abiertos en su firewall: 5044 (Beats → Logstash) desde los servidores, 5601 (Kibana) desde la red de IT.
  • Decidir la retención: cuántos días de logs guardas. Treinta es un buen punto de partida.

1. La pila con Compose

docker-compose.yml
services:  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3    environment:      - discovery.type=single-node      - xpack.security.enabled=true      - ELASTIC_PASSWORD_FILE=/run/secrets/elastic      - ES_JAVA_OPTS=-Xms2g -Xmx2g    volumes: [es-data:/usr/share/elasticsearch/data]    secrets: [elastic]    ulimits: { memlock: { soft: -1, hard: -1 } }  logstash:    image: docker.elastic.co/logstash/logstash:8.15.3    volumes: [./pipeline:/usr/share/logstash/pipeline:ro]    ports: ["5044:5044"]    depends_on: [elasticsearch]  kibana:    image: docker.elastic.co/kibana/kibana:8.15.3    ports: ["5601:5601"]    environment:      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200    depends_on: [elasticsearch]volumes:  es-data:secrets:  elastic:    file: ./secrets/elastic.txt
bash · srv-tools-01
sudo sysctl -w vm.max_map_count=262144 && echo "vm.max_map_count=262144" | sudo tee /etc/sysctl.d/99-elastic.confdocker compose up -d && sleep 60curl -s -u elastic:$(cat secrets/elastic.txt) http://localhost:9200/_cluster/health?pretty | grep -E 'status|number_of_nodes'  "status" : "green",  "number_of_nodes" : 1,

Un solo nodo y status: green es lo normal para un entorno de IT interno. Kibana necesita su propia cuenta de servicio: docker compose exec elasticsearch bin/elasticsearch-reset-password -u kibana_system y la pones en la variable ELASTICSEARCH_PASSWORD de Kibana.

2. El pipeline de Logstash

pipeline/syslog.conf
input {  beats { port => 5044 }}filter {  if [event][dataset] == "system.syslog" {    grok {      match => { "message" => "%{SYSLOGTIMESTAMP:ts} %{SYSLOGHOST:host.name} %{DATA:process.name}(?:\[%{POSINT:process.pid}\])?: %{GREEDYDATA:log.message}" }    }    date { match => ["ts", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss"] }    mutate { remove_field => ["ts"] }  }  if "Permission denied" in [message] or "Failed password" in [message] {    mutate { add_tag => ["security"] }  }}output {  elasticsearch {    hosts => ["http://elasticsearch:9200"]    user => "elastic"    password => "${ELASTIC_PASSWORD}"    index => "logs-linux-%{+YYYY.MM.dd}"  }}

El grok convierte una línea de texto en campos (host.name, process.name, log.message): sin eso solo podrías buscar texto libre. La etiqueta security es el primer ejemplo de algo que luego usarás en una alerta. Comprueba la sintaxis antes de arrancar: docker compose exec logstash bin/logstash --config.test_and_exit -f /usr/share/logstash/pipeline/syslog.conf.

3. Filebeat en los servidores

bash · srv-web-01
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.15.3-x86_64.rpm && sudo rpm -vi filebeat-8.15.3-x86_64.rpmsudo filebeat modules enable systemsudo sed -i 's|#output.logstash:|output.logstash:|; s|#hosts: \["localhost:5044"\]|hosts: ["srv-tools-01.contoso.local:5044"]|' /etc/filebeat/filebeat.ymlsudo sed -i 's|^output.elasticsearch:|#output.elasticsearch:|; s|^  hosts: \["localhost:9200"\]|#  hosts: ["localhost:9200"]|' /etc/filebeat/filebeat.ymlsudo filebeat test outputlogstash: srv-tools-01.contoso.local:5044...  connection...    parse host... OK    dns lookup... OK    TCP... OK  talk to server... OKsudo systemctl enable --now filebeat

filebeat test output es la prueba que haces antes de abrir un ticket al equipo de red: si TCP... OK falla, es el firewall; si falla talk to server, es Logstash. Con Ansible (guía anterior), este paso es un playbook de diez líneas para toda la flota.

4. Buscar en Kibana

Kibana · Discover
AnalyticsDiscoverlogs-linux-*
RangoLast 24 hours
KQLhost.name : "srv-web-01" and tags : "security"
Resultados37 hits
Oct 22, 2026 07:58:12 srv-web-01 sshd[2231] Failed password for invalid user admin from 10.20.4.77 port 51022 ssh2
Oct 22, 2026 07:58:14 srv-web-01 sshd[2231] Failed password for invalid user admin from 10.20.4.77 port 51030 ssh2
Oct 22, 2026 08:02:11 srv-web-01 httpd[1842] (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8081
Add filter
Save
Share

Para que Kibana vea los índices hace falta una Data view (Stack Management → Data views → Create, patrón logs-linux-*, campo de tiempo @timestamp). A partir de ahí: la misma búsqueda en todos los servidores a la vez, el histograma arriba te dice a qué hora empezó el problema, y Visualize sobre process.name te enseña qué servicio genera más errores.

5. Retención y espacio

bash · srv-tools-01
curl -s -u elastic:$(cat secrets/elastic.txt) -X PUT http://localhost:9200/_ilm/policy/logs-30d -H 'Content-Type: application/json' -d '{"policy":{"phases":{"hot":{"actions":{}},"delete":{"min_age":"30d","actions":{"delete":{}}}}}}'{"acknowledged":true}curl -s -u elastic:$(cat secrets/elastic.txt) -X PUT http://localhost:9200/_index_template/logs-linux -H 'Content-Type: application/json' -d '{"index_patterns":["logs-linux-*"],"template":{"settings":{"index.lifecycle.name":"logs-30d","number_of_replicas":0}}}'{"acknowledged":true}curl -s -u elastic:$(cat secrets/elastic.txt) 'http://localhost:9200/_cat/indices/logs-*?v&h=index,docs.count,store.size&s=index' | tail -3logs-linux-2026.10.20   412033   211.4mblogs-linux-2026.10.21   398870   204.9mblogs-linux-2026.10.22    88121    46.1mb

Con un solo nodo, number_of_replicas: 0 evita que el clúster esté siempre en amarillo. Unos 200 MB al día por veinte servidores es el orden de magnitud; mira _cat/indices al mes para ajustar la retención al disco que tienes.

Siguientes pasos

  • Windows: el agente es Winlogbeat con el mismo output.logstash; los eventos de seguridad (4625, 4740) llegan ya con campos.
  • Una alerta en Kibana (Alerts and Insights → Rules) sobre tags: security con más de 20 eventos en 5 minutos, enviada al canal del equipo.
  • Si el volumen crece, la opción es Elastic Agent + Fleet en lugar de Beats sueltos, o pasar los dashboards a Grafana (siguiente guía) con Elasticsearch como origen de datos.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

Logs from every Linux server (and Windows, with the same agent) arriving in one place where you can search for "permission denied" or a correlation ID in seconds, with charts of how many errors there were and when. By the end you will have the minimal stack in Docker and two servers shipping.

Before you start

  • The tools server from the Docker guide, with 8 GB of RAM and 100 GB of disk for the indices.
  • Ports open in its firewall: 5044 (Beats → Logstash) from the servers, 5601 (Kibana) from the IT network.
  • Decide the retention: how many days of logs you keep. Thirty is a good starting point.

1. The stack with Compose

docker-compose.yml
services:  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3    environment:      - discovery.type=single-node      - xpack.security.enabled=true      - ELASTIC_PASSWORD_FILE=/run/secrets/elastic      - ES_JAVA_OPTS=-Xms2g -Xmx2g    volumes: [es-data:/usr/share/elasticsearch/data]    secrets: [elastic]    ulimits: { memlock: { soft: -1, hard: -1 } }  logstash:    image: docker.elastic.co/logstash/logstash:8.15.3    volumes: [./pipeline:/usr/share/logstash/pipeline:ro]    ports: ["5044:5044"]    depends_on: [elasticsearch]  kibana:    image: docker.elastic.co/kibana/kibana:8.15.3    ports: ["5601:5601"]    environment:      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200    depends_on: [elasticsearch]volumes:  es-data:secrets:  elastic:    file: ./secrets/elastic.txt
bash · srv-tools-01
sudo sysctl -w vm.max_map_count=262144 && echo "vm.max_map_count=262144" | sudo tee /etc/sysctl.d/99-elastic.confdocker compose up -d && sleep 60curl -s -u elastic:$(cat secrets/elastic.txt) http://localhost:9200/_cluster/health?pretty | grep -E 'status|number_of_nodes'  "status" : "green",  "number_of_nodes" : 1,

One node and status: green is normal for an internal IT environment. Kibana needs its own service account: docker compose exec elasticsearch bin/elasticsearch-reset-password -u kibana_system, and you put it in Kibana's ELASTICSEARCH_PASSWORD variable.

2. The Logstash pipeline

pipeline/syslog.conf
input {  beats { port => 5044 }}filter {  if [event][dataset] == "system.syslog" {    grok {      match => { "message" => "%{SYSLOGTIMESTAMP:ts} %{SYSLOGHOST:host.name} %{DATA:process.name}(?:\[%{POSINT:process.pid}\])?: %{GREEDYDATA:log.message}" }    }    date { match => ["ts", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss"] }    mutate { remove_field => ["ts"] }  }  if "Permission denied" in [message] or "Failed password" in [message] {    mutate { add_tag => ["security"] }  }}output {  elasticsearch {    hosts => ["http://elasticsearch:9200"]    user => "elastic"    password => "${ELASTIC_PASSWORD}"    index => "logs-linux-%{+YYYY.MM.dd}"  }}

The grok turns a line of text into fields (host.name, process.name, log.message): without it you could only search free text. The security tag is the first example of something you will later use in an alert. Check the syntax before starting: docker compose exec logstash bin/logstash --config.test_and_exit -f /usr/share/logstash/pipeline/syslog.conf.

3. Filebeat on the servers

bash · srv-web-01
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.15.3-x86_64.rpm && sudo rpm -vi filebeat-8.15.3-x86_64.rpmsudo filebeat modules enable systemsudo sed -i 's|#output.logstash:|output.logstash:|; s|#hosts: \["localhost:5044"\]|hosts: ["srv-tools-01.contoso.local:5044"]|' /etc/filebeat/filebeat.ymlsudo sed -i 's|^output.elasticsearch:|#output.elasticsearch:|; s|^  hosts: \["localhost:9200"\]|#  hosts: ["localhost:9200"]|' /etc/filebeat/filebeat.ymlsudo filebeat test outputlogstash: srv-tools-01.contoso.local:5044...  connection...    parse host... OK    dns lookup... OK    TCP... OK  talk to server... OKsudo systemctl enable --now filebeat

filebeat test output is the test you run before opening a ticket with the network team: if TCP... OK fails, it is the firewall; if talk to server fails, it is Logstash. With Ansible (previous guide), this step is a ten-line playbook for the whole fleet.

4. Searching in Kibana

Kibana · Discover
AnalyticsDiscoverlogs-linux-*
RangeLast 24 hours
KQLhost.name : "srv-web-01" and tags : "security"
Results37 hits
Oct 22, 2026 07:58:12 srv-web-01 sshd[2231] Failed password for invalid user admin from 10.20.4.77 port 51022 ssh2
Oct 22, 2026 07:58:14 srv-web-01 sshd[2231] Failed password for invalid user admin from 10.20.4.77 port 51030 ssh2
Oct 22, 2026 08:02:11 srv-web-01 httpd[1842] (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8081
Add filter
Save
Share

For Kibana to see the indices you need a Data view (Stack Management → Data views → Create, pattern logs-linux-*, time field @timestamp). From there: the same search across every server at once, the histogram at the top tells you what time the problem started, and Visualize on process.name shows which service produces the most errors.

5. Retention and disk

bash · srv-tools-01
curl -s -u elastic:$(cat secrets/elastic.txt) -X PUT http://localhost:9200/_ilm/policy/logs-30d -H 'Content-Type: application/json' -d '{"policy":{"phases":{"hot":{"actions":{}},"delete":{"min_age":"30d","actions":{"delete":{}}}}}}'{"acknowledged":true}curl -s -u elastic:$(cat secrets/elastic.txt) -X PUT http://localhost:9200/_index_template/logs-linux -H 'Content-Type: application/json' -d '{"index_patterns":["logs-linux-*"],"template":{"settings":{"index.lifecycle.name":"logs-30d","number_of_replicas":0}}}'{"acknowledged":true}curl -s -u elastic:$(cat secrets/elastic.txt) 'http://localhost:9200/_cat/indices/logs-*?v&h=index,docs.count,store.size&s=index' | tail -3logs-linux-2026.10.20   412033   211.4mblogs-linux-2026.10.21   398870   204.9mblogs-linux-2026.10.22    88121    46.1mb

With a single node, number_of_replicas: 0 stops the cluster from being permanently yellow. Around 200 MB a day for twenty servers is the order of magnitude; look at _cat/indices after a month to fit the retention to the disk you have.

Next steps

  • Windows: the agent is Winlogbeat with the same output.logstash; security events (4625, 4740) arrive with fields already parsed.
  • An alert in Kibana (Alerts and Insights → Rules) on tags: security with more than 20 events in 5 minutes, sent to the team channel.
  • If volume grows, the option is Elastic Agent + Fleet instead of standalone Beats, or moving the dashboards to Grafana (next guide) with Elasticsearch as a data source.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →