Initial commit: EDGE Ignition gateway docker-compose build

Custom Dockerfile-based build for an Ignition Edge gateway with
pre-baked base gwbk, module registration, and admin password
provisioning, backed by MariaDB.
This commit is contained in:
2026-07-22 10:31:20 -05:00
commit 1911a4960f
15 changed files with 764 additions and 0 deletions

74
gw-build/Dockerfile Normal file
View File

@@ -0,0 +1,74 @@
# Draw from environment (see .env file) for Ignition version/tag to source from
ARG IGNITION_VERSION=${IGNITION_VERSION}
FROM inductiveautomation/ignition:${IGNITION_VERSION} as prep
# Switch to root user for base image updates
USER root
# Install some prerequisite packages
RUN apt-get update && apt-get install -y wget jq zip unzip sqlite3
ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/AWS-Injector-signed.modl"
ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_SHA256="5aa6ab27829e6dfa67026358553a3e5e5d1b8ec689e5ffaae3cdaddbc9a3c252"
ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/Azure-Injector-signed.modl"
ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_SHA256="90cee320d98fa9c1d8c0f3786586c8cd8dd256803e08ed3fcfc772acccdf3dbd"
ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/Google-Cloud-Injector-signed.modl"
ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_SHA256="72078b25fb3c6853d85a76c212fb63a8c1277707666b492f67fb91ea333775b8"
ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/MQTT-Transmission-signed.modl"
ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_SHA256="cb5d620513110c23b618989b56c5c2a67c45c36944a25d127586e8e303f0273a"
ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Transmission-signed.modl"
ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_SHA256="notused"
ARG SUPPLEMENTAL_MODULES
# Set working directory for this prep image and ensure that exits from sub-shells bubble up and report an error
WORKDIR /root
SHELL [ "/usr/bin/env", "-S", "bash", "-euo", "pipefail", "-O", "inherit_errexit", "-c" ]
# Retrieve all targeted modules and verify their integrity
COPY --chmod=0755 retrieve-modules.sh .
RUN if [ -n "${SUPPLEMENTAL_MODULES:-}" ]; then ./retrieve-modules.sh -m "${SUPPLEMENTAL_MODULES}"; fi
# Set CERTIFICATES/EULAS acceptance in gateway backup config db
COPY *.gwbk ./
COPY --chmod=0755 register-module.sh register-password.sh ./
ARG GATEWAY_ADMIN_USERNAME="admin"
ARG BASE_GWBK_NAME="base.gwbk"
RUN --mount=type=secret,id=gateway-admin-password \
unzip -q "${BASE_GWBK_NAME}" db_backup_sqlite.idb && \
shopt -s nullglob; \
for module in *.modl; do \
./register-module.sh \
-f "${module}" \
-d db_backup_sqlite.idb; \
done; \
shopt -u nullglob && \
./register-password.sh \
-u "${GATEWAY_ADMIN_USERNAME}" \
-f /run/secrets/gateway-admin-password \
-d db_backup_sqlite.idb && \
zip -q -f "${BASE_GWBK_NAME}" db_backup_sqlite.idb || \
if [[ ${ZIP_EXIT_CODE:=$?} == 12 ]]; then \
echo "No changes to internal database needed during module registration."; \
else \
echo "Unknown error (${ZIP_EXIT_CODE}) encountered during re-packaging of config db, exiting." && \
exit ${ZIP_EXIT_CODE}; \
fi
# Final Image
FROM inductiveautomation/ignition:${IGNITION_VERSION} as final
ARG BASE_GWBK_NAME="base.gwbk"
ARG IGNITION_EDITION="edge"
# Embed modules and base gwbk from prep image as well as entrypoint shim
COPY --from=prep --chown=ignition:ignition /root/*.modl ${IGNITION_INSTALL_LOCATION}/user-lib/modules/
COPY --from=prep --chown=ignition:ignition /root/${BASE_GWBK_NAME} ${IGNITION_INSTALL_LOCATION}/base.gwbk
COPY --chmod=0755 --chown=root:root docker-entrypoint-shim.sh /usr/local/bin/
# Return to ignition user
USER ignition
# Set Ignition Edition default based on build argument
ENV IGNITION_EDITION="${IGNITION_EDITION}"
# Target the entrypoint shim for any custom logic prior to gateway launch
ENTRYPOINT [ "docker-entrypoint-shim.sh" ]

BIN
gw-build/base.gwbk Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Kick off the built-in entrypoint, with an in-built restore (-r <gwbk path>) directive
exec docker-entrypoint.sh -r base.gwbk "$@"

125
gw-build/register-module.sh Executable file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
###############################################################################
# Performs auto-acceptance of EULA and import of certificates for third-party modules
###############################################################################
function main() {
if [ ! -f "${MODULE_LOCATION}" ]; then
echo ""
return 0 # Silently exit if there is no /modules path
elif [ ! -f "${DB_LOCATION}" ]; then
echo "WARNING: ${DB_FILE} not found, skipping module registration"
return 0
fi
register_module
}
###############################################################################
# Register the module with the target Config DB
###############################################################################
function register_module() {
local SQLITE3=( sqlite3 "${DB_LOCATION}" )
# Tie into db
local keytool module_sourcepath
module_basename=$(basename "${MODULE_LOCATION}")
module_sourcepath=${MODULE_LOCATION}
keytool=$(which keytool)
echo "Processing Module: ${module_basename}"
# Populate CERTIFICATES table
local cert_info subject_name thumbprint next_certificates_id thumbprint_already_exists
cert_info=$( unzip -qq -c "${module_sourcepath}" certificates.p7b | $keytool -printcert -v | head -n 9 )
thumbprint=$( echo "${cert_info}" | grep -A 2 "Certificate fingerprints" | grep SHA1 | cut -d : -f 2- | sed -e 's/\://g' | awk '{$1=$1;print tolower($0)}' )
subject_name=$( echo "${cert_info}" | grep -m 1 -Po '^Owner: CN=\K(.+?)(?=, (OU|O|L|ST|C)=)' | sed -e 's/"//g' )
echo " Thumbprint: ${thumbprint}"
echo " Subject Name: ${subject_name}"
next_certificates_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(CERTIFICATES_ID)+1,1) FROM CERTIFICATES" )
thumbprint_already_exists=$( "${SQLITE3[@]}" "SELECT 1 FROM CERTIFICATES WHERE lower(hex(THUMBPRINT)) = '${thumbprint}'" )
if [ "${thumbprint_already_exists}" != "1" ]; then
echo " Accepting Certificate as CERTIFICATES_ID=${next_certificates_id}"
"${SQLITE3[@]}" "INSERT INTO CERTIFICATES (CERTIFICATES_ID, THUMBPRINT, SUBJECTNAME) VALUES (${next_certificates_id}, x'${thumbprint}', '${subject_name}'); UPDATE SEQUENCES SET val=${next_certificates_id} WHERE name='CERTIFICATES_SEQ'"
else
echo " Thumbprint already found in CERTIFICATES table, skipping INSERT"
fi
# Populate EULAS table
local next_eulas_id license_crc32 module_id
local -i module_id_check
next_eulas_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(EULAS_ID)+1,1) FROM EULAS" )
license_filename=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=<license>).*(?=</license)' )
license_crc32=$( unzip -qq -c "${module_sourcepath}" "${license_filename}" | gzip -c | tail -c8 | od -t u4 -N 4 -A n | cut -c 2- )
module_id=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=<id>).*(?=</id)' )
module_id_check=$( "${SQLITE3[@]}" "SELECT CASE WHEN CRC=${license_crc32} THEN -1 ELSE 1 END FROM EULAS WHERE MODULEID='${module_id}'" )
if (( module_id_check == 1 )); then
echo " Removing previous EULAS entries for MODULEID='${module_id}'"
"${SQLITE3[@]}" "DELETE FROM EULAS WHERE MODULEID='${module_id}'"
fi
if (( module_id_check >= 0 )); then
echo " Accepting License on your behalf as EULAS_ID=${next_eulas_id}"
"${SQLITE3[@]}" "INSERT INTO EULAS (EULAS_ID, MODULEID, CRC) VALUES (${next_eulas_id}, '${module_id}', ${license_crc32}); UPDATE SEQUENCES SET val=${next_eulas_id} WHERE name='EULAS_SEQ'"
else
echo " License EULA already found in EULAS table, skipping INSERT"
fi
}
###############################################################################
# Outputs to stderr
###############################################################################
function debug() {
# shellcheck disable=SC2236
if [ ! -z ${verbose+x} ]; then
>&2 echo " DEBUG: $*"
fi
}
###############################################################################
# Print usage information
###############################################################################
function usage() {
>&2 echo "Usage: $0 -f <path/to/module> -d <path/to/db>"
}
# Argument Processing
while getopts ":hvf:d:" opt; do
case "$opt" in
v)
verbose=1
;;
f)
MODULE_LOCATION="${OPTARG}"
;;
d)
DB_LOCATION="${OPTARG}"
DB_FILE=$(basename "${DB_LOCATION}")
;;
h)
usage
exit 0
;;
\?)
usage
echo "Invalid option: -${OPTARG}" >&2
exit 1
;;
:)
usage
echo "Invalid option: -${OPTARG} requires an argument" >&2
exit 1
;;
esac
done
# shift positional args based on number consumed by getopts
shift $((OPTIND-1))
if [ -z "${MODULE_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then
usage
exit 1
fi
main

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
###############################################################################
# Retrieves third-party modules and verifies their checksums
###############################################################################
function main() {
if [ -z "${SUPPLEMENTAL_MODULES}" ]; then
return 0 # Silently exit if there are no supplemental modules to target
fi
retrieve_modules
}
###############################################################################
# Download the modules
###############################################################################
function retrieve_modules() {
IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}"
for module_install_key in "${module_install_key_arr[@]}"; do
download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL"
download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256"
if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then
download_basename=$(basename "${!download_url_env}")
wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \
[[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c -
else
echo "Error finding specified module ${module_install_key} in build args, aborting..."
exit 1
fi
done
}
###############################################################################
# Outputs to stderr
###############################################################################
function debug() {
# shellcheck disable=SC2236
if [ ! -z ${verbose+x} ]; then
>&2 echo " DEBUG: $*"
fi
}
###############################################################################
# Print usage information
###############################################################################
function usage() {
>&2 echo "Usage: $0 -m \"space-separated modules list\""
>&2 echo " -m: space-separated list of module identifiers to download"
}
# Argument Processing
while getopts ":hvm:" opt; do
case "$opt" in
v)
verbose=1
;;
m)
SUPPLEMENTAL_MODULES="${OPTARG}"
;;
h)
usage
exit 0
;;
\?)
usage
echo "Invalid option: -${OPTARG}" >&2
exit 1
;;
:)
usage
echo "Invalid option: -${OPTARG} requires an argument" >&2
exit 1
;;
esac
done
# shift positional args based on number consumed by getopts
shift $((OPTIND-1))
# exit on missing required args
if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then
usage
exit 1
fi
main

148
gw-build/register-password.sh Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
# Global variables
declare -u AUTH_SALT
###############################################################################
# Update an Ignition SQLite Configuration DB with a baseline username/password
# ----------------------------------------------------------------------------
# ref: https://gist.github.com/thirdgen88/c4257bd4c47b6cc7194d1f5e7cbd6444
###############################################################################
function main() {
if [ ! -f "${SECRET_LOCATION}" ]; then
echo ""
return 0 # Silently exit if there is no secret at target path
elif [ ! -f "${DB_LOCATION}" ]; then
echo "WARNING: ${DB_FILE} not found, skipping password registration"
return 0
fi
register_password
}
###############################################################################
# Updates the target Config DB with the target username and salted pw hash
###############################################################################
function register_password() {
local SQLITE3=( sqlite3 "${DB_LOCATION}" ) password_hash password_input
echo "Registering Admin Password with Configuration DB"
# Generate Salted PW Hash
password_input="$(< "${SECRET_LOCATION}")"
if [[ "${password_input}" =~ ^\[[0-9A-F]{8,}][0-9a-f]{64}$ ]]; then
debug "Password is already hashed"
password_hash="${password_input}"
else
password_hash=$(generate_salted_hash "$(<"${SECRET_LOCATION}")")
fi
# Update INTERNALUSERTABLE
echo " Setting default admin user to USERNAME='${GATEWAY_ADMIN_USERNAME}' and PASSWORD='${password_hash}'"
"${SQLITE3[@]}" "UPDATE INTERNALUSERTABLE SET USERNAME='${GATEWAY_ADMIN_USERNAME}', PASSWORD='${password_hash}' WHERE PROFILEID=1 AND USERID=1"
}
###############################################################################
# Processes password input and translates to salted hash
###############################################################################
function generate_salted_hash() {
local auth_pwhash auth_pwsalthash auth_password password_input
password_input="${1}"
debug "auth_salt is ${AUTH_SALT}"
auth_pwhash=$(printf %s "${password_input}" | sha256sum - | cut -c -64)
debug "auth_pwhash is ${auth_pwhash}"
auth_pwsalthash=$(printf %s "${password_input}${AUTH_SALT}" | sha256sum - | cut -c -64)
debug "auth_pwsalthash is ${auth_pwsalthash}"
auth_password="[${AUTH_SALT}]${auth_pwsalthash}"
echo "${auth_password}"
}
###############################################################################
# Outputs to stderr
###############################################################################
function debug() {
# shellcheck disable=SC2236
if [ ! -z ${verbose+x} ]; then
>&2 echo " DEBUG: $*"
fi
}
###############################################################################
# Print usage information
###############################################################################
function usage() {
>&2 echo "Usage: $0 -u <string> -f <path/to/file> -d <path/to/db> [...]"
>&2 echo " -u <string> Gateway Admin Username"
>&2 echo " -f <path/to/file> Path to secret file containing password or salted hash"
>&2 echo " -d <path/to/db> Path to Ignition Configuration DB"
>&2 echo " -s <salt method> Salt method, either 'timestamp' or 'random' (default)"
}
# Argument Processing
while getopts ":hvu:f:d:s:" opt; do
case "$opt" in
v)
verbose=1
;;
u)
GATEWAY_ADMIN_USERNAME="${OPTARG}"
;;
f)
SECRET_LOCATION="${OPTARG}"
;;
d)
DB_LOCATION="${OPTARG}"
DB_FILE=$(basename "${DB_LOCATION}")
;;
s)
# Compute AUTH_SALT based on timestamp or random
case "${OPTARG}" in
timestamp)
AUTH_SALT=$(date +%s | sha256sum | head -c 8)
;;
random)
# no-op, default will be set below
;;
*)
usage
echo "Invalid salt method: ${OPTARG}" >&2
exit 1
;;
esac
;;
h)
usage
exit 0
;;
\?)
usage
echo "Invalid option: -${OPTARG}" >&2
exit 1
;;
:)
usage
echo "Invalid option: -${OPTARG} requires an argument" >&2
exit 1
;;
esac
done
# shift positional args based on number consumed by getopts
shift $((OPTIND-1))
# Check for required defaults
if [ -z "${GATEWAY_ADMIN_USERNAME:-}" ] || [ -z "${SECRET_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then
usage
exit 1
fi
# set defaults for unset optional args
if [[ -z ${AUTH_SALT+x} ]]; then
AUTH_SALT=$(od -An -v -t x1 -N 4 /dev/random | tr -d ' ')
fi
main

88
gw-build/retrieve-modules.sh Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
###############################################################################
# Retrieves third-party modules and verifies their checksums
###############################################################################
function main() {
if [ -z "${SUPPLEMENTAL_MODULES}" ]; then
return 0 # Silently exit if there are no supplemental modules to target
fi
retrieve_modules
}
###############################################################################
# Download the modules
###############################################################################
function retrieve_modules() {
IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}"
for module_install_key in "${module_install_key_arr[@]}"; do
download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL"
download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256"
if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then
download_basename=$(basename "${!download_url_env}")
wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \
[[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c -
else
echo "Error finding specified module ${module_install_key} in build args, aborting..."
exit 1
fi
done
}
###############################################################################
# Outputs to stderr
###############################################################################
function debug() {
# shellcheck disable=SC2236
if [ ! -z ${verbose+x} ]; then
>&2 echo " DEBUG: $*"
fi
}
###############################################################################
# Print usage information
###############################################################################
function usage() {
>&2 echo "Usage: $0 -m \"space-separated modules list\""
>&2 echo " -m: space-separated list of module identifiers to download"
}
# Argument Processing
while getopts ":hvm:" opt; do
case "$opt" in
v)
verbose=1
;;
m)
SUPPLEMENTAL_MODULES="${OPTARG}"
;;
h)
usage
exit 0
;;
\?)
usage
echo "Invalid option: -${OPTARG}" >&2
exit 1
;;
:)
usage
echo "Invalid option: -${OPTARG} requires an argument" >&2
exit 1
;;
esac
done
# shift positional args based on number consumed by getopts
shift $((OPTIND-1))
# exit on missing required args
if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then
usage
exit 1
fi
main