ELK Stack — Security Exercises

8 minute read

Security Exercises

These exercises cover all the security topics in this chapter. Each exercise builds on real-world scenarios and increases in difficulty.


Exercise 1: Enable Security and Set Passwords

Scenario: You have inherited an Elasticsearch 8.x cluster that was started with security disabled (xpack.security.enabled: false). Your task is to enable security, set passwords for all built-in users, and verify that unauthenticated requests are rejected.

Requirements:

  1. Enable security in elasticsearch.yml
  2. Generate TLS certificates for the transport layer (security won’t start without them)
  3. Restart Elasticsearch
  4. Set passwords for all built-in users
  5. Configure Kibana to authenticate with Elasticsearch
  6. Verify that curl without credentials returns a 401 error
  7. Verify that curl with the elastic user credentials returns cluster info
Show Solution

Step 1 — Generate transport certificates:

/usr/share/elasticsearch/bin/elasticsearch-certutil ca \
  --out /etc/elasticsearch/certs/elastic-stack-ca.p12 \
  --pass ""

/usr/share/elasticsearch/bin/elasticsearch-certutil cert \
  --ca /etc/elasticsearch/certs/elastic-stack-ca.p12 \
  --ca-pass "" \
  --out /etc/elasticsearch/certs/transport.p12 \
  --pass ""

