Elasticsearch stores and searches large volumes of log data, and Logstash collects, parses and enriches logs before sending them to Elasticsearch. Together with Kibana they form the Elastic (ELK) stack. In this tutorial you will install Elasticsearch 9 and Logstash on a single Ubuntu 24.04 server from Elastic's official repository, keep the security features that Elasticsearch enables by default, and build a Logstash pipeline that parses Nginx access logs and indexes them.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with at least 8 GB of RAM and 2 vCPUs, for example a CubePath VPS. Both services run on the JVM; with 4 GB they start, but leave little room for data.
  • A non-root user with sudo privileges.
  • Nginx installed and serving some traffic, so there are entries in /var/log/nginx/access.log to index. Any other log works if you adapt the pipeline in Step 6.

You do not need to install Java: both packages include their own supported JDK.

Step 1 - Adding the Elastic repository

Install the tools needed to fetch the repository key:

sudo apt update
sudo apt install curl gpg

Download Elastic's signing key and store it in /etc/apt/keyrings, the standard location for third-party repository keys:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /etc/apt/keyrings/elasticsearch.gpg

Add the repository for the 9.x release series, restricted to that key:

echo "deb [signed-by=/etc/apt/keyrings/elasticsearch.gpg] https://artifacts.elastic.co/packages/9.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-9.x.list
sudo apt update

Confirm that apt sees the packages from Elastic:

apt-cache policy elasticsearch logstash | grep -E '^[a-z]|Candidate'
elasticsearch:
  Candidate: 9.1.4
logstash:
  Candidate: 1:9.1.4-1

Your version numbers will be newer; what matters is that both packages have a candidate. Elasticsearch and Logstash must use the same major version.

Step 2 - Installing Elasticsearch

Install the package:

sudo apt install elasticsearch

On installation, Elasticsearch configures its security automatically: it generates TLS certificates for the HTTP and transport layers, enables authentication and prints a password for the elastic superuser. Look for this block in the output and save the password somewhere safe:

--------------------------- Security autoconfiguration information ------------------------------

Authentication and authorization are enabled.
TLS for the transport and HTTP layers is enabled and configured.

The generated password for the elastic built-in superuser is : your_generated_password
...

If you lose it, you can set a new one at any time:

sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -i

By default, Elasticsearch only listens on localhost, which is what you want for a single-server setup: Logstash connects locally and nothing is exposed to the internet. Leave network.host in /etc/elasticsearch/elasticsearch.yml unset.

Step 3 - Setting the JVM heap size (optional)

Elasticsearch sizes its heap automatically based on the total RAM, assuming it has the machine to itself. Since Logstash runs on the same server, cap the heap explicitly. On an 8 GB server, 3 GB is a reasonable value. Create an options file:

sudo nano /etc/elasticsearch/jvm.options.d/heap.options
-Xms3g
-Xmx3g

Always set the minimum and maximum to the same value. Logstash uses a 1 GB heap by default (/etc/logstash/jvm.options), which is enough for this guide.

Step 4 - Starting and testing Elasticsearch

Enable and start the service. The first start can take up to a minute:

sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch

Copy the CA certificate that signed the HTTP certificate to your home directory, so you can use curl against the API without sudo:

sudo cp /etc/elasticsearch/certs/http_ca.crt ~/http_ca.crt
sudo chown "$USER": ~/http_ca.crt

Query the cluster with the elastic user. curl prompts for the password:

curl --cacert ~/http_ca.crt -u elastic https://localhost:9200
{
  "name" : "your-hostname",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "9.1.4",
    ...
  },
  "tagline" : "You Know, for Search"
}

A response with the version number confirms that Elasticsearch is running, TLS works, and authentication is enforced. If you get Connection refused, the service is still starting or failed; check sudo journalctl -u elasticsearch -n 50.

A single-node cluster cannot place replica shards, so new indices would stay in yellow health. Create an index template that disables replicas for the Nginx indices you will create:

curl --cacert ~/http_ca.crt -u elastic -X PUT "https://localhost:9200/_index_template/nginx-access" \
  -H 'Content-Type: application/json' -d '
{
  "index_patterns": ["nginx-access-*"],
  "template": {
    "settings": { "number_of_replicas": 0 }
  }
}'
{"acknowledged":true}

Step 5 - Creating a user for Logstash

Logstash should not use the elastic superuser. Create a role that can only write to the nginx-access-* indices, and a user with that role. Replace your_logstash_password with a strong password:

curl --cacert ~/http_ca.crt -u elastic -X POST "https://localhost:9200/_security/role/logstash_writer" \
  -H 'Content-Type: application/json' -d '
{
  "cluster": ["monitor", "manage_index_templates", "manage_ilm"],
  "indices": [
    {
      "names": ["nginx-access-*"],
      "privileges": ["write", "create", "create_index", "manage", "manage_ilm"]
    }
  ]
}'
curl --cacert ~/http_ca.crt -u elastic -X POST "https://localhost:9200/_security/user/logstash_internal" \
  -H 'Content-Type: application/json' -d '
{
  "password": "your_logstash_password",
  "roles": ["logstash_writer"],
  "full_name": "Logstash writer"
}'

Both calls return {"role":{"created":true}} and {"created":true} respectively. Verify that the new user can authenticate:

curl --cacert ~/http_ca.crt -u logstash_internal "https://localhost:9200/_security/_authenticate?pretty"

The response lists "username" : "logstash_internal" and the logstash_writer role.

Step 6 - Installing Logstash and building the pipeline

