Pilot demo 28.05.2026: closed gendsgn.ru/* (включая trade-in/) basic_auth gate. /health и /preview/* остаются public через route { } wrapper (Caddy 2.x directive ordering fix).
11 users (admin + user1..user10), bcrypt cost=14, snippet в git (audit trail). Scripts: scripts/auth/{add,remove,list}_user.sh.
Fixup PR #426 (route{} wrap + log rotation + base64 busybox-compat + runbook audit log fix).
102 lines
3.9 KiB
Bash
102 lines
3.9 KiB
Bash
#!/usr/bin/env bash
|
||
# add_user.sh — добавить пользователя в caddy/users.caddy.snippet
|
||
# Usage: ./scripts/auth/add_user.sh <username> "<comment>"
|
||
#
|
||
# - Читает пароль из stdin (pipe) или генерирует 16-char random если интерактивен.
|
||
# - Bcrypt через `docker run --rm caddy:2 caddy hash-password`.
|
||
# - Печатает plain password на stdout ОДИН РАЗ.
|
||
# - НЕ коммитит изменения — это задача разработчика.
|
||
set -euo pipefail
|
||
|
||
SNIPPET="$(dirname "$(dirname "$(realpath "$0")")")/caddy/users.caddy.snippet"
|
||
|
||
usage() {
|
||
echo "Usage: $0 <username> \"<comment>\"" >&2
|
||
exit 1
|
||
}
|
||
|
||
# ─── args ────────────────────────────────────────────────────────────────────
|
||
|
||
[[ $# -lt 2 ]] && usage
|
||
|
||
USERNAME="$1"
|
||
COMMENT="$2"
|
||
DATE="$(date -u +%Y-%m-%d)"
|
||
|
||
# Validate username: ^[a-z][a-z0-9-]{2,30}$
|
||
if ! [[ "$USERNAME" =~ ^[a-z][a-z0-9-]{2,30}$ ]]; then
|
||
echo "ERROR: invalid username '$USERNAME'. Must match ^[a-z][a-z0-9-]{2,30}$" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ─── idempotency guard ────────────────────────────────────────────────────────
|
||
|
||
if grep -qP "^\s+${USERNAME}\s" "$SNIPPET" 2>/dev/null; then
|
||
echo "ERROR: user '$USERNAME' already exists in $SNIPPET. Remove first with remove_user.sh." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ─── password ─────────────────────────────────────────────────────────────────
|
||
|
||
if [ -t 0 ]; then
|
||
# Interactive — generate random password
|
||
PLAIN_PASS="$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16)"
|
||
else
|
||
# Piped input
|
||
read -r PLAIN_PASS
|
||
fi
|
||
|
||
if [[ -z "$PLAIN_PASS" ]]; then
|
||
echo "ERROR: empty password" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ─── bcrypt via caddy container ───────────────────────────────────────────────
|
||
# Caddy 2.11+ requires base64-encoded bcrypt in Caddyfile basic_auth.
|
||
# Password passed via stdin to avoid exposure in process list.
|
||
|
||
HASH="$(printf '%s' "$PLAIN_PASS" | docker run --rm -i caddy:2 sh -c '
|
||
pass=$(cat)
|
||
raw=$(caddy hash-password --algorithm bcrypt --plaintext "$pass")
|
||
printf "%s" "$raw" | base64 | tr -d "\n"
|
||
')"
|
||
|
||
if [[ -z "$HASH" ]]; then
|
||
echo "ERROR: caddy hash-password returned empty hash" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ─── insert into snippet ─────────────────────────────────────────────────────
|
||
# Insert before the closing brace of basic_auth block
|
||
|
||
if ! grep -q '^}' "$SNIPPET"; then
|
||
echo "ERROR: cannot find closing brace in $SNIPPET" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Use a temp file for safe in-place edit
|
||
TMP="$(mktemp)"
|
||
trap 'rm -f "$TMP"' EXIT
|
||
|
||
# Insert new user line before the last closing `}` of the basic_auth block
|
||
awk -v user="$USERNAME" -v hash="$HASH" -v comment="$COMMENT" -v date="$DATE" '
|
||
/^}/ && !done {
|
||
printf " %-7s %s # %s (%s)\n", user, hash, comment, date
|
||
done=1
|
||
}
|
||
{ print }
|
||
' "$SNIPPET" > "$TMP"
|
||
|
||
mv "$TMP" "$SNIPPET"
|
||
|
||
# ─── output ───────────────────────────────────────────────────────────────────
|
||
|
||
echo "Added user '$USERNAME' to $SNIPPET"
|
||
echo ""
|
||
echo "Plain password (save now — not stored anywhere):"
|
||
echo "$PLAIN_PASS"
|
||
echo ""
|
||
echo "Next steps:"
|
||
echo " 1. Commit caddy/users.caddy.snippet → PR → merge → GHA deploy"
|
||
echo " 2. After deploy, Caddy auto-reloads (deploy.yml does caddy reload)"
|
||
echo " 3. Send username + password to stakeholder via secure channel"
|