-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.sh
More file actions
executable file
·50 lines (42 loc) · 1.62 KB
/
Copy pathbackup.sh
File metadata and controls
executable file
·50 lines (42 loc) · 1.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env bash
# Encrypted Ghostbit backup.
#
# Streams `python -m app.admin export` through age and writes one
# timestamped file per run to $BACKUP_DIR. The stream goes through a
# pipe end-to-end so the plaintext export is never materialised on
# disk — a stolen backup file is useless without the age recipient's
# private key.
#
# Usage (one-shot):
# BACKUP_DIR=/var/backups/ghostbit \
# AGE_RECIPIENT="age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
# scripts/backup.sh
#
# As a recurring job: see scripts/ghostbit-backup.{service,timer}.
#
# Restore:
# age --decrypt -i ~/.config/age/keys.txt backup-2026-05-24T03:00.jsonl.age \
# | python -m app.admin import
#
# Why age rather than gpg: age has a single recipient flag, no key-server
# nonsense, no asymmetric-vs-symmetric mode footgun, and the file format
# is small and forward-compatible. gpg works too — swap the encrypt
# command if your ops standardises on it.
set -euo pipefail
: "${BACKUP_DIR:?BACKUP_DIR must be set (e.g. /var/backups/ghostbit)}"
: "${AGE_RECIPIENT:?AGE_RECIPIENT must be set (an age public key)}"
command -v age >/dev/null || {
echo "age is not installed. https://age-encryption.org" >&2
exit 1
}
mkdir -p "$BACKUP_DIR"
# UTC + ISO-ish, sortable, safe in filenames.
TS=$(date -u +%Y-%m-%dT%H-%M-%SZ)
OUT="$BACKUP_DIR/ghostbit-$TS.jsonl.age"
TMP="$OUT.partial"
# Pipefail catches a failure on either side of the pipe.
python -m app.admin export | age -r "$AGE_RECIPIENT" -o "$TMP"
# Atomic rename so a partially-written file can never be picked up as a
# "good" backup by a downstream rotation/sync step.
mv "$TMP" "$OUT"
echo "$OUT"