Install Logstash from the same repository:

sudo apt install logstash

Logstash needs the Elasticsearch CA certificate to verify the TLS connection. Copy it into the Logstash configuration directory:

sudo mkdir -p /etc/logstash/certs
sudo cp /etc/elasticsearch/certs/http_ca.crt /etc/logstash/certs/
sudo chmod 0644 /etc/logstash/certs/http_ca.crt

Store the logstash_internal password in the Logstash keystore instead of writing it in the pipeline file. Create the keystore (answer y when asked about continuing without a keystore password) and add the password under the name ES_PWD:

sudo /usr/share/logstash/bin/logstash-keystore --path.settings /etc/logstash create
sudo /usr/share/logstash/bin/logstash-keystore --path.settings /etc/logstash add ES_PWD

The keystore is created by root, so give the logstash group read access:

sudo chown root:logstash /etc/logstash/logstash.keystore
sudo chmod 0640 /etc/logstash/logstash.keystore

Nginx logs are owned by the adm group. Add the logstash user to it so the service can read them:

sudo usermod -aG adm logstash

Now create the pipeline. Every .conf file in /etc/logstash/conf.d/ is loaded into the main pipeline:

sudo nano /etc/logstash/conf.d/nginx.conf
input {
  file {
    path => "/var/log/nginx/access.log"
    start_position => "beginning"
  }
}

filter {
  grok {
    match => { "message" => "%{COMBINEDAPACHELOG}" }
  }
  date {
    match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
    remove_field => [ "timestamp" ]
  }
}

output {
  elasticsearch {
    hosts => ["https://localhost:9200"]
    ssl_certificate_authorities => ["/etc/logstash/certs/http_ca.crt"]
    user => "logstash_internal"
    password => "${ES_PWD}"
    index => "nginx-access-%{+YYYY.MM.dd}"
  }
}

The three sections do the following:

  • input: reads access.log from the beginning the first time, then follows new lines. Logstash remembers its position between restarts.
  • filter: grok splits each line of the default Nginx combined format into structured fields (client address, method, URL, status code, bytes, user agent), and date uses the request time as the event timestamp instead of the time Logstash read the line.
  • output: sends the events over TLS to a daily index such as nginx-access-2026.09.25, authenticated as logstash_internal.

Test the configuration as the logstash user before starting the service:

sudo -u logstash /usr/share/logstash/bin/logstash --path.settings /etc/logstash --config.test_and_exit
...
Configuration OK
[INFO ][logstash.runner] Using config.test_and_exit mode. Config Validation Result: OK. Exiting Logstash

Step 7 - Starting Logstash and verifying the data

Enable and start Logstash:

sudo systemctl enable --now logstash

Logstash takes 30 to 60 seconds to start. Follow its log until the pipeline is running:

sudo journalctl -u logstash -f
[INFO ][logstash.javapipeline][main] Pipeline started {"pipeline.id"=>"main"}
[INFO ][logstash.agent] Pipelines running {:count=>1, :running_pipelines=>[:main], :non_running_pipelines=>[]}

Press Ctrl+C to stop following the log. Generate a few requests to Nginx, then list the new indices:

curl -s http://localhost/ > /dev/null
curl --cacert ~/http_ca.crt -u elastic "https://localhost:9200/_cat/indices/nginx-access-*?v"
health status index                   uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   nginx-access-2026.09.25 k3Jd0x5bQ2uYzv4fW1aZ9Q   1   0        128            0    142.3kb        142.3kb

The index is green because the template removed replicas. Look at one parsed document to confirm the fields were extracted:

curl --cacert ~/http_ca.crt -u elastic "https://localhost:9200/nginx-access-*/_search?size=1&pretty&filter_path=hits.hits._source.source,hits.hits._source.http,hits.hits._source.url"
{
  "hits" : {
    "hits" : [
      {
        "_source" : {
          "source" : { "address" : "127.0.0.1" },
          "http" : {
            "request" : { "method" : "GET" },
            "response" : { "status_code" : 200, "body" : { "bytes" : 615 } },
            "version" : "1.1"
          },
          "url" : { "original" : "/" }
        }
      }
    ]
  }
}

Field names follow the Elastic Common Schema (ECS), which Logstash uses by default, so they work out of the box with Kibana dashboards and queries.

Troubleshooting

Elasticsearch fails to start: read sudo journalctl -u elasticsearch -n 50 and /var/log/elasticsearch/elasticsearch.log. A heap larger than the available RAM, or a typo in elasticsearch.yml, are the most common causes.

Logstash logs 401 or security_exception: the password in the keystore does not match the logstash_internal user. Run the add ES_PWD command again (it asks to overwrite) and restart Logstash.

Logstash logs PKIX path building failed or certificate errors: the ssl_certificate_authorities path is wrong or not readable by the logstash user.

No documents arrive and no errors appear: the logstash user cannot read the log file. Confirm the group membership with id logstash and restart the service after adding it to adm, since group changes only apply to new processes.

Documents have a _grokparsefailure tag: the log format does not match COMBINEDAPACHELOG. This happens if you customized log_format in Nginx; adapt the grok pattern to your format.

Conclusion

You now have Elasticsearch 9 and Logstash running on Ubuntu 24.04 with TLS and authentication enabled, a least-privilege user for Logstash, and a pipeline that turns raw Nginx access logs into structured, searchable documents. Next, install Kibana from the same repository to explore the data visually, add an index lifecycle policy to delete old daily indices, and use Filebeat or Elastic Agent to ship logs from other servers to this Logstash instance.