#!/usr/bin/env sh # shellcheck disable=SC2016 # TrueNAS deploy script for SCALE/CORE using websocket (websocat binary) # It is recommend to use a wildcard certificate # # Tested with TrueNAS SCALE 25.10 (API "wss://host/api/current", JSON-RPC 2.0). # # Unlike "truenas_ws" hook, this script does NOT use midclt, the truenas_api_client Python package. # It only depends on: # - jq # - websocat (a static binary you deploy) # # Why: avoids installing a Python environment / TrueNAS package on remote machine just to push a certificate. # # IMPORTANT: This script is written in pure POSIX sh (no coproc, no bash arrays). # # # --------------------------------------------------------------------------- # Environment variables # --------------------------------------------------------------------------- # # # Use the folowing URL to create a new API token: /ui/apikeys # # Required: # export DEPLOY_TRUENAS_APIKEY="" # # Optional: # export DEPLOY_TRUENAS_HOSTNAME="" (required on first run) # export DEPLOY_TRUENAS_PROTOCOL="ws" # ws or wss (default: ws) # export DEPLOY_TRUENAS_PORT="80" # 80, 443, 8443 (default: 80) # NOTE: defaults are intentionally "ws"/80, not "wss"/443: a freshly # installed TrueNAS serves its Web UI over plain HTTP on port 80 out # of the box, and port 443 is not listening until HTTPS is configured. # Port 80 stays reachable even after HTTPS is enabled, so this keeps # the hook working on first run without extra setup. # export DEPLOY_TRUENAS_UPDATE_FTP="no" # yes or no (default: no) also updates the FTP certificate # export DEPLOY_TRUENAS_UPDATE_APPS="no" # yes or no (default: no) also updates the certificate for any # iX app exposing a "certificate_id" option. # WARNING: this redeploys (restarts) every matching app. # --------------------------------------------------------------------------- ######################## ### Public functions ### ######################## # truenas_websocat_deploy # # Deploy new certificate to TrueNAS services with websocat binary # # Arguments # 1: Domain # 2: Key-File # 3: Certificate-File # 4: CA-File # 5: FullChain-File # Returns: # 0: Success # 1: Missing or invalid API Key # 2: TrueNAS not ready (health check failed) # 3: (reserved) # 4: FTP & iX App cert error # 5: WebUI cert error # 6: Certificate creation job error # 7: Websocat / transport call error (socket write/read failed) # 8: Missing binary or invalid configuration # 9: TrueNAS API returned an explicit error (JSON-RPC .error field) truenas_websocat_deploy() { _jq_bin=$(command -v jq 2>/dev/null) if [ -z "$_jq_bin" ]; then _err "jq binary not found in PATH. Install it using your system's package manager." return 8 fi _websocat_bin=$(command -v websocat 2>/dev/null) if [ -z "$_websocat_bin" ]; then _err "websocat binary not found in PATH. Install it using your system's package manager, or download a static binary from https://github.com/vi/websocat/releases." return 8 fi _domain="$1" _file_key="$2" _file_cert="$3" _file_cca="$4" _file_fullchain="$5" _debug _domain "$_domain" _debug _file_key "$_file_key" _debug _file_cert "$_file_cert" _debug _file_ca "$_file_cca" _debug _file_fullchain "$_file_fullchain" if [ ! -x "$_jq_bin" ]; then _err "Binary not found or not executable: $_jq_bin" return 8 fi if [ ! -x "$_websocat_bin" ]; then _err "Binary not found or not executable: $_websocat_bin" return 8 fi ### ---- Configuration ---- _info "Checking environment variables..." _getdeployconf DEPLOY_TRUENAS_APIKEY _getdeployconf DEPLOY_TRUENAS_HOSTNAME _getdeployconf DEPLOY_TRUENAS_PROTOCOL _getdeployconf DEPLOY_TRUENAS_PORT _getdeployconf DEPLOY_TRUENAS_UPDATE_FTP _getdeployconf DEPLOY_TRUENAS_UPDATE_APPS # Check API Key if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable." return 1 fi # Check Hostname, default to localhost if not set if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then _info "TrueNAS hostname not set. Using 'localhost'." DEPLOY_TRUENAS_HOSTNAME="localhost" fi # Check protocol, default to ws if not set: a freshly installed TrueNAS serves its Web UI over plain HTTP, so wss/443 is not available out of the box. # Use DEPLOY_TRUENAS_PROTOCOL="wss" once HTTPS is configured, since the payload otherwise carries the API key and private key in plain text. if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then _info "TrueNAS protocol not set. Using 'ws'." DEPLOY_TRUENAS_PROTOCOL="ws" fi # Check port, default to 80 if not set (see protocol comment above) if [ -z "$DEPLOY_TRUENAS_PORT" ]; then _info "TrueNAS port not set. Using '80'." DEPLOY_TRUENAS_PORT="80" fi case "$DEPLOY_TRUENAS_PORT" in '' | *[!0-9]*) _err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric." return 8 ;; esac _truenas_websocat_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/api/current" # Check FTP update, default to no if not set if [ -z "$DEPLOY_TRUENAS_UPDATE_FTP" ]; then _info "Certificate update for FTP is not set. Using 'no'." DEPLOY_TRUENAS_UPDATE_FTP="no" fi # Check Apps update, default to no if not set if [ -z "$DEPLOY_TRUENAS_UPDATE_APPS" ]; then _info "Certificate update for Apps is not set. Using 'no'." DEPLOY_TRUENAS_UPDATE_APPS="no" fi _debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" _debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" _debug2 DEPLOY_TRUENAS_UPDATE_FTP "$DEPLOY_TRUENAS_UPDATE_FTP" _debug2 DEPLOY_TRUENAS_UPDATE_APPS "$DEPLOY_TRUENAS_UPDATE_APPS" _debug _truenas_websocat_uri "$_truenas_websocat_uri" _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" _info "Environment variables: OK" ### ---- Persistent WebSocket connection (FIFOs, sh/dash compatible) ---- # # Authentication is tied to the WebSocket connection: # the SAME connection must stay open from login until the end, otherwise every subsequent call comes back unauthenticated. # We use two FIFOs + `exec` to talk to a background websocat process, without relying on bash-only extensions. _websocat_tmpdir=$(mktemp -d /tmp/truenas_websocat.XXXXXX) || { _err "mktemp failed" return 3 } _websocat_fifo_in="${_websocat_tmpdir}/in" _websocat_fifo_out="${_websocat_tmpdir}/out" mkfifo "$_websocat_fifo_in" "$_websocat_fifo_out" || { _err "mkfifo failed" rm -rf "$_websocat_tmpdir" return 3 } "$_websocat_bin" -n -k "$_truenas_websocat_uri" <"$_websocat_fifo_in" >"$_websocat_fifo_out" 2>"${_websocat_tmpdir}/err.log" & _websocat_pid=$! # Opening "in" for read+write avoids a deadlock if websocat hasn't opened the fifo for reading yet at the time we write to it. exec 3<>"$_websocat_fifo_in" exec 4<"$_websocat_fifo_out" sleep 1 if ! kill -0 "$_websocat_pid" 2>/dev/null; then _err "websocat exited prematurely." _err "$(cat "${_websocat_tmpdir}/err.log" 2>/dev/null)" exec 3>&- 4<&- rm -rf "$_websocat_tmpdir" return 3 fi _websocat_req_counter=0 _truenas_websocat_cleanup() { exec 3>&- 2>/dev/null exec 4<&- 2>/dev/null [ -n "$_websocat_pid" ] && kill "$_websocat_pid" 2>/dev/null rm -rf "$_websocat_tmpdir" 2>/dev/null } # _truenas_websocat_rpc_call # Does NOT log the payload/response: some calls (certificate.create, core.get_jobs) contain the certificate and private key in plain text, which would massively bloat the logs. _truenas_websocat_rpc_call() { _truenas_websocat_method="$1" _truenas_websocat_params="$2" _websocat_req_counter=$((_websocat_req_counter + 1)) _req_id="$_websocat_req_counter" _truenas_websocat_payload=$("$_jq_bin" -c -n \ --arg jsonrpc "2.0" \ --arg id "$_req_id" \ --arg method "$_truenas_websocat_method" \ --argjson params "$_truenas_websocat_params" \ '{jsonrpc: $jsonrpc, id: $id, method: $method, params: $params}') printf '%s\n' "$_truenas_websocat_payload" >&3 || { _err "Socket write failed (method: $_truenas_websocat_method)" return 7 } IFS= read -r _truenas_websocat_response <&4 || { _err "Socket read failed (method: $_truenas_websocat_method)" return 7 } printf '%s' "$_truenas_websocat_response" } # _truenas_websocat_rpc_has_error