chown elasticsearch:elasticsearch /etc/elasticsearch/certs/*.p12

Step 2 — Update elasticsearch.yml:

xpack.security.enabled: true

xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/transport.p12
xpack.security.transport.ssl.truststore.path: certs/transport.p12

Step 3 — Restart and set passwords:

sudo systemctl restart elasticsearch

/usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive

Step 4 — Configure Kibana:

# /etc/kibana/kibana.yml
elasticsearch.username: "kibana_system"
elasticsearch.password: "your-kibana-system-password"
sudo systemctl restart kibana

Step 5 — Verify:

# Should return 401
curl http://localhost:9200/

# Should return cluster info
curl -u elastic:your-elastic-password http://localhost:9200/

Exercise 2: Generate and Configure TLS Certificates

Scenario: Your security team requires that all communication — node-to-node and client-to-node — must be encrypted with TLS. You have a 3-node cluster (es-node-1, es-node-2, es-node-3) and a Kibana instance. Generate all necessary certificates and configure every component.

Requirements:

  1. Generate a CA
  2. Generate transport certificates for all nodes
  3. Generate HTTP certificates with proper SANs for each node
  4. Configure elasticsearch.yml for transport and HTTP TLS on every node
  5. Extract the CA in PEM format for Kibana
  6. Configure kibana.yml for HTTPS (both browser-to-Kibana and Kibana-to-Elasticsearch)
  7. Verify with curl --cacert that HTTPS is working
Show Solution

Step 1 — Generate CA:

/usr/share/elasticsearch/bin/elasticsearch-certutil ca \
  --out /etc/elasticsearch/certs/elastic-stack-ca.p12 \
  --pass "ca-password"

Step 2 — Generate transport certificate:

/usr/share/elasticsearch/bin/elasticsearch-certutil cert \
  --ca /etc/elasticsearch/certs/elastic-stack-ca.p12 \
  --ca-pass "ca-password" \
  --out /etc/elasticsearch/certs/transport.p12 \
  --pass ""

Copy to all nodes:

for node in es-node-2 es-node-3; do
  scp /etc/elasticsearch/certs/transport.p12 ${node}:/etc/elasticsearch/certs/
  ssh ${node} "chown elasticsearch:elasticsearch /etc/elasticsearch/certs/transport.p12"
done

Step 3 — Generate HTTP certificates:

/usr/share/elasticsearch/bin/elasticsearch-certutil http
# Follow the wizard:
#   - Use existing CA: yes
#   - CA path: /etc/elasticsearch/certs/elastic-stack-ca.p12
#   - Generate per node: yes
#   - Add hostnames and IPs for each node

Distribute http.p12 files to each node.

Step 4 — Configure elasticsearch.yml (all nodes):

xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/transport.p12
xpack.security.transport.ssl.truststore.path: certs/transport.p12

xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: certs/http.p12

Step 5 — Extract CA for Kibana:

openssl pkcs12 -in /etc/elasticsearch/certs/elastic-stack-ca.p12 \
  -clcerts -nokeys -out /etc/kibana/certs/elasticsearch-ca.pem \
  -passin pass:ca-password

Step 6 — Configure kibana.yml:

elasticsearch.hosts: ["https://es-node-1:9200"]
elasticsearch.ssl.certificateAuthorities: ["/etc/kibana/certs/elasticsearch-ca.pem"]
elasticsearch.ssl.verificationMode: full

server.ssl.enabled: true
server.ssl.certificate: /etc/kibana/certs/kibana.crt
server.ssl.key: /etc/kibana/certs/kibana.key

Step 7 — Verify:

# Restart all services
sudo systemctl restart elasticsearch
sudo systemctl restart kibana

# Test HTTPS
curl --cacert /etc/elasticsearch/certs/ca.crt \
  -u elastic:password \
  https://es-node-1:9200/_cat/nodes?v

Exercise 3: Create Custom Roles with Field-Level and Document-Level Security

Scenario: Your organization has three teams working with customer data:

  • Support team — can see all customer records but must NOT see ssn or credit_card fields
  • EU compliance team — can only see records where country is in the EU, and cannot see ssn
  • Analytics team — can only read aggregated metrics from metrics-* indices

Create the roles and users, then verify that each user sees only what they should.

Requirements:

  1. Create the support_agent role with field-level security (exclude ssn, credit_card)
  2. Create the eu_compliance role with both FLS and DLS
  3. Create the analytics_reader role limited to metrics-* indices
  4. Create one user for each role
  5. Verify each user’s access with a search request
Show Solution

Create the roles:

# Support agent role — FLS only
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role/support_agent" \
  -H "Content-Type: application/json" -d'
{
  "indices": [
    {
      "names": ["customer-data-*"],
      "privileges": ["read", "view_index_metadata"],
      "field_security": {
        "grant": ["*"],
        "except": ["ssn", "credit_card"]
      }
    }
  ],
  "applications": [
    {
      "application": "kibana-.kibana",
      "privileges": ["feature_discover.read", "feature_dashboard.read"],
      "resources": ["*"]
    }
  ]
}'

# EU compliance role — FLS + DLS
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role/eu_compliance" \
  -H "Content-Type: application/json" -d'
{
  "indices": [
    {
      "names": ["customer-data-*"],
      "privileges": ["read", "view_index_metadata"],
      "field_security": {
        "grant": ["*"],
        "except": ["ssn"]
      },
      "query": {
        "terms": {
          "country": ["Germany", "France", "Spain", "Italy", "Netherlands",
                      "Belgium", "Austria", "Sweden", "Poland", "Ireland"]
        }
      }
    }
  ]
}'

# Analytics reader role
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role/analytics_reader" \
  -H "Content-Type: application/json" -d'
{
  "cluster": ["monitor"],
  "indices": [
    {
      "names": ["metrics-*"],
      "privileges": ["read", "view_index_metadata"]
    }
  ]
}'

Create users:

curl -k -u elastic:password -X POST "https://localhost:9200/_security/user/support_user" \
  -H "Content-Type: application/json" -d'
{
  "password": "SupportPass123!",
  "roles": ["support_agent"],
  "full_name": "Support Agent"
}'

curl -k -u elastic:password -X POST "https://localhost:9200/_security/user/eu_user" \
  -H "Content-Type: application/json" -d'
{
  "password": "EUCompliance123!",
  "roles": ["eu_compliance"],
  "full_name": "EU Compliance Officer"
}'

curl -k -u elastic:password -X POST "https://localhost:9200/_security/user/analytics_user" \
  -H "Content-Type: application/json" -d'
{
  "password": "Analytics123!",
  "roles": ["analytics_reader"],
  "full_name": "Data Analyst"
}'

Verify access:

# Support user — should NOT see ssn or credit_card fields
curl -k -u support_user:SupportPass123! \
  "https://localhost:9200/customer-data-*/_search?size=1" | jq '.hits.hits[0]._source | keys'

