update elasticsearch w/ ssl & xpack support

This commit is contained in:
2026-08-07 16:23:00 +02:00
parent 871b8d1f83
commit 86d8986630
24 changed files with 1139 additions and 509 deletions
-6
View File
@@ -1,12 +1,6 @@
---
- name: Install ELK stack using Docker
hosts: all
vars:
elk_version: "9.4.4"
roles:
# TODO check for docker install before purging it. This nukes containers
# we might not want destroyed.
# Leave a file, or read the same input requirements into a apt check
# which skips purge if satisfied.
- role: roles/docker
- role: roles/elasticsearch
+237 -13
View File
@@ -1,23 +1,247 @@
# elasticsearch
# elk_stack
Play configures ELK stack using docker & is available without HTTPS. Configure container variables in `tasks/SERVICE.yml` files, environment variables for the services & Java can also be configured here.
Deploys Elasticsearch, Logstash and Kibana as a single Docker Compose stack
on a Debian host that already has Docker (and the `docker compose` plugin)
installed and running. All three services join a shared bridge network
(`elk-network` by default) so they can reach each other by container/service
name — no hardcoded IPs, no `--link`.
The following are manual steps required during setup.
## Requirements
## elastic
- Debian host with Docker Engine + Docker Compose plugin already installed
- Ansible collection: `community.docker` (`ansible-galaxy collection install community.docker`)
- Python `docker` + `requests` modules on the target (the role installs these via pip)
- Enough RAM for the JVM heaps you configure (Elasticsearch + Logstash default to ~1.5G combined; leave headroom for OS/Docker)
After creating elasticsearch container SSH into the running host and generate a new password for user `elastic` using command:
## What it does
```bash
docker exec -it elasticsearch /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
1. Verifies Docker and Docker Compose are usable.
2. Creates `{{ elk_base_dir }}` (default `/opt/elk`) with a `config/` tree.
3. Templates:
- `config/elasticsearch/elasticsearch.yml`
- `config/kibana/kibana.yml`
- `config/logstash/logstash.yml`, `pipelines.yml`, and three pipeline
configs: `pipeline/main.conf` (generic beats/tcp/udp), `pipeline/pipeline-nginx.conf`
(nginx access logs), `pipeline/pipeline-laravel.conf` (Laravel logs)
- `docker-compose.yml` — one file, three services, one shared network, a named volume for ES data
4. Brings the stack up with `docker compose up`.
5. Waits for Elasticsearch to respond, then (if security is enabled) sets the
`kibana_system` and `logstash_system` built-in user passwords via the
Elasticsearch security API, and recreates Kibana/Logstash so they pick up
working credentials.
6. Waits for Kibana's `/api/status` to come back healthy.
## Networking
All three containers run on the `elk_network_name` bridge network
(default `elk-network`, subnet `172.28.0.0/24`). Inside that network, all
inter-service traffic uses `http://` or `https://` consistently based on
`elk_tls_enabled` (default stack, TLS on):
- Elasticsearch is reachable at `https://elasticsearch:9200`
- Kibana is configured with `elasticsearch.hosts: ["https://elasticsearch:9200"]`
- Logstash's pipelines all point at `https://elasticsearch:9200`
With `elk_tls_enabled: false`, all three of the above use `http://` instead.
Host ports (`elk_elasticsearch_http_port`, `elk_kibana_http_port`,
`elk_logstash_beats_port`, `elk_logstash_nginx_beats_port`,
`elk_logstash_laravel_beats_port`, etc.) are only for reaching the stack
*from outside* — the containers themselves never use `localhost` to talk to
each other.
## Security
`elk_security_enabled: true` by default (recommended). This turns on
Elastic's built-in security (basic auth, no TLS between containers since
traffic stays on the internal Docker network). Change these before running
in anything but a scratch environment — ideally via Ansible Vault:
```yaml
elk_elastic_password: "..."
elk_kibana_system_password: "..."
elk_logstash_system_password: "..."
elk_kibana_encryption_key: "..." # 32+ random chars, e.g. `openssl rand -hex 32`
```
## kibana
Set `elk_security_enabled: false` for a quick, unauthenticated local/dev
stack (no passwords) — do not do this on anything reachable from untrusted
networks.
Create a password for `kibana_system` user:
**Note:** `elk_tls_enabled` and `elk_security_enabled` are independent
switches you can set separately, but Elasticsearch only applies TLS
(`xpack.security.http.ssl.*`) when X-Pack security itself is on. So if
`elk_tls_enabled: true` and `elk_security_enabled: false`, this role
force-enables security anyway purely to make TLS possible — `elastic`,
`kibana_system`, and `logstash_system` all get real passwords from
`elk_elastic_password`/`elk_kibana_system_password`/
`elk_logstash_system_password` in that mode too, exactly as if
`elk_security_enabled: true` had been set, so change those from their
placeholder defaults the same way you would for a normal secured stack.
Setting both `elk_tls_enabled: false` and `elk_security_enabled: false` is
the only way to get a fully open, unauthenticated, plain-HTTP stack.
```bash
export ELASTIC_PASSWORD=
export KIBANA_PASSWORD=
curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" http://elasticsearch:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}";
## Example playbook
```yaml
- hosts: elk_servers
become: true
roles:
- role: elk_stack
vars:
elk_version: "8.15.3"
elk_elasticsearch_heap_size: "2g"
elk_logstash_heap_size: "1g"
elk_elastic_password: "{{ vault_elk_elastic_password }}"
elk_kibana_system_password: "{{ vault_elk_kibana_password }}"
elk_logstash_system_password: "{{ vault_elk_logstash_password }}"
elk_kibana_encryption_key: "{{ vault_elk_kibana_enc_key }}"
```
## Customizing the Logstash pipeline
Three pipelines run side by side inside the same Logstash process, each on
its own beats port (they can't share a port):
| Pipeline | Template | Beats port var | Purpose |
|---|---|---|---|
| `main` | `main.conf.j2` | `elk_logstash_beats_port` (5044) | Generic beats input, plus raw JSON over TCP/UDP (5000) |
| `nginx_pipeline` | `pipeline-nginx.conf.j2` | `elk_logstash_nginx_beats_port` (5045) | nginx access log parsing (grok + geoip) |
| `laravel_pipeline` | `pipeline-laravel.conf.j2` | `elk_logstash_laravel_beats_port` (5046) | Laravel log ingestion |
All three ship to Elasticsearch using the same `elk_tls_enabled`/
`elk_security_enabled` logic as the rest of the stack (TLS scheme + the
`logstash_system` credentials, when applicable). To add your own
filters/inputs/outputs, either edit these templates directly, or drop an
additional `*.conf.j2` file into `templates/` and extend `pipelines.yml.j2`
(with a new `pipeline.id`/`path.config` entry and a beats port that doesn't
collide with the ones above) and `tasks/main.yml` (a matching `template:`
task) to deploy it.
## Idempotency notes
- Re-running the role only recreates containers whose config actually
changed (via handlers), plus a compose reconciliation pass.
- Setting the `kibana_system`/`logstash_system` passwords is safe to repeat;
Elasticsearch just resets them to the same value each time and the role
only recreates those two containers when the API call reports a change.
## TLS (public certs for a private-IP node)
`elk_tls_enabled: true` (default) gets Elasticsearch, Kibana, and Logstash a
**real, publicly-trusted certificate** for a DNS name that happens to point
at a private IP (e.g. `logs-eu.domain.com -> 10.0.99.5`), using the ACME
**DNS-01** challenge against Cloudflare. This works even though the host
itself is unreachable from the internet, because DNS-01 only requires the
ability to create a `_acme-challenge` TXT record — no inbound HTTP/HTTPS
access to the node is needed at all.
### Why DNS-01, and why one shared cert
- Public CAs don't issue certificates for bare IP SANs (especially not
RFC1918/private addresses) — the workaround is a cert for a **DNS name**
that resolves to the private IP, with clients connecting via that name.
- Since all three services live on the same node behind the same DNS name,
they share **one cert** rather than one per service. Elastic explicitly
supports (and this is a common pattern) using [a single shared HTTP CA
across Elasticsearch, Kibana, and other stack components](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/tutorial-self-managed-secure)
— separate CAs/certs per component exist to isolate trust domains, which
isn't a goal here since everything is already inside the same trust
boundary (one box, one operator, one Docker network).
- Because the cert comes from a **public** CA (Let's Encrypt, via lego's
default ACME server), Kibana and Logstash don't need a custom CA
certificate distributed to them to trust Elasticsearch's cert — their
default trust stores already trust Let's Encrypt's root. This removes an
entire category of "distribute the CA cert everywhere" complexity that
self-signed/internal-CA setups require.
### What this role does
1. Expects `elk_cloudflare_token_env_file` (default
`/etc/lego/cloudflare-token.env`) to **already exist** on the box,
containing `CF_DNS_API_TOKEN=...`, root-owned, mode `0600`. This role
only *reads* that file — it never receives, logs, or stores the token
itself. See "IaC responsibilities" below for where this file comes from.
2. Installs the [`lego`](https://github.com/go-acme/lego) ACME client (a
single static Go binary — no Python/snap dependencies, easy to pin an
exact version).
3. On first run, issues a certificate for `elk_tls_domain` via `lego ... run`,
landing at `{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.{crt,key,issuer.crt}`.
4. Mounts that directory read-only into all three containers and wires
`xpack.security.http.ssl.*` (Elasticsearch), `server.ssl.*` (Kibana), and
`ssl_enabled` (Logstash's elasticsearch output) to point at it.
5. Installs a `lego-renew.service` + `lego-renew.timer` systemd pair that
runs daily, checks expiry, and **only** re-issues (and only then restarts
the three containers, via lego's `--run-hook`) when the cert is within
`elk_tls_renew_days` of expiring. No-op checks never restart anything.
### IaC responsibilities (Pulumi, not this role)
This role deliberately does **not** manage Cloudflare DNS records, zones, or
API tokens — that's Pulumi's job, since it already holds the Cloudflare
credentials and provisions the box. The pseudocode below sketches what
Pulumi needs to set up per node:
```python
# Pulumi pseudocode - see conversation history / infra repo for the real version
apex_zone = cloudflare.get_zone(name="domain.com")
# A record already exists in this setup: logs-eu.domain.com -> 10.0.99.x
# (proxied=False - a private IP can't sit behind Cloudflare's proxy/edge)
# Scoped API token: Zone.DNS.Edit, restricted to the apex zone's ID (or a
# delegated child zone, e.g. logs-eu.domain.com as its own zone, for harder
# isolation). This is the tightest scope Cloudflare's token model supports -
# there's no way to restrict a token to a single record name/pattern.
acme_dns_token = cloudflare.ApiToken(
"logs-eu-acme-dns-token",
policies=[{
"permission_groups": [dns_edit_permission_group],
"resources": {f"com.cloudflare.api.account.zone.{apex_zone.id}": "*"},
}],
# optional: condition.request_ip.in_ to pin the token to the node's
# egress IP, and/or expires_on for periodic rotation via Pulumi re-runs
)
# Hand the token to the VM via cloud-init, written root-only - Pulumi never
# passes this to Ansible as a variable.
cloud_init_write_files = [{
"path": "/etc/lego/cloudflare-token.env",
"owner": "root:root",
"permissions": "0600",
"content": f"CF_DNS_API_TOKEN={acme_dns_token.value}\n",
}]
```
For stronger isolation than a single shared zone token, NS-delegate
`logs-eu.domain.com` (and `logs-us.domain.com`, etc.) to their own
Cloudflare zones, and scope each region's token to only its own zone ID —
that way a compromised `logs-eu` node's token can't touch `logs-us` or the
apex domain's records at all.
### Key variables
| Variable | Purpose |
|---|---|
| `elk_tls_enabled` | Master switch; `false` reverts to plain HTTP |
| `elk_tls_domain` | The DNS name (e.g. `logs-eu.domain.com`) the cert covers and clients must connect via |
| `elk_tls_acme_email` | Contact address registered with the ACME account |
| `elk_tls_acme_server` | ACME endpoint — point at Let's Encrypt **staging** while testing to avoid production rate limits |
| `elk_cloudflare_token_env_file` | Path to the pre-existing, cloud-init-provisioned token file |
| `elk_tls_renew_days` | Renew when fewer than this many days remain before expiry |
| `elk_lego_version` | Pinned lego release to install |
### Operational notes
- **Testing**: set `elk_tls_acme_server` to
`https://acme-staging-v02.api.letsencrypt.org/directory` first — staging
certs aren't publicly trusted but validate the whole DNS-01/Cloudflare
flow without touching Let's Encrypt's production rate limits.
- **Downtime on renewal**: renewal restarts all three containers via
`docker compose restart`, which is brief (~10-20s) but not zero-downtime.
Acceptable for a single-node stack; a zero-downtime rolling reload would
require a multi-node Elasticsearch cluster, which is out of scope here.
- **Clients must use the DNS name, not the raw IP** — connecting to
`https://10.0.99.5:9200` directly will fail certificate hostname
verification even though the cert is otherwise valid, since the cert's
SAN is `logs-eu.domain.com`, not the IP.
+135
View File
@@ -0,0 +1,135 @@
---
# ---------------------------------------------------------------------------
# Environment variable overrides
# ---------------------------------------------------------------------------
# Every setting below can also be supplied as an env var on the CONTROL node
# (i.e. wherever `ansible-playbook` runs - a CI job, your laptop, etc.), not
# the remote target host. `lookup('env', ...)` reads from the control node's
# process environment at template-render time, which is exactly what you
# want for injecting CI/CD secrets (e.g. GitHub Actions `env:`/`secrets:`,
# a `.env` file sourced before the playbook runs) without touching inventory
# or vault files.
#
# Precedence, highest first: `-e` / extra vars on the CLI > host/group_vars
# in inventory > playbook `vars:` > these defaults (env var, if set, else
# the literal fallback shown). So an env var set here is easy to override
# per-host or per-run if needed - it's just the last thing consulted.
#
# Booleans: env vars are always strings, so `elk_tls_enabled`/
# `elk_security_enabled` run through `| bool` to turn "true"/"false"/"1"/"0"/
# "yes"/"no" (case-insensitive) into real Ansible booleans - a raw string
# "false" would otherwise be truthy in Jinja `{% if %}` conditionals.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# General
# ---------------------------------------------------------------------------
elk_version: "{{ lookup('env', 'ELK_VERSION') | default('8.15.3', true) }}"
elk_base_dir: /opt/elk
elk_compose_project_name: elk
elk_network_name: elk-network
elk_network_subnet: "172.28.0.0/24"
# Set true only after you've configured elk_elastic_password etc. for real use.
# When false, security is disabled for a quick local/dev stack.
elk_security_enabled: "{{ lookup('env', 'ELK_SECURITY_ENABLED') | default('true', true) | bool }}"
# Change these in production (e.g. via vault, or ELK_ELASTIC_PASSWORD etc.
# from CI secrets). These fallbacks are intentionally obvious placeholders
# so nobody ships them by accident.
elk_elastic_password: "{{ lookup('env', 'ELK_ELASTIC_PASSWORD') | default('ChangeMe_Elastic_123!', true) }}"
elk_kibana_system_password: "{{ lookup('env', 'ELK_KIBANA_SYSTEM_PASSWORD') | default('ChangeMe_Kibana_123!', true) }}"
elk_logstash_system_password: "{{ lookup('env', 'ELK_LOGSTASH_SYSTEM_PASSWORD') | default('ChangeMe_Logstash_123!', true) }}"
# A pre-shared 32+ char key used to encrypt the Kibana saved-objects store.
elk_kibana_encryption_key: "{{ lookup('env', 'ELK_KIBANA_ENCRYPTION_KEY') | default('ChangeMe_32Char_Kibana_EncKey!!!', true) }}"
# ---------------------------------------------------------------------------
# Elasticsearch
# ---------------------------------------------------------------------------
elk_elasticsearch_container_name: elasticsearch
elk_elasticsearch_http_port: 9200
elk_elasticsearch_transport_port: 9300
elk_elasticsearch_heap_size: "1g"
elk_elasticsearch_cluster_name: "elk-retailor"
elk_elasticsearch_node_name: "{{ lookup('env', 'ELK_ELASTIC_NODE_NAME') | default('es01', true) }}"
elk_elasticsearch_data_volume: elk_esdata
# ---------------------------------------------------------------------------
# Kibana
# ---------------------------------------------------------------------------
elk_kibana_container_name: kibana
elk_kibana_http_port: 5601
# ---------------------------------------------------------------------------
# Logstash
# ---------------------------------------------------------------------------
elk_logstash_container_name: logstash
elk_logstash_heap_size: "512m"
elk_logstash_tcp_port: 5000
elk_logstash_udp_port: 5000
elk_logstash_http_port: 9600
# Each pipeline that has its own beats input needs its own port - they all
# run inside the same Logstash process, so two pipelines can't bind the same
# port. main.conf (generic beats/tcp/udp) and the nginx/laravel example
# pipelines are each wired to a distinct one below.
elk_logstash_beats_port: 5044
elk_logstash_nginx_beats_port: 5045
elk_logstash_laravel_beats_port: 5046
# ---------------------------------------------------------------------------
# Docker image repository (override for an internal mirror/proxy)
# ---------------------------------------------------------------------------
elk_docker_image_repo: "docker.elastic.co"
# ---------------------------------------------------------------------------
# TLS via lego (ACME DNS-01 against Cloudflare)
# ---------------------------------------------------------------------------
# Master switch. When false, the stack runs plain HTTP as before (nothing
# below this point applies).
elk_tls_enabled: "{{ lookup('env', 'ELK_TLS_ENABLED') | default('true', true) | bool }}"
# The public DNS name this node is reachable at, e.g. "logs.domain.com".
# An A record for this name -> the node's private IP is assumed to already
# exist (managed by IaC, not this role). This name is what ES/Kibana/Logstash
# put in their certs and what clients must connect to (not the raw IP) for
# TLS verification to succeed.
elk_tls_domain: "{{ lookup('env', 'ELK_TLS_DOMAIN') | default('logs.domain.com', true) }}"
# Contact email lego registers with the ACME account (Let's Encrypt notifies
# this address about certificate/account issues, not renewal reminders,
# since renewal is automated).
elk_tls_acme_email: "{{ lookup('env', 'ELK_TLS_ACME_EMAIL') | default('webmaster@domain.com', true) }}"
# Let's Encrypt production endpoint by default. Point this at the staging
# endpoint while testing to avoid production rate limits:
# https://acme-staging-v02.api.letsencrypt.org/directory
elk_tls_acme_server: "{{ lookup('env', 'ELK_TLS_ACME_SERVER') | default('https://acme-v02.api.letsencrypt.org/directory', true) }}"
# lego binary version/arch and install location.
elk_lego_version: "4.20.4"
elk_lego_arch: "amd64" # amd64 | arm64
elk_lego_install_dir: /usr/local/bin
elk_lego_path: /etc/lego # lego's --path (accounts + certificates live under here)
elk_lego_certs_dir: "{{ elk_lego_path }}/certificates"
# Path to the Cloudflare token env file. This is expected to already exist,
# dropped by cloud-init at provisioning time (see IaC notes) as
# root:root 0600, containing a line like:
# CF_DNS_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# This role only READS this file - it never receives or stores the token
# itself, so the Cloudflare credential never touches Ansible inventory,
# vault, logs, or the control node.
elk_cloudflare_token_env_file: "{{ lookup('env', 'ELK_CLOUDFLARE_TOKEN_ENV_FILE') | default('/etc/lego/cloudflare-token.env', true) }}"
# Threshold used on renewal checks - lego renews if the current cert has
# fewer than this many days left before expiry.
elk_tls_renew_days: 30
# Renewal check cadence via systemd timer.
elk_tls_renew_on_calendar: "22:00:00"
elk_tls_renew_randomized_delay: "30m"
+38
View File
@@ -0,0 +1,38 @@
---
- name: restart elk stack
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
state: present
recreate: always
listen: "restart elk stack"
- name: recreate elasticsearch
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
services:
- elasticsearch
state: present
recreate: always
listen: "recreate elasticsearch"
- name: recreate logstash
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
services:
- logstash
state: present
recreate: always
listen: "recreate logstash"
- name: recreate kibana
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
services:
- kibana
state: present
recreate: always
listen: "recreate kibana"
+10 -10
View File
@@ -1,16 +1,16 @@
---
galaxy_info:
author: Your Name
description: Ansible role to deploy prometheus & grafana using Docker
license: MIT
min_ansible_version: "2.9"
role_name: elk_stack
author: your-name
description: Deploys Elasticsearch, Logstash and Kibana via Docker Compose on a shared network
min_ansible_version: "2.14"
platforms:
- name: Debian
versions:
- all
galaxy_tags:
- monitoring
- cadvisor
dependencies:
- docker
- bullseye
- bookworm
dependencies: []
collections:
- community.docker
@@ -1,41 +0,0 @@
---
- name: Pull Elasticsearch Docker image
docker_image:
name: docker.elastic.co/elasticsearch/elasticsearch-wolfi:{{ elk_version }}
source: pull
- name: Create Elasticsearch configuration file directory on host
file:
path: /etc/elasticsearch
state: directory
mode: '0755'
# - name: Create Elasticsearch configuration file
# template:
# src: elasticsearch.yml.j2
# dest: /etc/elasticsearch/elasticsearch.yml
- name: Start Elasticsearch container
docker_container:
name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch-wolfi:{{ elk_version }}
state: started
restart: yes
restart_policy: unless-stopped
published_ports:
- "9200:9200"
volumes:
- /etc/elasticsearch/esdata:/usr/share/elasticsearch/data
networks:
- name: elk_network
ipv4_address: 172.19.0.2
env:
node.name: elasticsearch
cluster.name: retailor-elk
discovery.type: single-node
bootstrap.memory_lock: "true"
# limits elasticsearch to 2 GB of RAM
ES_JAVA_OPTS: "-Xms1g -Xmx2g"
# disables SSL & xpack security
xpack.security.http.ssl.enabled: "false"
-11
View File
@@ -1,11 +0,0 @@
---
- name: Create a Docker network for Elasticsearch
docker_network:
name: elk_network
state: present
driver: bridge
ipam_config:
- subnet: "172.19.0.0/16"
gateway: "172.19.0.1"
iprange: "172.19.0.0/24"
-37
View File
@@ -1,37 +0,0 @@
---
- name: Create a Docker network for Kibana
docker_network:
name: elk_network
state: present
- name: Create kibana directory on host
file:
path: /etc/kibana
state: directory
mode: "0755"
- name: Pull Kibana Docker image
docker_image:
name: docker.elastic.co/kibana/kibana:{{ elk_version }}
source: pull
# TODO rember to move CA cert from elastic to Kibana
# docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt .
# docker cp http_ca.crt kibana:/usr/share/kibana/config/certs/ca/http_ca.crt
- name: Start Kibana container
docker_container:
name: kibana
image: docker.elastic.co/kibana/kibana:{{ elk_version }}
state: started
restart: yes
restart_policy: unless-stopped
published_ports:
- "5601:5601"
env:
ELASTICSEARCH_HOSTS: "{{ env_vars.ELASTIC_HOSTS }}"
ELASTICSEARCH_USERNAME: kibana_system
ELASTICSEARCH_PASSWORD: "{{ env_vars.KIBANA_PASSWORD }}"
TELEMETRY_ENABLED: "false"
networks:
- name: elk_network
ipv4_address: 172.19.0.3
-65
View File
@@ -1,65 +0,0 @@
---
- name: Create a Docker network for Logstash
docker_network:
name: elk_network
state: present
- name: Create logstash directory on host
file:
path: /etc/logstash
state: directory
mode: "0755"
- name: Copy logstash config
copy:
src: templates/pipelines.yml.j2
dest: /etc/logstash/pipelines.yml
- name: Create logstash directory on host
file:
path: /etc/logstash/pipeline
state: directory
mode: "0755"
- name: Copy logstash input configs
copy:
src: "{{ item }}"
dest: /etc/logstash/pipeline/{{ item | basename | regex_replace('\.j2$', '') }}
loop: "{{ query('fileglob', 'templates/logstash-conf.d/*.j2') }}"
- name: Pull Logstash Docker image
docker_image:
name: docker.elastic.co/logstash/logstash:{{ elk_version }}
source: pull
# TODO rember to move CA cert from elastic to Logstash
# docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt .
# docker cp http_ca.crt logstash:/usr/share/logstash/config/certs/ca/http_ca.crt
- name: Start Logstash container
docker_container:
name: logstash
image: docker.elastic.co/logstash/logstash:{{ elk_version }}
state: started
restart: yes
restart_policy: unless-stopped
command:
- /bin/bash
- -c
- |
echo "Waiting for Elasticsearch availability";
until curl -s {{ env_vars.ELASTIC_HOSTS }} | grep -q "missing authentication credentials"; do sleep 1; done;
echo "Starting logstash";
/usr/share/logstash/bin/logstash
published_ports:
- "5044-5049:5044-5049"
volumes:
- /etc/logstash/pipelines.yml:/usr/share/logstash/config/pipelines.yml
- /etc/logstash/pipeline:/usr/share/logstash/pipeline
env:
xpack.monitoring.enabled: "false"
ELASTIC_USER: elastic
ELASTIC_PASSWORD: "{{ env_vars.ELASTIC_PASSWORD }}"
ELASTIC_HOSTS: "{{ env_vars.ELASTIC_HOSTS }}"
networks:
- name: elk_network
ipv4_address: 172.19.0.4
+211 -6
View File
@@ -1,8 +1,213 @@
---
# ensure we have variables from .env files
- include_tasks: ../roles/env/tasks/main.yml
# ---------------------------------------------------------------------------
# TLS (elk_tls_enabled) and X-Pack security (elk_security_enabled) are
# documented as independent switches, but Elasticsearch only honors
# xpack.security.http.ssl.* when xpack.security.enabled is also true. So
# whenever TLS is on, security (and therefore basic auth / ELASTIC_PASSWORD)
# is force-enabled too - elk_effective_security_enabled is the single fact
# every template and task below should use instead of elk_security_enabled
# directly, so this coupling only has to be expressed once.
# ---------------------------------------------------------------------------
- name: Derive effective security state (TLS implies security)
ansible.builtin.set_fact:
elk_effective_security_enabled: "{{ elk_security_enabled or elk_tls_enabled }}"
- include_tasks: elk-network.yml
- include_tasks: elasticsearch.yml
- include_tasks: kibana.yml
- include_tasks: logstash.yml
- name: Assert Docker is available
ansible.builtin.command: docker info
register: elk_docker_info
changed_when: false
failed_when: elk_docker_info.rc != 0
- name: Ensure Docker Compose plugin is installed
ansible.builtin.command: docker compose version
register: elk_compose_check
changed_when: false
failed_when: elk_compose_check.rc != 0
- name: Ensure we have pip installed
apt:
pkg: "python3-pip"
state: present
- name: Ensure python docker SDK is present for community.docker modules
apt:
pkg:
- python3-docker
- python3-requests
state: present
- name: Create base ELK directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ elk_base_dir }}"
- "{{ elk_base_dir }}/config"
- "{{ elk_base_dir }}/config/elasticsearch"
- "{{ elk_base_dir }}/config/kibana"
- "{{ elk_base_dir }}/config/logstash"
- "{{ elk_base_dir }}/config/logstash/pipeline"
# ---------------------------------------------------------------------------
# TLS (must happen before compose/config templating below, since those
# templates reference the cert file paths lego produces)
# ---------------------------------------------------------------------------
- name: Issue/renew TLS certificate and set up renewal automation
ansible.builtin.include_tasks: tls.yml
when: elk_tls_enabled
- name: Set connection facts used by health checks below
ansible.builtin.set_fact:
elk_scheme: "{{ 'https' if elk_tls_enabled else 'http' }}"
elk_validate_certs: "{{ not elk_tls_enabled }}"
elk_elasticsearch_client_host: "{{ elk_tls_domain if elk_tls_enabled else elk_elasticsearch_container_name }}"
# ---------------------------------------------------------------------------
# Configs
# ---------------------------------------------------------------------------
- name: Template elasticsearch.yml
ansible.builtin.template:
src: elasticsearch.yml.j2
dest: "{{ elk_base_dir }}/config/elasticsearch/elasticsearch.yml"
mode: "0644"
notify: "recreate elasticsearch"
- name: Template kibana.yml
ansible.builtin.template:
src: kibana.yml.j2
dest: "{{ elk_base_dir }}/config/kibana/kibana.yml"
mode: "0644"
notify: "recreate kibana"
- name: Template logstash.yml
ansible.builtin.template:
src: logstash.yml.j2
dest: "{{ elk_base_dir }}/config/logstash/logstash.yml"
mode: "0644"
notify: "recreate logstash"
- name: Template logstash pipelines.yml
ansible.builtin.template:
src: pipelines.yml.j2
dest: "{{ elk_base_dir }}/config/logstash/pipelines.yml"
mode: "0644"
notify: "recreate logstash"
- name: Template logstash main pipeline
ansible.builtin.template:
src: main.conf.j2
dest: "{{ elk_base_dir }}/config/logstash/pipeline/main.conf"
mode: "0644"
notify: "recreate logstash"
- name: Template logstash nginx pipeline
ansible.builtin.template:
src: pipeline-nginx.conf.j2
dest: "{{ elk_base_dir }}/config/logstash/pipeline/pipeline-nginx.conf"
mode: "0644"
notify: "recreate logstash"
- name: Template logstash laravel pipeline
ansible.builtin.template:
src: pipeline-laravel.conf.j2
dest: "{{ elk_base_dir }}/config/logstash/pipeline/pipeline-laravel.conf"
mode: "0644"
notify: "recreate logstash"
- name: Template docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ elk_base_dir }}/docker-compose.yml"
mode: "0644"
notify: "restart elk stack"
# ---------------------------------------------------------------------------
# Bring up the stack
# ---------------------------------------------------------------------------
- name: Flush handlers so config changes apply before we continue
ansible.builtin.meta: flush_handlers
- name: Start ELK stack via docker compose
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
state: present
register: elk_compose_result
- name: Wait for Elasticsearch to answer on its HTTP port
ansible.builtin.uri:
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}"
validate_certs: "{{ elk_validate_certs }}"
status_code:
- 200
- 401 # 401 is expected once security is enabled but before we authenticate
register: elk_es_wait
until: elk_es_wait.status in [200, 401]
retries: 30
delay: 10
# ---------------------------------------------------------------------------
# Security bootstrap: set built-in user passwords (idempotent)
# ---------------------------------------------------------------------------
- name: Set kibana_system password
ansible.builtin.uri:
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}/_security/user/kibana_system/_password"
validate_certs: "{{ elk_validate_certs }}"
method: POST
user: elastic
password: "{{ elk_elastic_password }}"
force_basic_auth: true
body_format: json
body:
password: "{{ elk_kibana_system_password }}"
status_code: 200
when: elk_effective_security_enabled
register: elk_set_kibana_pw
retries: 5
delay: 10
until: elk_set_kibana_pw is succeeded
- name: Set logstash_system password
ansible.builtin.uri:
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}/_security/user/logstash_system/_password"
validate_certs: "{{ elk_validate_certs }}"
method: POST
user: elastic
password: "{{ elk_elastic_password }}"
force_basic_auth: true
body_format: json
body:
password: "{{ elk_logstash_system_password }}"
status_code: 200
when: elk_effective_security_enabled
register: elk_set_logstash_pw
retries: 5
delay: 10
until: elk_set_logstash_pw is succeeded
- name: Recreate kibana and logstash if passwords were just set for the first time
community.docker.docker_compose_v2:
project_src: "{{ elk_base_dir }}"
project_name: "{{ elk_compose_project_name }}"
services:
- kibana
- logstash
state: present
recreate: always
when: >
elk_effective_security_enabled and
(elk_set_kibana_pw is changed or elk_set_logstash_pw is changed)
- name: Wait for Kibana to answer on its HTTP port
ansible.builtin.uri:
url: "{{ elk_scheme }}://localhost:{{ elk_kibana_http_port }}/api/status"
validate_certs: "{{ elk_validate_certs }}"
status_code:
- 200
register: elk_kibana_wait
until: elk_kibana_wait.status == 200
retries: 30
delay: 10
+176
View File
@@ -0,0 +1,176 @@
---
# ---------------------------------------------------------------------------
# TLS certificate lifecycle via lego (ACME DNS-01 against Cloudflare).
#
# This file only READS the Cloudflare token from elk_cloudflare_token_env_file.
# It never receives the token as an Ansible variable - the token is expected
# to already exist on disk (root:root, 0600), placed there by cloud-init at
# provisioning time. See the role README for the IaC side of this.
# ---------------------------------------------------------------------------
- name: Assert the Cloudflare token file exists
ansible.builtin.stat:
path: "{{ elk_cloudflare_token_env_file }}"
register: elk_cf_token_stat
- name: Fail early with a clear message if the token file is missing
ansible.builtin.fail:
msg: >
{{ elk_cloudflare_token_env_file }} does not exist. This role expects
cloud-init (or equivalent provisioning) to have already written a
scoped Cloudflare API token here before Ansible runs. See the IaC
notes in the role README.
when: not elk_cf_token_stat.stat.exists
- name: Verify the token file is not world/group readable
ansible.builtin.assert:
that:
- elk_cf_token_stat.stat.mode == "0600"
fail_msg: >
{{ elk_cloudflare_token_env_file }} should be mode 0600
(currently {{ elk_cf_token_stat.stat.mode }}) since it holds a
Cloudflare API credential.
success_msg: "Cloudflare token file permissions look correct."
- name: Create lego directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0700"
loop:
- "{{ elk_lego_path }}"
- "{{ elk_lego_path }}/accounts"
- name: Create lego certificates directory (traversable by container users)
ansible.builtin.file:
path: "{{ elk_lego_certs_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Check installed lego version (if any)
ansible.builtin.command: "{{ elk_lego_install_dir }}/lego --version"
register: elk_lego_installed_version
changed_when: false
failed_when: false
- name: Download and install lego binary
when: >
elk_lego_installed_version.rc != 0 or
elk_lego_version not in (elk_lego_installed_version.stdout | default(''))
block:
- name: Download lego release tarball
ansible.builtin.get_url:
url: "https://github.com/go-acme/lego/releases/download/v{{ elk_lego_version }}/lego_v{{ elk_lego_version }}_linux_{{ elk_lego_arch }}.tar.gz"
dest: "/tmp/lego_{{ elk_lego_version }}.tar.gz"
mode: "0644"
- name: Extract lego binary
ansible.builtin.unarchive:
src: "/tmp/lego_{{ elk_lego_version }}.tar.gz"
dest: /tmp
remote_src: true
include:
- lego
- name: Install lego binary
ansible.builtin.copy:
src: /tmp/lego
dest: "{{ elk_lego_install_dir }}/lego"
owner: root
group: root
mode: "0755"
remote_src: true
- name: Clean up downloaded artifacts
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "/tmp/lego_{{ elk_lego_version }}.tar.gz"
- /tmp/lego
- name: Check whether a certificate already exists for this domain
ansible.builtin.stat:
path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt"
register: elk_cert_stat
# First issuance uses `lego ... run`, which is what actually requests a new
# certificate; `renew` (used by the systemd timer below) refuses to run
# against a domain with no existing certificate.
- name: Issue the initial certificate (first run only)
ansible.builtin.shell: |
set -o pipefail
set -a
source {{ elk_cloudflare_token_env_file }}
set +a
{{ elk_lego_install_dir }}/lego \
--accept-tos \
--email "{{ elk_tls_acme_email }}" \
--server "{{ elk_tls_acme_server }}" \
--domains "{{ elk_tls_domain }}" \
--dns cloudflare \
--path "{{ elk_lego_path }}" \
run
args:
executable: /bin/bash
when: not elk_cert_stat.stat.exists
register: elk_lego_first_issue
notify: "restart elk stack"
- name: Ensure issued cert/key are readable by the containers (root-owned, group-readable)
ansible.builtin.file:
path: "{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt"
- "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.issuer.crt"
when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped
- name: Private key stays root-only-readable but group-readable for the container runtime
ansible.builtin.file:
path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.key"
owner: root
group: root
mode: "0640"
when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped
# ---------------------------------------------------------------------------
# Ongoing renewal: systemd oneshot service + daily timer. lego's `renew`
# subcommand only actually re-issues (and only then fires --run-hook) when
# the certificate is within elk_tls_renew_days of expiring, so this is safe
# to run daily without causing unnecessary container restarts.
# ---------------------------------------------------------------------------
- name: Template lego renewal systemd service
ansible.builtin.template:
src: lego-renew.service.j2
dest: /etc/systemd/system/lego-renew.service
owner: root
group: root
mode: "0644"
register: elk_lego_service_unit
- name: Template lego renewal systemd timer
ansible.builtin.template:
src: lego-renew.timer.j2
dest: /etc/systemd/system/lego-renew.timer
owner: root
group: root
mode: "0644"
register: elk_lego_timer_unit
- name: Reload systemd if units changed
ansible.builtin.systemd:
daemon_reload: true
when: elk_lego_service_unit is changed or elk_lego_timer_unit is changed
- name: Enable and start the lego renewal timer
ansible.builtin.systemd:
name: lego-renew.timer
enabled: true
state: started
@@ -0,0 +1,147 @@
# ansible_managed
version: "3.8"
networks:
{{ elk_network_name }}:
driver: bridge
ipam:
config:
- subnet: {{ elk_network_subnet }}
volumes:
{{ elk_elasticsearch_data_volume }}:
driver: local
services:
elasticsearch:
image: "{{ elk_docker_image_repo }}/elasticsearch/elasticsearch:{{ elk_version }}"
container_name: "{{ elk_elasticsearch_container_name }}"
restart: unless-stopped
environment:
- node.name={{ elk_elasticsearch_node_name }}
- cluster.name={{ elk_elasticsearch_cluster_name }}
- discovery.type=single-node
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms{{ elk_elasticsearch_heap_size }} -Xmx{{ elk_elasticsearch_heap_size }}"
# xpack.security.enabled must be true whenever TLS is on - Elasticsearch
# only honors xpack.security.http.ssl.* when security itself is
# enabled. elk_effective_security_enabled captures that (it's true if
# either elk_security_enabled or elk_tls_enabled is true). Whenever
# security ends up on for either reason, ELASTIC_PASSWORD must also be
# set - otherwise the image generates a random, unknown elastic
# password and nothing else in this stack could authenticate.
- xpack.security.enabled={{ elk_effective_security_enabled | lower }}
{% if elk_effective_security_enabled %}
- ELASTIC_PASSWORD={{ elk_elastic_password }}
{% endif %}
{% if elk_tls_enabled %}
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.certificate=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.crt
- xpack.security.http.ssl.key=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.key
- xpack.security.http.ssl.certificate_authorities=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.issuer.crt
# Single node - transport SSL stays off. Turn this on (and add a
# matching transport cert) if this ever becomes a multi-node cluster.
- xpack.security.transport.ssl.enabled=false
{% else %}
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
{% endif %}
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- {{ elk_elasticsearch_data_volume }}:/usr/share/elasticsearch/data
- {{ elk_base_dir }}/config/elasticsearch/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro
{% if elk_tls_enabled %}
- {{ elk_lego_certs_dir }}:/usr/share/elasticsearch/config/certs:ro
{% endif %}
ports:
- "{{ elk_elasticsearch_http_port }}:9200"
- "{{ elk_elasticsearch_transport_port }}:9300"
networks:
{{ elk_network_name }}:
{% if elk_tls_enabled %}
aliases:
- "{{ elk_tls_domain }}"
{% endif %}
healthcheck:
test:
- CMD-SHELL
{% set es_scheme = 'https' if elk_tls_enabled else 'http' %}
{% set curl_tls_flag = '-k ' if elk_tls_enabled else '' %}
{# xpack.security.enabled follows elk_effective_security_enabled (see
above), so basic auth is required here under that same condition. #}
{% if elk_effective_security_enabled %}
- "curl -s {{ curl_tls_flag }}-u elastic:{{ elk_elastic_password }} {{ es_scheme }}://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"
{% else %}
- "curl -s {{ curl_tls_flag }}{{ es_scheme }}://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"
{% endif %}
interval: 10s
timeout: 10s
retries: 20
start_period: 30s
logstash:
image: "{{ elk_docker_image_repo }}/logstash/logstash:{{ elk_version }}"
container_name: "{{ elk_logstash_container_name }}"
restart: unless-stopped
environment:
- "LS_JAVA_OPTS=-Xms{{ elk_logstash_heap_size }} -Xmx{{ elk_logstash_heap_size }}"
- xpack.monitoring.enabled=false
{% if elk_effective_security_enabled %}
- ELASTIC_USER=logstash_system
- ELASTIC_PASSWORD={{ elk_logstash_system_password }}
{% endif %}
volumes:
- {{ elk_base_dir }}/config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
- {{ elk_base_dir }}/config/logstash/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
- {{ elk_base_dir }}/config/logstash/pipeline:/usr/share/logstash/pipeline:ro
{% if elk_tls_enabled %}
- {{ elk_lego_certs_dir }}:/usr/share/logstash/config/certs:ro
{% endif %}
ports:
- "{{ elk_logstash_beats_port }}:5044"
- "{{ elk_logstash_nginx_beats_port }}:5045"
- "{{ elk_logstash_laravel_beats_port }}:5046"
- "{{ elk_logstash_tcp_port }}:5000/tcp"
- "{{ elk_logstash_udp_port }}:5000/udp"
- "{{ elk_logstash_http_port }}:9600"
networks:
- {{ elk_network_name }}
depends_on:
elasticsearch:
condition: service_healthy
kibana:
image: "{{ elk_docker_image_repo }}/kibana/kibana:{{ elk_version }}"
container_name: "{{ elk_kibana_container_name }}"
restart: unless-stopped
environment:
- SERVER_NAME={{ elk_kibana_container_name }}
- ELASTICSEARCH_HOSTS={{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200
{% if elk_effective_security_enabled %}
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD={{ elk_kibana_system_password }}
- XPACK_SECURITY_ENCRYPTIONKEY={{ elk_kibana_encryption_key }}
{% endif %}
{% if elk_tls_enabled %}
- SERVER_SSL_ENABLED=true
- SERVER_SSL_CERTIFICATE=/usr/share/kibana/config/certs/{{ elk_tls_domain }}.crt
- SERVER_SSL_KEY=/usr/share/kibana/config/certs/{{ elk_tls_domain }}.key
# Public CA (Let's Encrypt) is already trusted by the container's
# system trust store, so no custom CA needs to be supplied here for
# the Elasticsearch connection.
{% endif %}
volumes:
- {{ elk_base_dir }}/config/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml:ro
{% if elk_tls_enabled %}
- {{ elk_lego_certs_dir }}:/usr/share/kibana/config/certs:ro
{% endif %}
ports:
- "{{ elk_kibana_http_port }}:5601"
networks:
- {{ elk_network_name }}
depends_on:
elasticsearch:
condition: service_healthy
@@ -0,0 +1,29 @@
# ansible_managed
cluster.name: "{{ elk_elasticsearch_cluster_name }}"
node.name: "{{ elk_elasticsearch_node_name }}"
network.host: 0.0.0.0
discovery.type: single-node
# TLS (elk_tls_enabled) is a transport-layer concern independent of X-Pack
# security (elk_security_enabled) - HOWEVER Elasticsearch only honors
# xpack.security.http.ssl.* when xpack.security.enabled is true, so security
# is force-enabled (elk_effective_security_enabled, set in tasks/main.yml)
# whenever TLS is on, even if elk_security_enabled itself is false. The
# ELASTIC_PASSWORD env var is set under the same condition (see
# docker-compose.yml.j2), so basic auth is always usable whenever this is true.
xpack.security.enabled: {{ elk_effective_security_enabled | lower }}
{% if elk_tls_enabled %}
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.certificate: certs/{{ elk_tls_domain }}.crt
xpack.security.http.ssl.key: certs/{{ elk_tls_domain }}.key
xpack.security.http.ssl.certificate_authorities: ["certs/{{ elk_tls_domain }}.issuer.crt"]
xpack.security.transport.ssl.enabled: false
{% else %}
xpack.security.http.ssl.enabled: false
xpack.security.transport.ssl.enabled: false
{% endif %}
# Reachable at {{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200 from other
# containers on the {{ elk_network_name }} docker network, and from outside
# the host at {{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_tls_domain if elk_tls_enabled else elk_elasticsearch_client_host }}:{{ elk_elasticsearch_http_port }}
@@ -1,272 +0,0 @@
###################### Filebeat Configuration Example #########################
# This file is an example configuration file highlighting only the most common
# options. The filebeat.reference.yml file from the same directory contains all the
# supported options with more comments. You can use it as a reference.
#
# You can find the full configuration reference here:
# https://www.elastic.co/guide/en/beats/filebeat/index.html
# For more available modules and options, please see the filebeat.reference.yml sample
# configuration file.
# ============================== Filebeat inputs ===============================
filebeat.inputs:
# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.
- type: log
# Change to true to enable this input configuration.
enabled: false
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /var/log/ngnix/*.log
#- c:\programdata\elasticsearch\logs\*
# Exclude lines. A list of regular expressions to match. It drops the lines that are
# matching any regular expression from the list.
#exclude_lines: ['^DBG']
# Include lines. A list of regular expressions to match. It exports the lines that are
# matching any regular expression from the list.
#include_lines: ['^ERR', '^WARN']
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
# are matching any regular expression from the list. By default, no files are dropped.
#exclude_files: ['.gz$']
# Optional additional fields. These fields can be freely picked
# to add additional information to the crawled log files for filtering
#fields:
# level: debug
# review: 1
### Multiline options
# Multiline can be used for log messages spanning multiple lines. This is common
# for Java Stack Traces or C-Line Continuation
# The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
#multiline.pattern: ^\[
# Defines if the pattern set under pattern should be negated or not. Default is false.
#multiline.negate: false
# Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
# that was (not) matched before or after or as long as a pattern is not matched based on negate.
# Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
#multiline.match: after
# filestream is an input for collecting log messages from files. It is going to replace log input in the future.
- type: filestream
# Change to true to enable this input configuration.
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /var/log/nginx/*.log
#- c:\programdata\elasticsearch\logs\*
exclude_files: ['\.gz$']
# Exclude lines. A list of regular expressions to match. It drops the lines that are
# matching any regular expression from the list.
#exclude_lines: ['^DBG']
# Include lines. A list of regular expressions to match. It exports the lines that are
# matching any regular expression from the list.
#include_lines: ['^ERR', '^WARN']
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
# are matching any regular expression from the list. By default, no files are dropped.
#prospector.scanner.exclude_files: ['.gz$']
# Optional additional fields. These fields can be freely picked
# to add additional information to the crawled log files for filtering
#fields:
# level: debug
# review: 1
# ============================== Filebeat modules ==============================
filebeat.config.modules:
# Glob pattern for configuration loading
path: ${path.config}/modules.d/*.yml
# Set to true to enable config reloading
reload.enabled: false
# Period on which files under path should be checked for changes
#reload.period: 10s
# ======================= Elasticsearch template setting =======================
setup.template.settings:
index.number_of_shards: 1
#index.codec: best_compression
#_source.enabled: false
# ================================== General ===================================
# The name of the shipper that publishes the network data. It can be used to group
# all the transactions sent by a single shipper in the web interface.
#name:
# The tags of the shipper are included in their own field with each
# transaction published.
#tags: ["service-X", "web-tier"]
# Optional fields that you can specify to add additional information to the
# output.
#fields:
# env: staging
# ================================= Dashboards =================================
# These settings control loading the sample dashboards to the Kibana index. Loading
# the dashboards is disabled by default and can be enabled either by setting the
# options here or by using the `setup` command.
#setup.dashboards.enabled: false
# The URL from where to download the dashboards archive. By default this URL
# has a value which is computed based on the Beat name and version. For released
# versions, this URL points to the dashboard archive on the artifacts.elastic.co
# website.
#setup.dashboards.url:
# =================================== Kibana ===================================
# Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API.
# This requires a Kibana endpoint configuration.
setup.kibana:
# Kibana Host
# Scheme and port can be left out and will be set to the default (http and 5601)
# In case you specify and additional path, the scheme is required: http://localhost:5601/path
# IPv6 addresses should always be defined as: https://[2001:db8::1]:5601
#host: "localhost:5601"
# Kibana Space ID
# ID of the Kibana Space into which the dashboards should be loaded. By default,
# the Default Space will be used.
#space.id:
# =============================== Elastic Cloud ================================
# These settings simplify using Filebeat with the Elastic Cloud (https://cloud.elastic.co/).
# The cloud.id setting overwrites the `output.elasticsearch.hosts` and
# `setup.kibana.host` options.
# You can find the `cloud.id` in the Elastic Cloud web UI.
#cloud.id:
# The cloud.auth setting overwrites the `output.elasticsearch.username` and
# `output.elasticsearch.password` settings. The format is `<user>:<pass>`.
#cloud.auth:
# ================================== Outputs ===================================
# Configure what output to use when sending the data collected by the beat.
# ---------------------------- Elasticsearch Output ----------------------------
# output.elasticsearch:
# Array of hosts to connect to.
# hosts: ["elastic.schleppe:9200"]
# Protocol - either `http` (default) or `https`.
# protocol: "https"
# Authentication credentials - either API key or username/password.
#api_key: "id:api_key"
#username: "elastic"
#password: "changeme"
# ------------------------------ Logstash Output -------------------------------
output.logstash:
# The Logstash hosts
hosts: ["elasticsearch:5400"]
# Optional SSL. By default is off.
# List of root certificates for HTTPS server verifications
# ssl.certificate_authorities: ["/etc/elk-certs/elk-ssl.crt"]
# Certificate for SSL client authentication
# ssl.certificate: "/etc/elk-certs/elk-ssl.crt"
# Client Certificate Key
# ssl.key: "/etc/elk-certs/elk-ssl.key"
# ================================= Processors =================================
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
- add_docker_metadata: ~
- add_kubernetes_metadata: ~
# ================================== Logging ===================================
# Sets log level. The default log level is info.
# Available log levels are: error, warning, info, debug
# logging.level: debug
# At debug level, you can selectively enable logging only for some components.
# To enable all selectors use ["*"]. Examples of other selectors are "beat",
# "publisher", "service".
#logging.selectors: ["*"]
# ============================= X-Pack Monitoring ==============================
# Filebeat can export internal metrics to a central Elasticsearch monitoring
# cluster. This requires xpack monitoring to be enabled in Elasticsearch. The
# reporting is disabled by default.
# Set to true to enable the monitoring reporter.
#monitoring.enabled: false
# Sets the UUID of the Elasticsearch cluster under which monitoring data for this
# Filebeat instance will appear in the Stack Monitoring UI. If output.elasticsearch
# is enabled, the UUID is derived from the Elasticsearch cluster referenced by output.elasticsearch.
#monitoring.cluster_uuid:
# Uncomment to send the metrics to Elasticsearch. Most settings from the
# Elasticsearch output are accepted here as well.
# Note that the settings should point to your Elasticsearch *monitoring* cluster.
# Any setting that is not set is automatically inherited from the Elasticsearch
# output configuration, so if you have the Elasticsearch output configured such
# that it is pointing to your Elasticsearch monitoring cluster, you can simply
# uncomment the following line.
#monitoring.elasticsearch:
# ============================== Instrumentation ===============================
# Instrumentation support for the filebeat.
#instrumentation:
# Set to true to enable instrumentation of filebeat.
#enabled: false
# Environment in which filebeat is running on (eg: staging, production, etc.)
#environment: ""
# APM Server hosts to report instrumentation results to.
#hosts:
# - http://localhost:8200
# API Key for the APM Server(s).
# If api_key is set then secret_token will be ignored.
#api_key:
# Secret token for the APM Server(s).
#secret_token:
# ================================= Migration ==================================
# This allows to enable 6.7 migration aliases
#migration.6_to_7.enabled: true
@@ -0,0 +1,28 @@
# ansible_managed
server.name: "{{ elk_kibana_container_name }}"
server.host: "0.0.0.0"
server.port: 5601
# Talk to Elasticsearch over the shared docker network using its service name.
elasticsearch.hosts: ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
{% if elk_effective_security_enabled %}
elasticsearch.username: "kibana_system"
elasticsearch.password: "{{ elk_kibana_system_password }}"
xpack.security.encryptionKey: "{{ elk_kibana_encryption_key }}"
{% endif %}
{% if elk_tls_enabled %}
server.ssl.enabled: true
server.ssl.certificate: /usr/share/kibana/config/certs/{{ elk_tls_domain }}.crt
server.ssl.key: /usr/share/kibana/config/certs/{{ elk_tls_domain }}.key
server.publicBaseUrl: "https://{{ elk_tls_domain }}:{{ elk_kibana_http_port }}"
# No elasticsearch.ssl.certificateAuthorities entry needed here: the cert
# chain is issued by a public CA (Let's Encrypt) which the container's
# default trust store already trusts.
{% endif %}
monitoring.ui.container.elasticsearch.enabled: true
i18n.locale: "en"
@@ -0,0 +1,28 @@
# ansible_managed
[Unit]
Description=Renew ELK TLS certificate via lego (Cloudflare DNS-01)
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=oneshot
EnvironmentFile={{ elk_cloudflare_token_env_file }}
ExecStart={{ elk_lego_install_dir }}/lego \
--accept-tos \
--email "{{ elk_tls_acme_email }}" \
--server "{{ elk_tls_acme_server }}" \
--domains "{{ elk_tls_domain }}" \
--dns cloudflare \
--path "{{ elk_lego_path }}" \
--run-hook "/usr/bin/docker compose -f {{ elk_base_dir }}/docker-compose.yml -p {{ elk_compose_project_name }} restart elasticsearch kibana logstash" \
renew --days {{ elk_tls_renew_days }}
User=root
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lego-renew
# Light hardening. NoNewPrivileges/ProtectSystem kept loose enough that
# lego can write certs under elk_lego_path and reach the docker socket
# via the docker CLI.
PrivateTmp=yes
@@ -0,0 +1,11 @@
# ansible_managed
[Unit]
Description=Daily timer for ELK TLS certificate renewal check (lego)
[Timer]
OnCalendar=*-*-* {{ elk_tls_renew_on_calendar }}
RandomizedDelaySec={{ elk_tls_renew_randomized_delay }}
Persistent=true
[Install]
WantedBy=timers.target
@@ -1,17 +0,0 @@
input {
beats {
port => 5045
}
}
filter {
}
output {
elasticsearch {
index => "laravel-logs-%{+YYYY.MM}"
hosts => "${ELASTIC_HOSTS}"
user => "elastic"
password => "${ELASTIC_PASSWORD}"
}
}
@@ -1,24 +0,0 @@
input {
beats {
port => 5044
}
}
filter {
if [pipeline_id] == "nginx" {
mutate { add_field => { "route" => "nginx_pipeline" } }
} else if [pipeline_id] == "laravel" {
mutate { add_field => { "route" => "laravel_pipeline" } }
}
}
output {
if [pipeline_id] == "nginx" {
pipeline { send_to => "nginx_pipeline" }
} else if [pipeline_id] == "laravel" {
pipeline { send_to => "laravel_pipeline" }
} else {
# Handle unknown cases
stdout { codec => rubydebug }
}
}
@@ -0,0 +1,6 @@
# ansible_managed
http.host: "0.0.0.0"
http.port: 9600
path.config: /usr/share/logstash/pipeline
xpack.monitoring.enabled: false
@@ -0,0 +1,38 @@
# ansible_managed
input {
beats {
port => 5044
}
tcp {
port => 5000
codec => json_lines
}
udp {
port => 5000
codec => json_lines
}
}
filter {
# Add filters here as needed (grok, mutate, date, etc.)
}
output {
elasticsearch {
# Reach Elasticsearch over the shared docker network using its service name.
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
{% if elk_effective_security_enabled %}
user => "logstash_system"
password => "{{ elk_logstash_system_password }}"
{% endif %}
{% if elk_tls_enabled %}
ssl_enabled => true
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
# store already, no custom CA needs to be supplied here.
{% endif %}
index => "logstash-%{+YYYY.MM.dd}"
}
# Uncomment for local debugging of pipeline output:
# stdout { codec => rubydebug }
}
@@ -0,0 +1,26 @@
# ansible_managed
input {
beats {
port => 5046
}
}
filter {
}
output {
elasticsearch {
# Reach Elasticsearch over the shared docker network using its service name.
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
{% if elk_effective_security_enabled %}
user => "logstash_system"
password => "{{ elk_logstash_system_password }}"
{% endif %}
{% if elk_tls_enabled %}
ssl_enabled => true
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
# store already, no custom CA needs to be supplied here.
{% endif %}
index => "laravel-logs-%{+YYYY.MM}"
}
}
@@ -1,6 +1,7 @@
# ansible_managed
input {
beats {
port => 5044
port => 5045
}
}
@@ -36,10 +37,17 @@ filter {
output {
elasticsearch {
# Reach Elasticsearch over the shared docker network using its service name.
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
{% if elk_effective_security_enabled %}
user => "logstash_system"
password => "{{ elk_logstash_system_password }}"
{% endif %}
{% if elk_tls_enabled %}
ssl_enabled => true
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
# store already, no custom CA needs to be supplied here.
{% endif %}
index => "weblogs-%{+YYYY.MM}"
hosts => "${ELASTIC_HOSTS}"
user => "elastic"
password => "${ELASTIC_PASSWORD}"
document_type => "nginx_logs"
}
}
@@ -1,5 +1,9 @@
# ansible_managed
- pipeline.id: main
path.config: "/usr/share/logstash/pipeline/main.conf"
- pipeline.id: nginx_pipeline
path.config: "/usr/share/logstash/pipeline/nginx_pipeline.conf"
path.config: "/usr/share/logstash/pipeline/pipeline-nginx.conf"
- pipeline.id: laravel_pipeline
path.config: "/usr/share/logstash/pipeline/laravel_pipeline.conf"
path.config: "/usr/share/logstash/pipeline/pipeline-laravel.conf"