Created
April 1, 2025 04:05
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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