# EU user — should only see EU records, no ssn field
curl -k -u eu_user:EUCompliance123! \
  "https://localhost:9200/customer-data-*/_search?size=5" | jq '.hits.hits[]._source.country'

# Analytics user — should only access metrics-*
curl -k -u analytics_user:Analytics123! \
  "https://localhost:9200/metrics-*/_search?size=1"

# Analytics user — should be denied access to customer-data-*
curl -k -u analytics_user:Analytics123! \
  "https://localhost:9200/customer-data-*/_search?size=1"
# Expected: 403 Forbidden

Exercise 4: API Key Management

Scenario: Your CI/CD pipeline needs to index deployment records into deployments-* indices. Create a scoped API key for the pipeline, test it, and then rotate it by creating a new key and invalidating the old one.

Requirements:

  1. Create an API key named ci-pipeline-key with write access only to deployments-*
  2. Use the API key to index a test document
  3. Verify the key cannot read from other indices
  4. Create a replacement key named ci-pipeline-key-v2
  5. Invalidate the original key
  6. Verify the old key no longer works
Show Solution

Step 1 — Create the API key:

curl -k -u elastic:password -X POST "https://localhost:9200/_security/api_key" \
  -H "Content-Type: application/json" -d'
{
  "name": "ci-pipeline-key",
  "role_descriptors": {
    "ci_writer": {
      "indices": [
        {
          "names": ["deployments-*"],
          "privileges": ["write", "create_index", "auto_configure"]
        }
      ]
    }
  }
}'

Save the id, api_key, and encoded values from the response.

Step 2 — Index a document with the API key:

curl -k -H "Authorization: ApiKey <encoded-value>" \
  -X POST "https://localhost:9200/deployments-2026.04.01/_doc" \
  -H "Content-Type: application/json" -d'
{
  "app": "payment-service",
  "version": "2.5.1",
  "environment": "production",
  "timestamp": "2026-04-01T10:00:00Z",
  "status": "success"
}'

Step 3 — Verify restricted access:

# Should fail — no read access to logs-*
curl -k -H "Authorization: ApiKey <encoded-value>" \
  "https://localhost:9200/logs-*/_search?size=1"
# Expected: 403 Forbidden

Step 4 — Create replacement key:

curl -k -u elastic:password -X POST "https://localhost:9200/_security/api_key" \
  -H "Content-Type: application/json" -d'
{
  "name": "ci-pipeline-key-v2",
  "role_descriptors": {
    "ci_writer": {
      "indices": [
        {
          "names": ["deployments-*"],
          "privileges": ["write", "create_index", "auto_configure"]
        }
      ]
    }
  }
}'

Step 5 — Invalidate the old key:

curl -k -u elastic:password -X DELETE "https://localhost:9200/_security/api_key" \
  -H "Content-Type: application/json" -d'
{
  "ids": ["<old-key-id>"]
}'

Step 6 — Verify old key is rejected:

curl -k -H "Authorization: ApiKey <old-encoded-value>" \
  "https://localhost:9200/deployments-*/_search"
# Expected: 401 Unauthorized

Exercise 5: Configure an LDAP Realm

Scenario: Your organization uses an LDAP directory. Configure Elasticsearch to authenticate users against LDAP and map LDAP groups to Elasticsearch roles. The LDAP server details:

  • URL: ldaps://ldap.corp.example.com:636
  • Bind DN: cn=es-bind,ou=services,dc=corp,dc=example,dc=com
  • User search base: ou=employees,dc=corp,dc=example,dc=com
  • Group search base: ou=groups,dc=corp,dc=example,dc=com
  • Groups to map:
    • cn=platform-admins → superuser
    • cn=developers → editor
    • cn=operations → custom ops_viewer role (read access to logs-*, metrics-*, apm-*)

