commit 1911a4960fed7dde48e0b532f44599a5796d81cb Author: a.williamson Date: Wed Jul 22 10:31:20 2026 -0500 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. diff --git a/.env b/.env new file mode 100644 index 0000000..6a347a0 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +IGNITION_VERSION=8.3.7 +GATEWAY_MAX_MEMORY=512 \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..da3c86f --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,124 @@ +# EDGE docker-compose build/runtime fixes — 2026-07-22 + +## Starting problem +`docker compose -f EDGE/docker-compose.yml up -d --build` failed. The custom +`gw-build/Dockerfile` (patterned after +https://github.com/thirdgen88/ignition-examples/tree/main/iiot) referenced four +helper scripts that did not exist anywhere in `gw-build/` or on disk: + +- `retrieve-modules.sh` +- `register-module.sh` +- `register-password.sh` +- `docker-entrypoint-shim.sh` + +Build cache masked this initially — earlier `RUN` layers that *used* these +scripts showed `CACHED` from a prior successful build, while the `COPY` steps +that needed the actual files on disk failed with "not found". + +## What was checked +- Searched the whole filesystem (`~/git`, home directory) for the four + filenames — not found anywhere, including in the sibling `BAT/` repo (which + uses the stock Ignition image directly, no custom Dockerfile). +- Checked for any previously-built `edge-ignition` image, container, or volume + that might have the files baked in — none existed; this build has never + succeeded on this machine. +- User confirmed the reference source: the `iiot` example in + `thirdgen88/ignition-examples`, which contains all four scripts plus a + matching `Dockerfile`. + +## Changes made + +### 1. Copied the four missing scripts from the reference repo +Fetched from `thirdgen88/ignition-examples` (`iiot/gw-build/`) into +`EDGE/gw-build/`, marked executable (0755): +- `retrieve-modules.sh` +- `register-module.sh` +- `register-password.sh` +- `docker-entrypoint-shim.sh` — this one already existed locally, identical to + upstream, so no change was needed there. + +Note: a stray `register-modules.sh` (plural) was found already sitting in +`gw-build/` — an exact duplicate of `retrieve-modules.sh`'s content under the +wrong filename, unreferenced by the Dockerfile. Left in place, not deleted +(flagged to user). + +### 2. Dropped `mqttdistributor`/`mqttengine` from the build (user decision) +The reference `Dockerfile` defines `ARG` pairs +(`SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_URL`/`_SHA256`, +`SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_URL`/`_SHA256`) that our local +Dockerfile was missing, even though `docker-compose.yml` requested +`SUPPLEMENTAL_MODULES: "mqttdistributor mqttengine"`. User said they don't +want those modules, so instead of adding the missing ARGs, the module list in +`docker-compose.yml` was changed to: +```yaml +SUPPLEMENTAL_MODULES: "" +``` + +### 3. Fixed `retrieve-modules.sh` invocation for the empty-module case +`retrieve-modules.sh`'s `main()` is written to silently no-op when +`SUPPLEMENTAL_MODULES` is empty, but its own `getopts` argument-validation +block rejects `-m ""` before `main()` ever runs (`exit 1` with a usage +message). This is a bug in the vendored script itself, inherited from the +reference repo — not something introduced here. + +Rather than edit the vendored script (to keep it matching upstream), the +Dockerfile's `RUN` line was changed to skip calling the script at all when +there are no modules to fetch: +```dockerfile +# before +RUN ./retrieve-modules.sh \ + -m "${SUPPLEMENTAL_MODULES:-}" + +# after +RUN if [ -n "${SUPPLEMENTAL_MODULES:-}" ]; then ./retrieve-modules.sh -m "${SUPPLEMENTAL_MODULES}"; fi +``` + +### 4. Fixed `BASE_GWBK_NAME` mismatch +`docker-compose.yml` passed `BASE_GWBK_NAME: gateway.gwbk` as a build arg, but +the actual gateway backup file in `gw-build/` is named `base.gwbk` (matching +the Dockerfile's own default `ARG BASE_GWBK_NAME="base.gwbk"`). This mismatch +didn't hard-fail the build immediately — it caused `unzip` inside the +module/password-registration `RUN` step to silently fail-and-fallback +("cannot find or open gateway.gwbk" / "skipping password registration"), +masking the real problem until the final `COPY --from=prep .../${BASE_GWBK_NAME}` +step errored with "not found". + +Fixed by correcting the compose arg to match the real filename: +```yaml +BASE_GWBK_NAME: base.gwbk +``` +This also means admin password registration now actually runs during the +build (previously it was silently skipped due to the filename mismatch). + +### 5. Fixed a runtime crash loop after the image built successfully +Once the image built, the `EDGE` container crash-looped with: +``` +ERROR: Gateway Public HTTP/HTTPS/Address must be specified together: + - HTTPS Port not specified or is invalid +``` +The compose `command:` for the `ignition` service set `-a +gateway.localtest.me` (public address) and `-h 8088` (HTTP port) but no `-s` +(HTTPS port). Ignition's entrypoint requires all three (address, HTTP port, +HTTPS port) to be set together or none at all — this is just gateway +public-address metadata used for URL generation, not a request to actually +expose HTTPS. Fixed by adding the conventional Ignition default HTTPS port: +```yaml +command: > + -n FW-DST-SEN + -m 512 + -a gateway.localtest.me + -h 8088 + -s 8043 +``` +No compose port mapping was added for 8043 — it isn't published, only used +for the internal public-address config. + +## Result +`docker compose -f EDGE/docker-compose.yml up -d --build` now completes +successfully. Both `EDGE` (Ignition gateway) and `EDGE-db` (MariaDB) +containers reach `healthy` status. Gateway is reachable at +`http://gateway.localtest.me:8088` (or `http://localhost:8088`). + +## Open item for follow-up +- Confirm whether the stray `gw-build/register-modules.sh` (plural, unused + duplicate) should be deleted. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c95be6d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,102 @@ +x-default-logging: + &default-logging + logging: + options: + max-size: '100m' + max-file: '5' + driver: json-file + +x-ignition-opts: + &ignition-opts + <<: *default-logging + env_file: gw-init/gateway.env + secrets: + - gateway-admin-password +services: + ignition: + <<: *ignition-opts + build: + context: gw-build + dockerfile: Dockerfile + args: + IGNITION_VERSION: ${IGNITION_VERSION:-latest} + SUPPLEMENTAL_MODULES: "" + BASE_GWBK_NAME: base.gwbk + GATEWAY_ADMIN_USERNAME: admin + secrets: + # NOTE: changing a build secret will not bust the cache, run the build with `--no-cache` to force a rebuild + - gateway-admin-password + pull_policy: build + container_name: EDGE + ports: + - "8088:8088" + + environment: + ACCEPT_IGNITION_EULA: "Y" + GATEWAY_ADMIN_USERNAME: admin + GATEWAY_ADMIN_PASSWORD: password + IGNITION_EDITION: edge + DISABLE_QUICKSTART: "true" + # Run the gateway as the HOST dev uid/gid (primebench pattern): started + # as root (user 0:0), the official entrypoint chowns the bind mounts to + # IGNITION_UID/GID and drops privileges — repo-mapped gateway files stay + # owned by you, so host-side edits need no chown dance (WSL2 included). + IGNITION_UID: "${IGNITION_UID:-1000}" + IGNITION_GID: "${IGNITION_GID:-1000}" + TZ: America/Chicago + user: "0:0" + # Runtime args: gateway name + 500 mb max JVM heap (8.3 uses the -m flag, not an env var) + command: > + -n FW-DST-SEN + -m 512 + -a gateway.localtest.me + -h 8088 + -s 8043 + volumes: + + - db-data-EDGE:/usr/local/bin/ignition/data + - ./gw-backup/gateway:/backup + + depends_on: + db: + condition: service_healthy + restart: unless-stopped + networks: + - EDGE + + db: + image: mariadb:11.8 + container_name: EDGE-db + environment: + MARIADB_ROOT_PASSWORD: password + MARIADB_DATABASE: ignition + MARIADB_USER: ignition + MARIADB_PASSWORD: ignition + ports: + # localhost only, for inspecting with a SQL client + - "127.0.0.1:3306:3306" + volumes: + - db-data-EDGE:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 15s + interval: 5s + timeout: 5s + retries: 12 + restart: unless-stopped + networks: + - EDGE + +secrets: + gateway-admin-password: + file: secrets/gateway-admin-password + db-ignition-password: + file: secrets/db-ignition-password + db-root-password: + file: secrets/db-root-password + +networks: + EDGE: + +volumes: + db-data-EDGE: diff --git a/gw-build/Dockerfile b/gw-build/Dockerfile new file mode 100644 index 0000000..d567443 --- /dev/null +++ b/gw-build/Dockerfile @@ -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" ] \ No newline at end of file diff --git a/gw-build/base.gwbk b/gw-build/base.gwbk new file mode 100644 index 0000000..4b520ba Binary files /dev/null and b/gw-build/base.gwbk differ diff --git a/gw-build/docker-entrypoint-shim.sh b/gw-build/docker-entrypoint-shim.sh new file mode 100644 index 0000000..5fb8fd4 --- /dev/null +++ b/gw-build/docker-entrypoint-shim.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Kick off the built-in entrypoint, with an in-built restore (-r ) directive +exec docker-entrypoint.sh -r base.gwbk "$@" \ No newline at end of file diff --git a/gw-build/register-module.sh b/gw-build/register-module.sh new file mode 100755 index 0000000..0663bbf --- /dev/null +++ b/gw-build/register-module.sh @@ -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 '(?<=).*(?=).*(?== 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 -d " +} + +# 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 diff --git a/gw-build/register-modules.sh b/gw-build/register-modules.sh new file mode 100644 index 0000000..30935b8 --- /dev/null +++ b/gw-build/register-modules.sh @@ -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 \ No newline at end of file diff --git a/gw-build/register-password.sh b/gw-build/register-password.sh new file mode 100755 index 0000000..ebb6e82 --- /dev/null +++ b/gw-build/register-password.sh @@ -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 -f -d [...]" + >&2 echo " -u Gateway Admin Username" + >&2 echo " -f Path to secret file containing password or salted hash" + >&2 echo " -d Path to Ignition Configuration DB" + >&2 echo " -s 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 diff --git a/gw-build/retrieve-modules.sh b/gw-build/retrieve-modules.sh new file mode 100755 index 0000000..534ae6d --- /dev/null +++ b/gw-build/retrieve-modules.sh @@ -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 diff --git a/gw-init/gateway.env b/gw-init/gateway.env new file mode 100644 index 0000000..d2f445e --- /dev/null +++ b/gw-init/gateway.env @@ -0,0 +1,5 @@ +ACCEPT_IGNITION_EULA=Y +# GATEWAY_ADMIN_USERNAME=admin +# GATEWAY_ADMIN_PASSWORD_FILE=/run/secrets/gateway-admin-password +TZ=America/Chicago +GATEWAY_MODULES_ENABLED=all \ No newline at end of file diff --git a/modules/MQTT-Transmission-signed (1).modl b/modules/MQTT-Transmission-signed (1).modl new file mode 100644 index 0000000..2f71f92 Binary files /dev/null and b/modules/MQTT-Transmission-signed (1).modl differ diff --git a/secrets/db-ignition-password b/secrets/db-ignition-password new file mode 100644 index 0000000..7aa311a --- /dev/null +++ b/secrets/db-ignition-password @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/secrets/db-root-password b/secrets/db-root-password new file mode 100644 index 0000000..7aa311a --- /dev/null +++ b/secrets/db-root-password @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/secrets/gateway-admin-password b/secrets/gateway-admin-password new file mode 100644 index 0000000..7aa311a --- /dev/null +++ b/secrets/gateway-admin-password @@ -0,0 +1 @@ +password \ No newline at end of file