├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── gw-build ├── Dockerfile ├── base.gwbk ├── docker-entrypoint-shim.sh ├── register-module.sh ├── register-password.sh └── retrieve-modules.sh └── gw-secrets └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text eol=lf 3 | *.gwbk binary 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kevin Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Derived Image Solution for Ignition 2 | 3 | ![Ignition 8.1.47](https://img.shields.io/badge/ignition-8.1.47-brightgreen.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAEt2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgdGlmZjpJbWFnZUxlbmd0aD0iNDgiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNDgiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLjAiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLjAiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI0OCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjQ4IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiCiAgIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjAtMTEt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) 4 | ![Cirrus Link 4.0.26](https://img.shields.io/badge/cirrus--link-4.0.26-336b93.svg) 5 | 6 | This solution provides an example of how to implement a derived Ignition Docker image with the following features: 7 | 8 | - Adding third-party modules such as MQTT Engine and Azure Injector. 9 | - Bundling an integrated gateway backup. 10 | - Setting default username and password of `default` built-in user source. 11 | - Shimming the entrypoint script with a spot to introduce custom functionality to run prior to launching the gateway. 12 | 13 | ## How to Build 14 | 15 | Before you get started, create a file called `gw-secrets/GATEWAY_ADMIN_PASSWORD` with the password that you want to associate with the `admin` user. There is special functionality built into the "prep" stage of the multi-stage build that updates the `default` user source from the built-in gateway backup with your desired baseline password. This is converted to a one-way salt+hash for storage in the gwbk. 16 | 17 | There is a `docker-compose.yml` solution that can be used to build the image. You can build+run the image with: 18 | 19 | ``` 20 | docker compose up --build -d 21 | ``` 22 | 23 | > [!WARNING] 24 | > The resultant gateway that will launch at http://localhost:8088 does **not** have a volume associated with it in order to assist with rapid testing of changes. Please add a volume definition to persist your gateway state data if you use this solution outside of testing the derived image build! 25 | 26 | If you want to build the image with `docker build`, you can use something like the command below: 27 | 28 | ``` 29 | docker build \ 30 | --build-arg IGNITION_VERSION=8.1.47 \ 31 | --build-arg SUPPLEMENTAL_MODULES="mqttdistributor mqttengine mqtttransmission" \ 32 | --secret id=gateway-admin-password,src=gw-secrets/GATEWAY_ADMIN_PASSWORD \ 33 | -t myimage:mytag \ 34 | gw-build 35 | ``` 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | x-default-logging: &default-logging 3 | logging: 4 | options: 5 | max-size: '100m' 6 | max-file: '5' 7 | driver: json-file 8 | 9 | services: 10 | gateway: 11 | <<: [ *default-logging ] 12 | build: 13 | context: gw-build 14 | dockerfile: Dockerfile 15 | args: 16 | IGNITION_VERSION: ${IGNITION_VERSION:-8.1.47} 17 | SUPPLEMENTAL_MODULES: "azureiotinjector mqttengine" 18 | # GATEWAY_ADMIN_USERNAME: superadmin 19 | secrets: 20 | - gateway-admin-password 21 | # image: myimage:mytag # uncomment to apply a custom tag to the built image 22 | ports: 23 | - 8088:8088 24 | # volumes: # this volumes section is commented out for easier testing 25 | # - gateway-data:/usr/local/bin/ignition/data 26 | environment: 27 | # Defining these below will perform a password reset, see the build above for 28 | # updating/preloading the admin username/password in the `default` user source. 29 | # GATEWAY_ADMIN_USERNAME: admin 30 | # GATEWAY_ADMIN_PASSWORD_FILE: /run/secrets/gateway-admin-password 31 | TZ: America/Chicago 32 | GATEWAY_MODULES_ENABLED: "alarm-notification,\ 33 | allen-bradley-drivers,\ 34 | bacnet-driver,\ 35 | dnp3-driver,\ 36 | enterprise-administration,\ 37 | iec-61850-driver,\ 38 | logix-driver,\ 39 | mitsubishi-driver,\ 40 | modbus-driver-v2,\ 41 | omron-driver,\ 42 | opc-ua,\ 43 | perspective,\ 44 | reporting,\ 45 | serial-support-client,\ 46 | serial-support-gateway,\ 47 | sfc,\ 48 | siemens-drivers,\ 49 | sms-notification,\ 50 | sql-bridge,\ 51 | symbol-factory,\ 52 | tag-historian,\ 53 | udp-tcp-drivers,\ 54 | vision,\ 55 | voice-notification,\ 56 | web-browser,\ 57 | web-developer" 58 | command: > 59 | -n Ignition-base 60 | -m 1024 61 | 62 | secrets: 63 | gateway-admin-password: 64 | file: gw-secrets/GATEWAY_ADMIN_PASSWORD 65 | -------------------------------------------------------------------------------- /gw-build/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.14 2 | ARG IGNITION_VERSION 3 | FROM inductiveautomation/ignition:${IGNITION_VERSION:-latest} AS prep 4 | 5 | # Temporarily become root for system-level updates (required for 8.1.26+) 6 | USER root 7 | 8 | # Install some prerequisite packages 9 | RUN apt-get update && apt-get install -y wget ca-certificates jq zip unzip sqlite3 10 | 11 | ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/AWS-Injector-signed.modl" 12 | ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_SHA256="f3f3be84c3a4972a2c711382fe0780fcfc3acf152207b90ab7ab95fccff1a623" 13 | ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/Azure-Injector-signed.modl" 14 | ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_SHA256="dfbbab663aba90a29f382efc216341a953f12ec68e23882ec0d13c8c8e9fb054" 15 | ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/Google-Cloud-Injector-signed.modl" 16 | ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_SHA256="43071699bc309395dadd2c124ad0e4172ee6c79b9cc7e93c3f88224f7dc97dac" 17 | ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/MQTT-Transmission-signed.modl" 18 | ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_SHA256="fdf8ae733ec5a82218977e7b20bc2afda0423adfd1a127ae8c21ce27bf5bc6c7" 19 | ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Transmission-signed.modl" 20 | ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_SHA256="notused" 21 | ARG SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/MQTT-Engine-signed.modl" 22 | ARG SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_SHA256="f6ef3c575b9b6aa04deebeb040b27b734629908c25369fb4ff1e26a00d37f098" 23 | ARG SUPPLEMENTAL_MQTTENGINENIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Engine-signed.modl" 24 | ARG SUPPLEMENTAL_MQTTENGINENIGHTLY_DOWNLOAD_SHA256="notused" 25 | ARG SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.26/MQTT-Distributor-signed.modl" 26 | ARG SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_SHA256="7d1e3ed9700228b8d41b3b7730bc7e058f0be4ddbaeb6d635b4c196355c44e34" 27 | ARG SUPPLEMENTAL_MQTTDISTRIBUTORNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Distributor-signed.modl" 28 | ARG SUPPLEMENTAL_MQTTDISTRIBUTORNIGHTLY_DOWNLOAD_SHA256="notused" 29 | ARG SUPPLEMENTAL_MODULES 30 | 31 | 32 | # Set working directory for this prep image and ensure that exits from sub-shells bubble up and report an error 33 | WORKDIR /root 34 | SHELL [ "/usr/bin/env", "-S", "bash", "-euo", "pipefail", "-O", "inherit_errexit", "-c" ] 35 | 36 | # Retrieve all targeted modules and verify their integrity 37 | COPY --chmod=0755 retrieve-modules.sh . 38 | RUN ./retrieve-modules.sh \ 39 | -m "${SUPPLEMENTAL_MODULES:-}" 40 | 41 | # Set CERTIFICATES/EULAS acceptance in gateway backup config db 42 | COPY base.gwbk . 43 | COPY --chmod=0755 register-module.sh register-password.sh ./ 44 | ARG GATEWAY_ADMIN_USERNAME="admin" 45 | RUN --mount=type=secret,id=gateway-admin-password \ 46 | unzip -q base.gwbk db_backup_sqlite.idb && \ 47 | shopt -s nullglob; \ 48 | for module in *.modl; do \ 49 | ./register-module.sh \ 50 | -f "${module}" \ 51 | -d db_backup_sqlite.idb; \ 52 | done; \ 53 | shopt -u nullglob && \ 54 | ./register-password.sh \ 55 | -u "${GATEWAY_ADMIN_USERNAME}" \ 56 | -f /run/secrets/gateway-admin-password \ 57 | -d db_backup_sqlite.idb && \ 58 | zip -q -f base.gwbk db_backup_sqlite.idb || \ 59 | if [[ ${ZIP_EXIT_CODE:=$?} == 12 ]]; then \ 60 | echo "No changes to internal database needed during module registration."; \ 61 | else \ 62 | echo "Unknown error (${ZIP_EXIT_CODE}) encountered during re-packaging of config db, exiting." && \ 63 | exit ${ZIP_EXIT_CODE}; \ 64 | fi 65 | 66 | # Final Image 67 | FROM inductiveautomation/ignition:${IGNITION_VERSION:-latest} AS final 68 | 69 | # Temporarily become root for system-level updates (required for 8.1.26+) 70 | USER root 71 | 72 | # Add supplemental packages, such as git if needed/desired 73 | RUN apt-get update && \ 74 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 75 | git && \ 76 | rm -rf /var/lib/apt/lists/* 77 | 78 | # Embed modules and base gwbk from prep image as well as entrypoint shim 79 | COPY --from=prep --chown=ignition:ignition /root/*.modl ${IGNITION_INSTALL_LOCATION}/user-lib/modules/ 80 | COPY --from=prep --chown=ignition:ignition /root/base.gwbk ${IGNITION_INSTALL_LOCATION}/base.gwbk 81 | COPY --chmod=0755 --chown=root:root docker-entrypoint-shim.sh /usr/local/bin/ 82 | 83 | # Return to ignition user 84 | USER ignition 85 | 86 | # Supplement other default environment variables 87 | ENV ACCEPT_IGNITION_EULA=Y \ 88 | IGNITION_EDITION=standard \ 89 | GATEWAY_MODULES_ENABLED=all 90 | 91 | # Target the entrypoint shim for any custom logic prior to gateway launch 92 | ENTRYPOINT [ "docker-entrypoint-shim.sh" ] 93 | -------------------------------------------------------------------------------- /gw-build/base.gwbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdgen88/ignition-derived-example/bf484b1f56bc5beb77f5ccfe271f41779c116128/gw-build/base.gwbk -------------------------------------------------------------------------------- /gw-build/docker-entrypoint-shim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ### add other custom logic here that will run at container launch 5 | 6 | # Kick off the built-in entrypoint, with an in-built restore (-r ) directive 7 | exec docker-entrypoint.sh -r base.gwbk "$@" 8 | -------------------------------------------------------------------------------- /gw-build/register-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | ############################################################################### 6 | # Performs auto-acceptance of EULA and import of certificates for third-party modules 7 | ############################################################################### 8 | function main() { 9 | if [ ! -f "${MODULE_LOCATION}" ]; then 10 | echo "" 11 | return 0 # Silently exit if there is no /modules path 12 | elif [ ! -f "${DB_LOCATION}" ]; then 13 | echo "WARNING: ${DB_FILE} not found, skipping module registration" 14 | return 0 15 | fi 16 | 17 | register_module 18 | } 19 | 20 | ############################################################################### 21 | # Register the module with the target Config DB 22 | ############################################################################### 23 | function register_module() { 24 | local SQLITE3=( sqlite3 "${DB_LOCATION}" ) 25 | 26 | # Tie into db 27 | local keytool module_sourcepath 28 | module_basename=$(basename "${MODULE_LOCATION}") 29 | module_sourcepath=${MODULE_LOCATION} 30 | keytool=$(which keytool) 31 | 32 | echo "Processing Module: ${module_basename}" 33 | 34 | # Populate CERTIFICATES table 35 | local cert_info subject_name thumbprint next_certificates_id thumbprint_already_exists 36 | cert_info=$( unzip -qq -c "${module_sourcepath}" certificates.p7b | $keytool -printcert -v | head -n 9 ) 37 | 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)}' ) 38 | subject_name=$( echo "${cert_info}" | grep -m 1 -Po '^Owner: CN=\K(.+?)(?=, (OU|O|L|ST|C)=)' | sed -e 's/"//g' ) 39 | echo " Thumbprint: ${thumbprint}" 40 | echo " Subject Name: ${subject_name}" 41 | next_certificates_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(CERTIFICATES_ID)+1,1) FROM CERTIFICATES" ) 42 | thumbprint_already_exists=$( "${SQLITE3[@]}" "SELECT 1 FROM CERTIFICATES WHERE lower(hex(THUMBPRINT)) = '${thumbprint}'" ) 43 | if [ "${thumbprint_already_exists}" != "1" ]; then 44 | echo " Accepting Certificate as CERTIFICATES_ID=${next_certificates_id}" 45 | "${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'" 46 | else 47 | echo " Thumbprint already found in CERTIFICATES table, skipping INSERT" 48 | fi 49 | 50 | # Populate EULAS table 51 | local next_eulas_id license_crc32 module_id 52 | local -i module_id_check 53 | next_eulas_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(EULAS_ID)+1,1) FROM EULAS" ) 54 | license_filename=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=).*(?=).*(?== 0 )); then 63 | echo " Accepting License on your behalf as EULAS_ID=${next_eulas_id}" 64 | "${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'" 65 | else 66 | echo " License EULA already found in EULAS table, skipping INSERT" 67 | fi 68 | } 69 | 70 | ############################################################################### 71 | # Outputs to stderr 72 | ############################################################################### 73 | function debug() { 74 | # shellcheck disable=SC2236 75 | if [ ! -z ${verbose+x} ]; then 76 | >&2 echo " DEBUG: $*" 77 | fi 78 | } 79 | 80 | ############################################################################### 81 | # Print usage information 82 | ############################################################################### 83 | function usage() { 84 | >&2 echo "Usage: $0 -f -d " 85 | } 86 | 87 | # Argument Processing 88 | while getopts ":hvf:d:" opt; do 89 | case "$opt" in 90 | v) 91 | verbose=1 92 | ;; 93 | f) 94 | MODULE_LOCATION="${OPTARG}" 95 | ;; 96 | d) 97 | DB_LOCATION="${OPTARG}" 98 | DB_FILE=$(basename "${DB_LOCATION}") 99 | ;; 100 | h) 101 | usage 102 | exit 0 103 | ;; 104 | \?) 105 | usage 106 | echo "Invalid option: -${OPTARG}" >&2 107 | exit 1 108 | ;; 109 | :) 110 | usage 111 | echo "Invalid option: -${OPTARG} requires an argument" >&2 112 | exit 1 113 | ;; 114 | esac 115 | done 116 | 117 | # shift positional args based on number consumed by getopts 118 | shift $((OPTIND-1)) 119 | 120 | if [ -z "${MODULE_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then 121 | usage 122 | exit 1 123 | fi 124 | 125 | main -------------------------------------------------------------------------------- /gw-build/register-password.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | # Global variables 6 | declare -u AUTH_SALT 7 | 8 | ############################################################################### 9 | # Update an Ignition SQLite Configuration DB with a baseline username/password 10 | # ---------------------------------------------------------------------------- 11 | # ref: https://gist.github.com/thirdgen88/c4257bd4c47b6cc7194d1f5e7cbd6444 12 | ############################################################################### 13 | function main() { 14 | if [ ! -f "${SECRET_LOCATION}" ]; then 15 | echo "" 16 | return 0 # Silently exit if there is no secret at target path 17 | elif [ ! -f "${DB_LOCATION}" ]; then 18 | echo "WARNING: ${DB_FILE} not found, skipping password registration" 19 | return 0 20 | fi 21 | 22 | register_password 23 | } 24 | 25 | ############################################################################### 26 | # Updates the target Config DB with the target username and salted pw hash 27 | ############################################################################### 28 | function register_password() { 29 | local SQLITE3=( sqlite3 "${DB_LOCATION}" ) password_hash password_input 30 | 31 | echo "Registering Admin Password with Configuration DB" 32 | 33 | # Generate Salted PW Hash 34 | password_input="$(< "${SECRET_LOCATION}")" 35 | if [[ "${password_input}" =~ ^\[[0-9A-F]{8,}][0-9a-f]{64}$ ]]; then 36 | debug "Password is already hashed" 37 | password_hash="${password_input}" 38 | else 39 | password_hash=$(generate_salted_hash "$(<"${SECRET_LOCATION}")") 40 | fi 41 | 42 | # Update INTERNALUSERTABLE 43 | echo " Setting default admin user to USERNAME='${GATEWAY_ADMIN_USERNAME}' and PASSWORD='${password_hash}'" 44 | "${SQLITE3[@]}" "UPDATE INTERNALUSERTABLE SET USERNAME='${GATEWAY_ADMIN_USERNAME}', PASSWORD='${password_hash}' WHERE PROFILEID=1 AND USERID=1" 45 | } 46 | 47 | ############################################################################### 48 | # Processes password input and translates to salted hash 49 | ############################################################################### 50 | function generate_salted_hash() { 51 | local auth_pwhash auth_pwsalthash auth_password password_input 52 | password_input="${1}" 53 | 54 | debug "auth_salt is ${AUTH_SALT}" 55 | auth_pwhash=$(printf %s "${password_input}" | sha256sum - | cut -c -64) 56 | debug "auth_pwhash is ${auth_pwhash}" 57 | auth_pwsalthash=$(printf %s "${password_input}${AUTH_SALT}" | sha256sum - | cut -c -64) 58 | debug "auth_pwsalthash is ${auth_pwsalthash}" 59 | auth_password="[${AUTH_SALT}]${auth_pwsalthash}" 60 | 61 | echo "${auth_password}" 62 | } 63 | 64 | ############################################################################### 65 | # Outputs to stderr 66 | ############################################################################### 67 | function debug() { 68 | # shellcheck disable=SC2236 69 | if [ ! -z ${verbose+x} ]; then 70 | >&2 echo " DEBUG: $*" 71 | fi 72 | } 73 | 74 | ############################################################################### 75 | # Print usage information 76 | ############################################################################### 77 | function usage() { 78 | >&2 echo "Usage: $0 -u -f -d [...]" 79 | >&2 echo " -u Gateway Admin Username" 80 | >&2 echo " -f Path to secret file containing password or salted hash" 81 | >&2 echo " -d Path to Ignition Configuration DB" 82 | >&2 echo " -s Salt method, either 'timestamp' or 'random' (default)" 83 | } 84 | 85 | # Argument Processing 86 | while getopts ":hvu:f:d:s:" opt; do 87 | case "$opt" in 88 | v) 89 | verbose=1 90 | ;; 91 | u) 92 | GATEWAY_ADMIN_USERNAME="${OPTARG}" 93 | ;; 94 | f) 95 | SECRET_LOCATION="${OPTARG}" 96 | ;; 97 | d) 98 | DB_LOCATION="${OPTARG}" 99 | DB_FILE=$(basename "${DB_LOCATION}") 100 | ;; 101 | s) 102 | # Compute AUTH_SALT based on timestamp or random 103 | case "${OPTARG}" in 104 | timestamp) 105 | AUTH_SALT=$(date +%s | sha256sum | head -c 8) 106 | ;; 107 | random) 108 | # no-op, default will be set below 109 | ;; 110 | *) 111 | usage 112 | echo "Invalid salt method: ${OPTARG}" >&2 113 | exit 1 114 | ;; 115 | esac 116 | ;; 117 | h) 118 | usage 119 | exit 0 120 | ;; 121 | \?) 122 | usage 123 | echo "Invalid option: -${OPTARG}" >&2 124 | exit 1 125 | ;; 126 | :) 127 | usage 128 | echo "Invalid option: -${OPTARG} requires an argument" >&2 129 | exit 1 130 | ;; 131 | esac 132 | done 133 | 134 | # shift positional args based on number consumed by getopts 135 | shift $((OPTIND-1)) 136 | 137 | # Check for required defaults 138 | if [ -z "${GATEWAY_ADMIN_USERNAME:-}" ] || [ -z "${SECRET_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then 139 | usage 140 | exit 1 141 | fi 142 | 143 | # set defaults for unset optional args 144 | if [[ -z ${AUTH_SALT+x} ]]; then 145 | AUTH_SALT=$(od -An -v -t x1 -N 4 /dev/random | tr -d ' ') 146 | fi 147 | 148 | main 149 | -------------------------------------------------------------------------------- /gw-build/retrieve-modules.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | ############################################################################### 6 | # Retrieves third-party modules and verifies their checksums 7 | ############################################################################### 8 | function main() { 9 | if [ -z "${SUPPLEMENTAL_MODULES}" ]; then 10 | return 0 # Silently exit if there are no supplemental modules to target 11 | fi 12 | 13 | retrieve_modules 14 | } 15 | 16 | ############################################################################### 17 | # Download the modules 18 | ############################################################################### 19 | function retrieve_modules() { 20 | IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}" 21 | for module_install_key in "${module_install_key_arr[@]}"; do 22 | download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL" 23 | download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256" 24 | if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then 25 | download_basename=$(basename "${!download_url_env}") 26 | wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \ 27 | [[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c - 28 | else 29 | echo "Error finding specified module ${module_install_key} in build args, aborting..." 30 | exit 1 31 | fi 32 | done 33 | } 34 | 35 | ############################################################################### 36 | # Outputs to stderr 37 | ############################################################################### 38 | function debug() { 39 | # shellcheck disable=SC2236 40 | if [ ! -z ${verbose+x} ]; then 41 | >&2 echo " DEBUG: $*" 42 | fi 43 | } 44 | 45 | ############################################################################### 46 | # Print usage information 47 | ############################################################################### 48 | function usage() { 49 | >&2 echo "Usage: $0 -m \"space-separated modules list\"" 50 | >&2 echo " -m: space-separated list of module identifiers to download" 51 | } 52 | 53 | # Argument Processing 54 | while getopts ":hvm:" opt; do 55 | case "$opt" in 56 | v) 57 | verbose=1 58 | ;; 59 | m) 60 | SUPPLEMENTAL_MODULES="${OPTARG}" 61 | ;; 62 | h) 63 | usage 64 | exit 0 65 | ;; 66 | \?) 67 | usage 68 | echo "Invalid option: -${OPTARG}" >&2 69 | exit 1 70 | ;; 71 | :) 72 | usage 73 | echo "Invalid option: -${OPTARG} requires an argument" >&2 74 | exit 1 75 | ;; 76 | esac 77 | done 78 | 79 | # shift positional args based on number consumed by getopts 80 | shift $((OPTIND-1)) 81 | 82 | # exit on missing required args 83 | if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then 84 | usage 85 | exit 1 86 | fi 87 | 88 | main -------------------------------------------------------------------------------- /gw-secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * --------------------------------------------------------------------------------