First Commit

This commit is contained in:
Lonny Lowe 2026-09-24 11:29:54 -05:00
commit 2e5a4751a7
30 changed files with 2517 additions and 0 deletions

74
scripts/backup.sh Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# =====================================================================================
# Online full-domain backup (AD database + SYSVOL + secrets) via `samba-tool domain backup`.
# Copied out of the container into ./backups/<timestamp>.tgz and checksummed.
#
# ./scripts/backup.sh # uses $AD_CONTAINER / .env
# KEEP=14 ./scripts/backup.sh # prune to the newest 14 backups
#
# A backup you have not restored is a rumour. Restore drill: ./scripts/restore.sh
# =====================================================================================
set -euo pipefail
cd "$(dirname "$0")/.."
. "$(dirname "$0")/envfile.sh"; ad_env_load .env # parsed, never executed (see envfile.sh)
CID="${AD_CONTAINER:-ad-dc}"
REALM="${AD_REALM:?}"
HOST="${AD_HOSTNAME:-dc1}"
ZONE="${AD_DNS_DOMAIN:?}"
FQDN="${HOST}.${ZONE}"
CC=/tmp/backup_ccache
DEST="${AD_BACKUP_DIR:-backups}"
KEEP="${KEEP:-14}"
PW="${AD_ADMIN_PASSWORD:-}"
[ -z "$PW" ] && [ -r secrets/ad_admin.password ] && PW="$(tr -d '\r\n' < secrets/ad_admin.password)"
[ -n "$PW" ] || { echo "no administrator password available" >&2; exit 2; }
docker inspect -f '{{.State.Status}}' "$CID" | grep -q running || { echo "$CID is not running" >&2; exit 1; }
mkdir -p "$DEST"
# Preflight: a backup that dies after 10 minutes of work is worse than one that never starts.
# Docker creates bind-mount source dirs as root, and `docker cp` extracts to the host as the
# *invoking* user - so a root-owned backups/ fails with a cryptic "openat: permission denied".
if [ ! -w "$DEST" ]; then
echo "!! backup dir '$DEST' is not writable by $(whoami) (owned $(stat -c '%U:%G %a' "$DEST"))" >&2
echo " typically created root-owned by Docker for a bind mount. Fix with:" >&2
echo " sudo chown $(whoami) \"$DEST\"" >&2
exit 1
fi
ts="$(date -u +%Y%m%dT%H%M%SZ)"
echo ">> backing up ${FQDN} (online, consistent snapshot) -> ${DEST}/"
printf '%s\n' "$PW" | docker exec -i -e KRB5CCNAME="FILE:${CC}" -e AD_FQDN="$FQDN" "$CID" \
bash -euo pipefail -c '
kinit "${AD_ADMIN_USER:-administrator}@${AD_REALM}"
samba-tool domain backup online \
--server="$AD_FQDN" \
--targetdir=/var/backups/ad-dc \
--use-krb5-ccache="$KRB5CCNAME"
'
# Samba names the archive samba-backup-<domain>-<ts>.tar.bz2 (older builds: .tgz)
newest="$(docker exec "$CID" bash -c 'ls -1t /var/backups/ad-dc/*.tar.bz2 /var/backups/ad-dc/*.tgz 2>/dev/null | head -1')"
[ -n "$newest" ] || { echo "backup produced no archive" >&2; exit 1; }
docker cp "${CID}:${newest}" "${DEST}/${ts}-$(basename "$newest")"
out="${DEST}/${ts}-$(basename "$newest")"
( cd "$DEST" && sha256sum "$(basename "$out")" > "$(basename "$out").sha256" )
# Don't leave archives inside the container layer - they survive restarts and silently grow it.
docker exec "$CID" rm -f "$newest" 2>/dev/null || true
size="$(du -h "$out" | cut -f1)"
echo ">> wrote $out ($size)"
echo " checksum: $(cat "$out.sha256")"
# rotation
mapfile -t existing < <(ls -1t "$DEST"/*.tar.bz2 "$DEST"/*.tgz 2>/dev/null || true)
if [ "${#existing[@]}" -gt "$KEEP" ]; then
for old in "${existing[@]:$KEEP}"; do
echo " pruning $old"
rm -f "$old" "$old.sha256"
done
fi
echo ">> done. Copy $out off this host (backup of a backup) and keep the AD_DB key safe."