ELK Stack — Security Exercises
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:
- Enable security in
elasticsearch.yml - Generate TLS certificates for the transport layer (security won’t start without them)
- Restart Elasticsearch
- Set passwords for all built-in users
- Configure Kibana to authenticate with Elasticsearch
- Verify that
curlwithout credentials returns a401error - Verify that
curlwith theelasticuser 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:
- Generate a CA
- Generate transport certificates for all nodes
- Generate HTTP certificates with proper SANs for each node
- Configure
elasticsearch.ymlfor transport and HTTP TLS on every node - Extract the CA in PEM format for Kibana
- Configure
kibana.ymlfor HTTPS (both browser-to-Kibana and Kibana-to-Elasticsearch) - Verify with
curl --cacertthat 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
ssnorcredit_cardfields - EU compliance team — can only see records where
countryis in the EU, and cannot seessn - 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:
- Create the
support_agentrole with field-level security (excludessn,credit_card) - Create the
eu_compliancerole with both FLS and DLS - Create the
analytics_readerrole limited tometrics-*indices - Create one user for each role
- 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:
- Create an API key named
ci-pipeline-keywith write access only todeployments-* - Use the API key to index a test document
- Verify the key cannot read from other indices
- Create a replacement key named
ci-pipeline-key-v2 - Invalidate the original key
- 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→superusercn=developers→editorcn=operations→ customops_viewerrole (read access tologs-*,metrics-*,apm-*)
Requirements:
- Configure the LDAP realm in
elasticsearch.yml - Store the bind password in the Elasticsearch keystore
- Create the
ops_viewercustom role - Create role mappings for all three groups
- 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.