Skip to content

Instantly share code, notes, and snippets.

@linuxmalaysia
Created April 1, 2025 04:05
Show Gist options
  • Save linuxmalaysia/3c79011ceeca38e434b7e51da3fa63b8 to your computer and use it in GitHub Desktop.
Save linuxmalaysia/3c79011ceeca38e434b7e51da3fa63b8 to your computer and use it in GitHub Desktop.
Script to set up Elasticsearch 8.17.4 using Podman with the hardened Wolfi image, based on the official Docker documentation.
#!/bin/bash
# Script to set up Elasticsearch 8.17.4 using Podman with the hardened Wolfi image, based on the official Docker documentation.
# Note: Using Wolfi images might have specific kernel or dependency requirements.
# https://www.elastic.co/guide/en/elasticsearch/reference/8.17/docker.html
# GNU GENERAL PUBLIC LICENSE Version 3
# Harisfazillah Jamel and Google Gemini
# 31 Mac 2025
# https://github.com/HarisfazillahJamel/podman-elastic-stack
# --- Script Description ---
# This script automates the process of setting up Elasticsearch 8.17.4
# using Podman, a containerization tool similar to Docker. It uses
# a hardened Wolfi image, which is designed with security in mind.
# The script also handles tasks like retrieving the Elasticsearch
# password and SSL certificates, making the setup process easier.
# --- Key Technologies ---
# * Podman: A containerization engine (like Docker, but rootless)
# * Elasticsearch: A search and analytics engine
# * Wolfi: A Linux distribution designed for security and small size
set -e
# The set -e command is crucial. It tells the script to exit
# immediately if any command fails. This prevents the script from
# continuing and potentially causing more errors if an earlier step
# didn't work correctly.
# --- Determine Script's Directory ---
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
# This line figures out the directory where the script itself is located.
# * $0: This variable holds the name of the script.
# * realpath "$0": This expands the script name to its full,
# absolute path (e.g., /home/user/myscript.sh).
# * dirname "...": This extracts the directory part of the path
# (e.g., /home/user).
# The result is stored in the SCRIPT_DIR variable.
# --- Variables ---
ELK_VERSION="8.17.4"
ELK_BASE_DIR="${SCRIPT_DIR}" # Base directory is where the script is located
ELK_DIR="${ELK_BASE_DIR}/elk-wolfi"
CERT_DIR="${ELK_DIR}/certs"
# Using hardened Wolfi image. Wolfi is a Linux distribution.
ELASTICSEARCH_IMAGE="docker.elastic.co/elasticsearch/elasticsearch-wolfi:${ELK_VERSION}"
KIBANA_IMAGE="docker.elastic.co/kibana/kibana:${ELK_VERSION}" #Kibana is a data visualization tool for Elasticsearch
NETWORK_NAME="elastic"
TEMP_CREDENTIALS_FILE="${ELK_DIR}/temp_credentials.txt"
# --- Helper Functions ---
info() {
echo "--- $1 ---"
}
# This defines a simple function called 'info'. It takes one argument
# ($1) and prints it to the console, surrounded by "---" for emphasis.
# Example: info "Starting Elasticsearch" will print:
# --- Starting Elasticsearch ---
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# This function checks if a command is installed on the system.
# * command -v "$1": This tries to find the command specified by
# the argument $1. If the command is found, it prints its path
# to standard output.
# * >/dev/null 2>&1: This redirects both standard output (where
# the command's path would be printed) and standard error to
# /dev/null, which is a special file that discards any data written
# to it. This is done so that the command's output doesn't clutter
# the console.
# The function returns true (0) if the command is found, and false (1)
# if it's not.
# --- Step 1: Install Podman and Podman Compose ---
info "Step 1: Install Podman and Podman Compose"
if ! command_exists podman; then
info "Podman not found. Installing..."
sudo dnf update -y
sudo dnf install epel-release -y
sudo dnf install podman -y
else
info "Podman is already installed."
fi
# This block checks if Podman is installed. If not, it attempts to
# install it using 'dnf', which is the package manager for Fedora
# and related distributions (like Red Hat Enterprise Linux and CentOS).
# * sudo: Executes the following commands with administrator
# privileges.
# * dnf update -y: Updates the system's package lists. The -y
# option automatically answers "yes" to any prompts.
# * epel-release: Provides access to extra packages.
# * dnf install podman -y: Installs Podman.
if ! command_exists podman-compose; then
info "podman-compose not found. Installing from EPEL repository..."
sudo dnf install epel-release -y # Ensure EPEL is enabled
sudo dnf install podman-compose -y
else
info "podman-compose is already installed."
fi
# This block does the same as the previous one, but for
# 'podman-compose', which is a tool for defining and running
# multi-container applications with Podman.
# --- Step 3: Pull Elasticsearch Docker Image (Wolfi hardened image) ---
info "Step 3: Pull Elasticsearch Docker Image (Wolfi hardened image)"
# Note: Using Wolfi images might require specific kernel or dependency requirements.
podman pull "${ELASTICSEARCH_IMAGE}"
# This line uses Podman to download the Elasticsearch image
# from a container registry (docker.elastic.co). The image is
# specified by the ELASTICSEARCH_IMAGE variable, which includes
# the version (8.17.4) and indicates that it's a Wolfi-based image.
# * podman pull: The Podman command to download a container image.
# --- Step 4: Optional: Install and Verify Cosign ---
info "Step 4: Optional: Install and Verify Cosign"
if ! command_exists cosign; then
info "Cosign not found. Please install it manually if you wish to verify the image signature."
else
info "Cosign found. Verifying Elasticsearch image signature..."
wget https://artifacts.elastic.co/cosign.pub -O cosign.pub
cosign verify --key cosign.pub "${ELASTICSEARCH_IMAGE}"
rm cosign.pub
fi
# This part is about security. It checks if 'cosign' is installed.
# Cosign is a tool for verifying the digital signatures of container
# images. This ensures that the image hasn't been tampered with.
# * cosign: A tool to verify software signatures.
# * wget: Downloads a file from the internet.
# * cosign verify: Verifies the signature of the Elasticsearch
# image using the public key downloaded from Elastic.
# * rm: Removes a file.
# --- Step 5: Start Elasticsearch Container using podman-compose ---
info "Step 5: Start Elasticsearch Container using podman-compose"
# We will create a podman-compose.yml file here
mkdir -p "${ELK_DIR}"
cat > "${ELK_DIR}/podman-compose.yml" <<EOL
version: '3.8'
services:
elasticsearch:
image: ${ELASTICSEARCH_IMAGE}
container_name: es01
networks:
- ${NETWORK_NAME}
ports:
- "9200:9200"
environment:
- discovery.type=single-node
mem_limit: 1GB
networks:
${NETWORK_NAME}:
driver: bridge
EOL
# This is a key part of the script. It creates a 'podman-compose.yml'
# file, which is a configuration file that tells Podman Compose how
# to set up the Elasticsearch container.
# * mkdir -p "${ELK_DIR}": Creates the directory
# "${ELK_DIR}" if it doesn't exist. The -p option creates any
# parent directories that are also needed.
# * cat > "${ELK_DIR}/podman-compose.yml" <<EOL ... EOL: This
# is a 'here document'. It redirects the text between the
# 'EOL' markers to the file
# "${ELK_DIR}/podman-compose.yml".
# The contents of the file:
# * version: '3.8': Specifies the version of the Compose file format.
# * services: Defines the services (containers) that will be run.
# * elasticsearch: Defines the Elasticsearch service.
# * image: ${ELASTICSEARCH_IMAGE}: Specifies the container image to use.
# * container_name: es01: Assigns a name to the container.
# * networks: Connects the container to a network.
# * ports: Maps port 9200 on the host to port 9200 in the container
# (the default port for Elasticsearch).
# * environment: Sets an environment variable
# (discovery.type=single-node) to configure Elasticsearch
# to run in single-node mode (suitable for development).
# * mem_limit: 1GB: Limits the memory.
# * networks: Defines the networks.
cd "${ELK_DIR}"
podman-compose up -d
# * cd "${ELK_DIR}": Changes the current directory to
# "${ELK_DIR}", where the 'podman-compose.yml' file is located.
# * podman-compose up -d: Starts the Elasticsearch container
# in detached mode (-d), meaning it runs in the background.
# --- Step 6: Retrieve and Store Elasticsearch Password ---
info "Step 6: Retrieve and Store Elasticsearch Password"
echo "Please wait for Elasticsearch to start..."
for i in $(seq 60 -1 1); do
echo "Waiting for Elasticsearch to start... $i seconds remaining..."
podman ps -a
sleep 1
done
# This loop waits for a maximum of 60 seconds for Elasticsearch to start.
# * seq 60 -1 1: Generates a sequence of numbers from 60 down to 1.
# * podman ps -a : show all podman containers.
# * sleep 1: Pauses execution for 1 second.
# Change to the base directory
cd "${ELK_BASE_DIR}"
# Check if ELK_DIR exists
if [ -d "${ELK_DIR}" ]; then
info "Directory '${ELK_DIR}' already exists. Changing into it."
cd "${ELK_DIR}"
else
info "Directory '${ELK_DIR}' does not exist. Creating it."
mkdir -p "${ELK_DIR}"
cd "${ELK_DIR}"
fi
echo "--- Step 6: Retrieve and Store Elasticsearch Password ---" > "${TEMP_CREDENTIALS_FILE}"
date >> "${TEMP_CREDENTIALS_FILE}"
info "Resetting and retrieving elastic user password..."
PASSWORD_OUTPUT=$(podman exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -a -f -b 2>>"${TEMP_CREDENTIALS_FILE}")
ELASTIC_PASSWORD=$(echo "$PASSWORD_OUTPUT" | grep -oP 'New value: \K.*' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -n "${ELASTIC_PASSWORD}" ]; then
echo "Elastic password set to: ${ELASTIC_PASSWORD}"
echo "Elastic password set to: ${ELASTIC_PASSWORD}" >> "${TEMP_CREDENTIALS_FILE}"
echo "Recommendation: You can store this password as an environment variable in your shell using:"
echo "ELASTIC_PASSWORD=$ELASTIC_PASSWORD"
else
echo "Error resetting elastic password. Check ${TEMP_CREDENTIALS_FILE}"
fi
# This is a critical security step. Elasticsearch generates an
# initial password for the 'elastic' user, which needs to be
# retrieved.
# * cd "${ELK_BASE_DIR}": Changes to the base directory.
# * The script checks for the existence of the directory.
# * podman exec -it es01 ...: Executes a command inside the
# running Elasticsearch container (es01).
# * -it: Allocates a pseudo-TTY and keeps STDIN open,
# allowing for interactive commands.
# * /usr/share/elasticsearch/bin/elasticsearch-reset-password:
# The Elasticsearch command-line tool to reset the password.
# * -u elastic: reset the elastic user.
# * -a: force.
# * -f: run without confirmation.
# * -b: batch mode.
# * 2>>"${TEMP_CREDENTIALS_FILE}": append any error to the temp file.
# * PASSWORD_OUTPUT=$(...): Captures the output of the
# elasticsearch-reset-password command.
# * echo "$PASSWORD_OUTPUT" | grep -oP 'New value: \K.*' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
# This uses a pipeline of commands to extract the password
# from the output:
# * echo "$PASSWORD_OUTPUT": Prints the output.
# * grep -oP 'New value: \K.*': Uses grep to find the
# "New value: " string and extract the password after it.
# * -o: Prints only the matching part.
# * -P: Enables Perl-compatible regular expressions.
# * \K: Discards what matched before.
# * .*: Matches anything after "New value: ".
# * sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' : Removes leading and trailing spaces.
# * s/^[[:space:]]*// : Remove leading spaces.
# * s/[[:space:]]*$//' : Remove trailing spaces.
# * if [ -n "${ELASTIC_PASSWORD}" ]; then ... else ... fi:
# Checks if the password was successfully extracted.
# * echo "Elastic password set to: ${ELASTIC_PASSWORD}": Prints
# the password to the console.
# * echo "Elastic password set to: ${ELASTIC_PASSWORD}" >> "${TEMP_CREDENTIALS_FILE}": Also saves the password to the file.
# * echo "Recommendation...": Prints a reminder to store the
# password securely.
# --- Step 7: Copy SSL Certificate ---
info "Step 7: Copy SSL Certificate"
if [ -d "${CERT_DIR}" ]; then
info "Cleaning up existing certificate files in '${CERT_DIR}'..."
find "${CERT_DIR}" -type f -delete
info "Existing certificate files removed."
else
info "Certificate directory '${CERT_DIR}' does not exist."
fi
mkdir -p "${CERT_DIR}"
podman cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt "${CERT_DIR}/http_ca.crt"
info "SSL certificate copied to ${CERT_DIR}/http_ca.crt"
# Elasticsearch uses SSL certificates for secure communication.
# This part of the script copies the certificate from the container
# to the host machine.
# * if [ -d "${CERT_DIR}" ]; then ... fi: Checks if the
# certificate directory exists.
# * find "${CERT_DIR}" -type f -delete: If the directory
# exists, this command deletes any regular files in it.
# This is done to ensure that any old certificates are removed.
# * mkdir -p "${CERT_DIR}": Creates the certificate directory
# if it doesn't exist.
# * podman cp es01:...: Copies the
# 'http_ca.crt' file from the Elasticsearch container to the
# host's CERT_DIR.
# --- Step 8: Make REST API Call ---
info "Step 8: Make REST API Call"
EXTRACTED_PASSWORD=$(grep "Elastic password set to:" "${TEMP_CREDENTIALS_FILE}" | sed 's/.*Elastic password set to: //' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -f "${CERT_DIR}/http_ca.crt" ]; then
CREDENTIALS="elastic:${EXTRACTED_PASSWORD}"
BASE64_CREDENTIALS=$(echo -n "$CREDENTIALS" | base64)
AUTHORIZATION_HEADER="Authorization: Basic ${BASE64_CREDENTIALS}"
info "Making REST API call using -H"
/usr/bin/curl --cacert "$CERT_DIR/http_ca.crt" -H "$AUTHORIZATION_HEADER" https://localhost:9200
info "Waiting for 5 seconds..."
sleep 5
info "Making REST API call using -u"
/usr/bin/curl --cacert "$CERT_DIR/http_ca.crt" -u "$CREDENTIALS" https://localhost:9200
else
echo "Error: http_ca.crt not found. Skipping API calls."
fi
# This part of the script tests the connection to Elasticsearch
# by making a REST API call using the 'curl' command.
# * EXTRACTED_PASSWORD=$(...): Extracts the password from the file.
# * if [ -f "${CERT_DIR}/http_ca.crt" ]; then ... fi: Checks if
# the SSL certificate file exists.
# * CREDENTIALS="elastic:${EXTRACTED_PASSWORD}": Constructs the
# username:password string.
# * BASE64_CREDENTIALS=$(echo -n "$CREDENTIALS" | base64):
# Encodes the username:password string using Base64, which is
# required for the Authorization header in HTTP.
# * echo -n: The -n option prevents a newline character.
# * AUTHORIZATION_HEADER="Authorization: Basic ${BASE64_CREDENTIALS}":
# Constructs the HTTP Authorization header.
# * curl ...: Makes an HTTP request to Elasticsearch.
# * --cacert "$CERT_DIR/http_ca.crt": Specifies the SSL
# certificate to use for verifying the server's identity.
# * -H "$AUTHORIZATION_HEADER": Includes the Authorization
# header with the Base64-encoded credentials.
# * https://localhost:9200: The URL.
# * The script makes the same curl call using the -u option.
# --- Step 9: Retrieve and Clean Kibana Enrollment Token ---
info "Retrieving Kibana enrollment token..."
KIBANA_ENROLLMENT_TOKEN=$(podman exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana 2>>"${TEMP_CREDENTIALS_FILE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -n "${KIBANA_ENROLLMENT_TOKEN}" ]; then
echo "Kibana enrollment token: ${KIBANA_ENROLLMENT_TOKEN}"
echo "Kibana enrollment token: ${KIBANA_ENROLLMENT_TOKEN}" >> "${TEMP_CREDENTIALS_FILE}"
else
echo "Error retrieving Kibana enrollment token. Check ${TEMP_CREDENTIALS_FILE}"
fi
# * podman exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana:
# * -s kibana: generate a token for Kibana.
# * The script extracts the token.
echo ""
info "Elasticsearch setup complete! You can access it at https://localhost:9200."
info "Remember to check the temporary file '${TEMP_CREDENTIALS_FILE}' for the Elasticsearch password and the Kibana enrollment token."
echo "Recommendation: You can store this password as an environment variable in your shell using:"
echo "ELASTIC_PASSWORD=$ELASTIC_PASSWORD"
echo "Kibana enrollment token: ${KIBANA_ENROLLMENT_TOKEN}"
# Prints a completion message with instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment