#!/usr/bin/env bash # ============================================================================ # SHARP-ER - generate the Strapi secrets an environment file needs # ---------------------------------------------------------------------------- # ./scripts/generate-secrets.sh print a block to paste # ./scripts/generate-secrets.sh --write .env fill the empty keys in .env # ./scripts/generate-secrets.sh --write apps/cms/.env # ./scripts/generate-secrets.sh --write .env --force rotate values already set # # Safe by default: an existing, non-placeholder value is never overwritten # unless --force is given, so re-running it after adding one key only fills the # gap. With --force the previous file is kept as .bak. # # Rotating APP_KEYS signs everyone out of the admin panel; rotating # ENCRYPTION_KEY makes values already encrypted in the database unreadable. # ============================================================================ set -euo pipefail KEYS=(APP_KEYS API_TOKEN_SALT ADMIN_JWT_SECRET JWT_SECRET TRANSFER_TOKEN_SALT ENCRYPTION_KEY) APP_KEYS_COUNT=4 TARGET="" FORCE=0 if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m' else C_OK=""; C_WARN=""; C_ERR=""; C_DIM=""; C_OFF="" fi ok() { printf '%s ok %s %s\n' "$C_OK" "$C_OFF" "$*"; } info() { printf '%s .. %s %s\n' "$C_DIM" "$C_OFF" "$*"; } warn() { printf '%s warn %s %s\n' "$C_WARN" "$C_OFF" "$*" >&2; } die() { printf '%s fail %s %s\n' "$C_ERR" "$C_OFF" "$*" >&2; exit 1; } usage() { cat <<'USAGE' generate-secrets.sh - fresh random secrets for Strapi. ./scripts/generate-secrets.sh print a block to paste ./scripts/generate-secrets.sh --write .env fill empty keys in place ./scripts/generate-secrets.sh --write .env --force rotate values already set Keys handled: APP_KEYS (4 comma-separated), API_TOKEN_SALT, ADMIN_JWT_SECRET, JWT_SECRET, TRANSFER_TOKEN_SALT, ENCRYPTION_KEY. If the target does not exist but a matching .env.example sits next to it, the template is copied first, so this also bootstraps a new environment file. Existing values are preserved unless --force is passed (previous file kept as .bak). USAGE exit 0 } while [ $# -gt 0 ]; do case "$1" in --write) TARGET="${2:?--write needs a file path}"; shift 2 ;; --force) FORCE=1; shift ;; -h|--help) usage ;; *) die "unknown option: $1 (try --help)" ;; esac done # --- Randomness ------------------------------------------------------------- # 32 bytes, base64. openssl is used when present; node and /dev/urandom are # equally good fallbacks so the script works on a bare machine. random_secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 32 | tr -d '\n' elif command -v node >/dev/null 2>&1; then node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64'))" elif [ -r /dev/urandom ]; then head -c 32 /dev/urandom | base64 | tr -d '\n' else die "no source of randomness found (need openssl, node, or /dev/urandom)." fi } value_for() { local key="$1" i out="" if [ "$key" = "APP_KEYS" ]; then for ((i = 0; i < APP_KEYS_COUNT; i++)); do out+="${out:+,}$(random_secret)" done printf '%s' "$out" else random_secret fi } # A value that still has to be replaced: empty, or one of the template's # own placeholders. is_placeholder() { local value="$1" value="${value%\"}"; value="${value#\"}" value="${value%\'}"; value="${value#\'}" [ -z "$value" ] && return 0 case "$value" in replace-with*|changeme*|CHANGEME*|todo*|TODO*) return 0 ;; *) return 1 ;; esac } # --- Print mode ------------------------------------------------------------- if [ -z "$TARGET" ]; then printf '# Strapi secrets - generated %s. Paste into .env / apps/cms/.env.\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" for key in "${KEYS[@]}"; do printf '%s=%s\n' "$key" "$(value_for "$key")" done exit 0 fi # --- Write mode ------------------------------------------------------------- if [ ! -f "$TARGET" ]; then template="$(dirname -- "$TARGET")/.env.example" [ -f "$template" ] || die "$TARGET does not exist and there is no $template to copy." cp -- "$template" "$TARGET" info "created $TARGET from $(basename -- "$(dirname -- "$template")")/.env.example" fi tmp="$(mktemp "${TARGET}.XXXXXX")" # Secrets must never be world-readable, not even for a moment. chmod 600 "$tmp" trap 'rm -f -- "$tmp"' EXIT declare -A seen=() filled=(); kept=(); rotated=() while IFS= read -r line || [ -n "$line" ]; do key="${line%%=*}" handled=0 for candidate in "${KEYS[@]}"; do [ "$key" = "$candidate" ] || continue handled=1 seen["$candidate"]=1 current="${line#*=}" if [ "$FORCE" -eq 1 ]; then printf '%s=%s\n' "$candidate" "$(value_for "$candidate")" >> "$tmp" rotated+=("$candidate") elif is_placeholder "$current"; then printf '%s=%s\n' "$candidate" "$(value_for "$candidate")" >> "$tmp" filled+=("$candidate") else printf '%s\n' "$line" >> "$tmp" kept+=("$candidate") fi break done [ "$handled" -eq 1 ] || printf '%s\n' "$line" >> "$tmp" done < "$TARGET" missing=() for key in "${KEYS[@]}"; do [ -n "${seen[$key]:-}" ] || missing+=("$key") done if [ "${#missing[@]}" -gt 0 ]; then { printf '\n# Strapi secrets - added by scripts/generate-secrets.sh on %s\n' "$(date -u '+%Y-%m-%d')" for key in "${missing[@]}"; do printf '%s=%s\n' "$key" "$(value_for "$key")" done } >> "$tmp" filled+=("${missing[@]}") fi if [ "$FORCE" -eq 1 ]; then cp -- "$TARGET" "${TARGET}.bak" chmod 600 "${TARGET}.bak" warn "previous values kept in ${TARGET}.bak - delete it once the new ones work." fi cat -- "$tmp" > "$TARGET" chmod 600 "$TARGET" rm -f -- "$tmp" trap - EXIT [ "${#filled[@]}" -gt 0 ] && ok "generated: ${filled[*]}" [ "${#rotated[@]}" -gt 0 ] && ok "rotated: ${rotated[*]}" [ "${#kept[@]}" -gt 0 ] && info "left alone (already set): ${kept[*]}" ok "$TARGET is ready (mode 600)" exit 0