Requirements:

  1. Configure the LDAP realm in elasticsearch.yml
  2. Store the bind password in the Elasticsearch keystore
  3. Create the ops_viewer custom role
  4. Create role mappings for all three groups
  5. Restart Elasticsearch and test authentication
Show Solution

Step 1 — Configure elasticsearch.yml:

xpack.security.authc.realms.ldap.corp_ldap:
  order: 2
  url: "ldaps://ldap.corp.example.com:636"
  bind.dn: "cn=es-bind,ou=services,dc=corp,dc=example,dc=com"
  user_search.base_dn: "ou=employees,dc=corp,dc=example,dc=com"
  user_search.filter: "(uid={0})"
  group_search.base_dn: "ou=groups,dc=corp,dc=example,dc=com"
  ssl.certificate_authorities: ["/etc/elasticsearch/certs/ldap-ca.crt"]
  unmapped_groups_as_roles: false
  cache.ttl: "5m"
  cache.max_users: 10000

Step 2 — Store the bind password:

/usr/share/elasticsearch/bin/elasticsearch-keystore add \
  xpack.security.authc.realms.ldap.corp_ldap.secure_bind_password

Step 3 — Create the ops_viewer role:

curl -k -u elastic:password -X POST "https://localhost:9200/_security/role/ops_viewer" \
  -H "Content-Type: application/json" -d'
{
  "cluster": ["monitor"],
  "indices": [
    {
      "names": ["logs-*", "metrics-*", "apm-*"],
      "privileges": ["read", "view_index_metadata"]
    }
  ],
  "applications": [
    {
      "application": "kibana-.kibana",
      "privileges": [
        "feature_discover.read",
        "feature_dashboard.read",
        "feature_infrastructure.read",
        "feature_logs.read",
        "feature_apm.read"
      ],
      "resources": ["*"]
    }
  ]
}'

Step 4 — Create role mappings:

# Platform admins → superuser
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role_mapping/corp_admins" \
  -H "Content-Type: application/json" -d'
{
  "enabled": true,
  "roles": ["superuser"],
  "rules": {
    "all": [
      { "field": { "realm.name": "corp_ldap" } },
      { "field": { "groups": "cn=platform-admins,ou=groups,dc=corp,dc=example,dc=com" } }
    ]
  }
}'

# Developers → editor
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role_mapping/corp_developers" \
  -H "Content-Type: application/json" -d'
{
  "enabled": true,
  "roles": ["editor"],
  "rules": {
    "all": [
      { "field": { "realm.name": "corp_ldap" } },
      { "field": { "groups": "cn=developers,ou=groups,dc=corp,dc=example,dc=com" } }
    ]
  }
}'

# Operations → ops_viewer
curl -k -u elastic:password -X POST "https://localhost:9200/_security/role_mapping/corp_operations" \
  -H "Content-Type: application/json" -d'
{
  "enabled": true,
  "roles": ["ops_viewer"],
  "rules": {
    "all": [
      { "field": { "realm.name": "corp_ldap" } },
      { "field": { "groups": "cn=operations,ou=groups,dc=corp,dc=example,dc=com" } }
    ]
  }
}'

Step 5 — Restart and test:

sudo systemctl restart elasticsearch

# Test LDAP authentication (replace with a real LDAP user)
curl -k -u ldap_user:ldap_password https://localhost:9200/_security/_authenticate

# Expected response includes realm info:
# {
#   "username": "ldap_user",
#   "roles": ["editor"],
#   "authentication_realm": {
#     "name": "corp_ldap",
#     "type": "ldap"
#   }
# }

Summary

Exercise Key Skill
1 Enabling security and setting built-in user passwords
2 TLS certificate generation and full-stack encryption
3 Custom roles with field-level and document-level security
4 API key lifecycle — create, use, rotate, invalidate
5 LDAP realm configuration and group-to-role mapping

Return to Security for the chapter overview.