Creating a dedicated Elasticsearch identity for Logstash output limits write access to only the index pattern the pipeline should populate, avoiding the habit of pointing ingestion at the built-in elastic superuser.
Elasticsearch authorizes bulk indexing through roles and users exposed by the /_security/ APIs. A custom role such as logstash_writer carries the cluster and index privileges, while a dedicated user such as logstash_internal authenticates the elasticsearch output plugin. The built-in logstash_system account remains a monitoring user and is not intended for pipeline output writes.
Privilege scope must match the actual target pattern and lifecycle behavior. A daily logs-* index-mode pipeline with ILM disabled in the output block only needs template, monitor, create, and write privileges. Add the lifecycle privileges only when Logstash owns the lifecycle policy, rollover alias, or managed index setup for the target.
$ curl --silent --show-error --fail \
--cacert /etc/logstash/certs/http_ca.crt \
--user elastic:password \
--header "Content-Type: application/json" \
--request PUT "https://elasticsearch.example.net:9200/_security/role/logstash_writer?pretty" \
--data '{
"cluster": ["manage_index_templates", "monitor"],
"indices": [
{
"names": ["logs-*"],
"privileges": ["write", "create", "create_index"]
}
]
}'
{
"role" : {
"created" : true
}
}
Change logs-* to the actual output index or data stream pattern. Add cluster manage_ilm plus index manage and manage_ilm only when the output manages ILM.
Publicly trusted certificates and Elastic Cloud do not need the --cacert option shown here.
$ curl --silent --show-error --fail \
--cacert /etc/logstash/certs/http_ca.crt \
--user elastic:password \
--header "Content-Type: application/json" \
--request PUT "https://elasticsearch.example.net:9200/_security/user/logstash_internal?pretty" \
--data '{
"password": "strong-password",
"roles": ["logstash_writer"],
"full_name": "Internal Logstash output user"
}'
{
"created" : true
}
Credentials passed on the command line can be exposed via shell history or process listings on multi-user systems.
$ curl --silent --show-error --fail \
--cacert /etc/logstash/certs/http_ca.crt \
--user elastic:password \
"https://elasticsearch.example.net:9200/_security/user/logstash_internal?pretty&filter_path=*.username,*.roles,*.enabled"
{
"logstash_internal" : {
"username" : "logstash_internal",
"roles" : [
"logstash_writer"
],
"enabled" : true
}
}
$ curl --silent --show-error --fail \
--cacert /etc/logstash/certs/http_ca.crt \
--user logstash_internal:strong-password \
--header "Content-Type: application/json" \
--request POST "https://elasticsearch.example.net:9200/_security/user/_has_privileges?pretty" \
--data '{
"cluster": ["manage_index_templates", "monitor"],
"index": [
{
"names": ["logs-*"],
"privileges": ["write", "create", "create_index"]
}
]
}'
{
"username" : "logstash_internal",
"has_all_requested" : true,
"cluster" : {
"manage_index_templates" : true,
"monitor" : true
},
"index" : {
"logs-*" : {
"create" : true,
"create_index" : true,
"write" : true
}
},
"application" : { }
}
The _has_privileges API lets the output user check its own assigned privileges without writing a test document into the target index pattern.
$ sudo -E /usr/share/logstash/bin/logstash-keystore --path.settings /etc/logstash add LOGSTASH_INTERNAL_PASSWORD Using bundled JDK: /usr/share/logstash/jdk Enter value for LOGSTASH_INTERNAL_PASSWORD: Added 'LOGSTASH_INTERNAL_PASSWORD' to the Logstash keystore.
Enter the same password used when creating logstash_internal. The key name must match the placeholder in the output block.
output {
elasticsearch {
hosts => ["https://elasticsearch.example.net:9200"]
ssl_enabled => true
ssl_certificate_authorities => ["/etc/logstash/certs/http_ca.crt"]
user => "logstash_internal"
password => "${LOGSTASH_INTERNAL_PASSWORD}"
ilm_enabled => false
index => "logs-%{+YYYY.MM.dd}"
}
}
The ssl_certificate_authorities path must be readable by the logstash service account when the cluster uses a private or self-signed HTTP certificate.
ilm_enabled ⇒ false keeps the explicit daily logs-YYYY.MM.dd target in effect. Remove that setting and add the matching ILM privileges only when the output should manage rollover through ILM.
$ sudo -u logstash /usr/share/logstash/bin/logstash --path.settings /etc/logstash --path.data /tmp/logstash-configtest --config.test_and_exit Using bundled JDK: /usr/share/logstash/jdk Configuration OK [2026-04-02T08:11:41,726][INFO ][logstash.runner ] Using config.test_and_exit mode. Config Validation Result: OK. Exiting Logstash
Configuration validation confirms pipeline syntax and plugin settings, not Elasticsearch credentials or index privileges.
$ sudo systemctl restart logstash
$ sudo journalctl --unit logstash --since "5 minutes ago" --no-pager
Jun 18 09:42:11 logstash-host logstash[21457]: [2026-06-18T09:42:11,351][INFO ][logstash.outputs.elasticsearch][main] Elasticsearch pool URLs updated {:changes=>{:removed=>[], :added=>[https://elasticsearch.example.net:9200/]}}
Jun 18 09:42:11 logstash-host logstash[21457]: [2026-06-18T09:42:11,884][INFO ][logstash.javapipeline ][main] Pipeline started {"pipeline.id"=>"main"}
##### snipped #####
Authentication failures typically show 401 or 403 responses, while index privilege problems usually mention missing rights to create the index, write documents, or manage ILM.
$ curl --silent --show-error --fail \
--cacert /etc/logstash/certs/http_ca.crt \
--user reader_user:reader-password \
"https://elasticsearch.example.net:9200/logs-*/_count?pretty"
{
"count" : 1,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
}
}
The dedicated Logstash output user can stay write-only. Use a separate account with read and view_index_metadata privileges when verifying indexed documents.