182 lines
6.0 KiB
Bash
182 lines
6.0 KiB
Bash
#!/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 <file>.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
|
||
|
|
<file>.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
|