fix(securitate): hardening prod — headere, body-cap, non-root, backup
Findings security-review P1/P2 (2026-07-03): - headere de securitate pe toate raspunsurile (nosniff, X-Frame-Options, Referrer-Policy, HSTS doar pe HTTPS) + teste - body-cap global 10MB ca middleware ASGI pur (413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) - imagine Docker non-root (uid 10001), port 8010 aliniat, loguri pe volumul /data - fail-fast la boot cu rar_env=prod fara AUTOPASS_REQUIRE_API_KEY sau AUTOPASS_SESSION_SECRET - compose: env-uri critice obligatorii (:?) ca api/worker sa nu diverga tacit; FORWARDED_ALLOW_IPS ca rate-limit-ul sa vada IP-ul real dupa Traefik - signup fara PII in stdout: log_event in loc de print cu email (idem notify degradat) - ratelimit: sterge cheile fara timestamp-uri valide (crestere monotona a memoriei pe IP-uri reale) - backup criptat SQLite (backup online API, gpg AES256) + verificare restore + docs/backup.md Suita completa verde: 1557 passed, 1 skipped (live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
134
tools/backup_db.sh
Executable file
134
tools/backup_db.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backup criptat SQLite pentru gateway RAR AUTOPASS (T2/P0-4, evidenta legala L.142/2023).
|
||||
#
|
||||
# Foloseste API-ul de backup online SQLite (sqlite3.Connection.backup, stdlib python3) —
|
||||
# NU `cp`/`rsync`: baza ruleaza in WAL, iar o copie simpla a fisierului .db poate prinde
|
||||
# tranzactii necheckpoint-ate (copie inconsistenta). python3 e ales ca prim mijloc (nu
|
||||
# binarul CLI `sqlite3`) pentru ca e garantat prezent in imaginea Docker si pe masina de
|
||||
# dezvoltare; binarul `sqlite3` e folosit doar daca python3 lipseste (caz neasteptat).
|
||||
#
|
||||
# Rezultatul e comprimat (gzip) si criptat simetric (gpg AES256) — snapshot-ul contine
|
||||
# PII criptat Fernet plus metadate, un backup NECRIPTAT nu e permis.
|
||||
#
|
||||
# Utilizare:
|
||||
# AUTOPASS_BACKUP_PASSPHRASE_FILE=/run/secrets/backup_pass tools/backup_db.sh
|
||||
# AUTOPASS_BACKUP_PASSPHRASE=parola-de-test tools/backup_db.sh
|
||||
#
|
||||
# Variabile de mediu:
|
||||
# AUTOPASS_DB_PATH cale baza sursa (default /data/autopass.db)
|
||||
# AUTOPASS_BACKUP_DIR director destinatie backup-uri (default /data/backups)
|
||||
# AUTOPASS_BACKUP_KEEP cate backup-uri locale pastreaza (default 14)
|
||||
# AUTOPASS_BACKUP_PASSPHRASE_FILE fisier cu parola de criptare (preferat)
|
||||
# AUTOPASS_BACKUP_PASSPHRASE parola de criptare inline (fallback, ex. teste)
|
||||
# AUTOPASS_BACKUP_RCLONE_REMOTE tinta rclone optionala (ex. remote:bucket/autopass)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DB_PATH="${AUTOPASS_DB_PATH:-/data/autopass.db}"
|
||||
BACKUP_DIR="${AUTOPASS_BACKUP_DIR:-/data/backups}"
|
||||
KEEP="${AUTOPASS_BACKUP_KEEP:-14}"
|
||||
RCLONE_REMOTE="${AUTOPASS_BACKUP_RCLONE_REMOTE:-}"
|
||||
|
||||
log() { echo "[backup_db] $*"; }
|
||||
err() { echo "[backup_db] EROARE: $*" >&2; }
|
||||
|
||||
CLEANUP_PATHS=()
|
||||
cleanup() {
|
||||
local p
|
||||
for p in "${CLEANUP_PATHS[@]:-}"; do
|
||||
[[ -n "$p" ]] && rm -rf "$p"
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- parola de criptare: fara ea, refuzam sa scriem un backup necriptat ---
|
||||
if [[ -n "${AUTOPASS_BACKUP_PASSPHRASE_FILE:-}" ]]; then
|
||||
if [[ ! -f "$AUTOPASS_BACKUP_PASSPHRASE_FILE" ]]; then
|
||||
err "AUTOPASS_BACKUP_PASSPHRASE_FILE indica un fisier inexistent: $AUTOPASS_BACKUP_PASSPHRASE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
PASSPHRASE_FILE="$AUTOPASS_BACKUP_PASSPHRASE_FILE"
|
||||
elif [[ -n "${AUTOPASS_BACKUP_PASSPHRASE:-}" ]]; then
|
||||
PASSPHRASE_FILE="$(mktemp)"
|
||||
chmod 600 "$PASSPHRASE_FILE"
|
||||
CLEANUP_PATHS+=("$PASSPHRASE_FILE")
|
||||
printf '%s' "$AUTOPASS_BACKUP_PASSPHRASE" > "$PASSPHRASE_FILE"
|
||||
else
|
||||
err "lipseste parola de criptare a backup-ului."
|
||||
err "seteaza AUTOPASS_BACKUP_PASSPHRASE_FILE (recomandat) sau AUTOPASS_BACKUP_PASSPHRASE."
|
||||
err "backup necriptat NU este permis (contine PII criptat Fernet si metadate)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$DB_PATH" ]]; then
|
||||
err "baza de date nu exista: $DB_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
STAMP="$(date -u +%Y%m%d-%H%M%S)"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
CLEANUP_PATHS+=("$TMP_DIR")
|
||||
SNAPSHOT="$TMP_DIR/autopass-$STAMP.db"
|
||||
|
||||
log "snapshot online din $DB_PATH"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "$DB_PATH" "$SNAPSHOT" <<'PYEOF'
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
src = sqlite3.connect(sys.argv[1])
|
||||
dst = sqlite3.connect(sys.argv[2])
|
||||
try:
|
||||
with dst:
|
||||
src.backup(dst)
|
||||
finally:
|
||||
src.close()
|
||||
dst.close()
|
||||
PYEOF
|
||||
elif command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "$DB_PATH" ".backup '$SNAPSHOT'"
|
||||
else
|
||||
err "nici python3, nici binarul sqlite3 nu sunt disponibile - nu pot face backup online"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "$SNAPSHOT" ]]; then
|
||||
err "snapshot-ul rezultat e gol sau lipseste: $SNAPSHOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "comprim (gzip)"
|
||||
gzip -9 "$SNAPSHOT"
|
||||
SNAPSHOT_GZ="$SNAPSHOT.gz"
|
||||
|
||||
FINAL="$BACKUP_DIR/autopass-$STAMP.db.gz.gpg"
|
||||
log "criptez simetric (gpg AES256) -> $FINAL"
|
||||
gpg --batch --yes --symmetric --cipher-algo AES256 \
|
||||
--passphrase-file "$PASSPHRASE_FILE" \
|
||||
--output "$FINAL" \
|
||||
"$SNAPSHOT_GZ"
|
||||
|
||||
log "backup scris: $FINAL ($(du -h "$FINAL" | cut -f1))"
|
||||
|
||||
# --- retentie locala: pastreaza ultimele $KEEP, sterge restul ---
|
||||
mapfile -t OLD_BACKUPS < <(ls -1t "$BACKUP_DIR"/autopass-*.db.gz.gpg 2>/dev/null | tail -n "+$((KEEP + 1))")
|
||||
if [[ "${#OLD_BACKUPS[@]}" -gt 0 ]]; then
|
||||
log "sterg ${#OLD_BACKUPS[@]} backup(uri) vechi (pastrez ultimele $KEEP)"
|
||||
rm -f "${OLD_BACKUPS[@]}"
|
||||
fi
|
||||
|
||||
# --- upload optional remote ---
|
||||
if [[ -n "$RCLONE_REMOTE" ]]; then
|
||||
if command -v rclone >/dev/null 2>&1; then
|
||||
log "upload rclone -> $RCLONE_REMOTE"
|
||||
rclone copy "$FINAL" "$RCLONE_REMOTE"
|
||||
else
|
||||
log "AUTOPASS_BACKUP_RCLONE_REMOTE setat dar rclone nu e instalat - sar peste upload remote"
|
||||
fi
|
||||
else
|
||||
log "AUTOPASS_BACKUP_RCLONE_REMOTE nesetat - sar peste upload remote (doar local)"
|
||||
fi
|
||||
|
||||
log "gata."
|
||||
112
tools/restore_check.sh
Executable file
112
tools/restore_check.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verificare de restaurare pentru backup-urile SQLite criptate (T2/P0-4).
|
||||
#
|
||||
# Un backup neverificat nu e backup: decripteaza + dezarhiveaza (intr-un fisier temporar,
|
||||
# NU suprascrie baza vie) ultimul backup (sau cel indicat explicit) si ruleaza
|
||||
# PRAGMA integrity_check + un SELECT count(*) FROM submissions. Iese cu cod != 0 daca
|
||||
# ceva esueaza sau daca integritatea nu e "ok".
|
||||
#
|
||||
# Utilizare:
|
||||
# AUTOPASS_BACKUP_PASSPHRASE_FILE=/run/secrets/backup_pass tools/restore_check.sh
|
||||
# tools/restore_check.sh /data/backups/autopass-20260706-120000.db.gz.gpg
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="${AUTOPASS_BACKUP_DIR:-/data/backups}"
|
||||
|
||||
log() { echo "[restore_check] $*"; }
|
||||
err() { echo "[restore_check] EROARE: $*" >&2; }
|
||||
|
||||
CLEANUP_PATHS=()
|
||||
cleanup() {
|
||||
local p
|
||||
for p in "${CLEANUP_PATHS[@]:-}"; do
|
||||
[[ -n "$p" ]] && rm -rf "$p"
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -n "${AUTOPASS_BACKUP_PASSPHRASE_FILE:-}" ]]; then
|
||||
if [[ ! -f "$AUTOPASS_BACKUP_PASSPHRASE_FILE" ]]; then
|
||||
err "AUTOPASS_BACKUP_PASSPHRASE_FILE indica un fisier inexistent: $AUTOPASS_BACKUP_PASSPHRASE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
PASSPHRASE_FILE="$AUTOPASS_BACKUP_PASSPHRASE_FILE"
|
||||
elif [[ -n "${AUTOPASS_BACKUP_PASSPHRASE:-}" ]]; then
|
||||
PASSPHRASE_FILE="$(mktemp)"
|
||||
chmod 600 "$PASSPHRASE_FILE"
|
||||
CLEANUP_PATHS+=("$PASSPHRASE_FILE")
|
||||
printf '%s' "$AUTOPASS_BACKUP_PASSPHRASE" > "$PASSPHRASE_FILE"
|
||||
else
|
||||
err "lipseste parola de decriptare."
|
||||
err "seteaza AUTOPASS_BACKUP_PASSPHRASE_FILE (recomandat) sau AUTOPASS_BACKUP_PASSPHRASE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" != "" ]]; then
|
||||
TARGET="$1"
|
||||
else
|
||||
TARGET="$(ls -1t "$BACKUP_DIR"/autopass-*.db.gz.gpg 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$TARGET" || ! -f "$TARGET" ]]; then
|
||||
err "niciun backup gasit de verificat (director: $BACKUP_DIR)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "verific: $TARGET"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
CLEANUP_PATHS+=("$TMP_DIR")
|
||||
GZ_FILE="$TMP_DIR/snapshot.db.gz"
|
||||
DB_FILE="$TMP_DIR/snapshot.db"
|
||||
|
||||
log "decriptez (gpg)"
|
||||
gpg --batch --yes --decrypt --passphrase-file "$PASSPHRASE_FILE" \
|
||||
--output "$GZ_FILE" "$TARGET"
|
||||
|
||||
log "dezarhivez (gzip)"
|
||||
gzip -d "$GZ_FILE"
|
||||
|
||||
if [[ ! -s "$DB_FILE" ]]; then
|
||||
err "fisierul restaurat e gol sau lipseste: $DB_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "PRAGMA integrity_check + count(submissions)"
|
||||
set +e
|
||||
RESULT="$(python3 - "$DB_FILE" <<'PYEOF'
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
conn = sqlite3.connect(sys.argv[1])
|
||||
try:
|
||||
integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
|
||||
try:
|
||||
count = conn.execute("SELECT count(*) FROM submissions").fetchone()[0]
|
||||
except sqlite3.OperationalError as exc:
|
||||
print(f"integrity={integrity}")
|
||||
print(f"eroare_count_submissions={exc}")
|
||||
sys.exit(1)
|
||||
print(f"integrity={integrity}")
|
||||
print(f"count_submissions={count}")
|
||||
finally:
|
||||
conn.close()
|
||||
PYEOF
|
||||
)"
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$RESULT"
|
||||
|
||||
if [[ $STATUS -ne 0 ]]; then
|
||||
err "verificare esuata (vezi mesajul de mai sus)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "^integrity=ok$" <<<"$RESULT"; then
|
||||
err "PRAGMA integrity_check NU a raportat 'ok'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "backup valid: restaurabil, integritate ok."
|
||||
Reference in New Issue
Block a user