Git-Server git · main
gitsync + this read-only git browser
PHP 77.1% CSS 11.4% Markdown 8.6% Makefile 2.9%git clone https://git.christianimmanuel.de/web-tools/Git-Server.gitwget https://git.christianimmanuel.de/web-tools/Git-Server/archive/Git-Server.tar.gzFix backup root
README.md | 17 +++++++++++++++- gitsync | 64 +++++++++++++++++++++++++++++++++++++++++++++-------------- gitsync.conf | 3 +++ web/index.php | 2 +- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0bd5eda..6aafc9d 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,22 @@ Publish exactly the files an `lfs-backup` config lists, always private: - Layout on the site: `SECTION/path`, e.g. `user-shell/.bashrc`, `system-boot/etc/fstab`. - Globs, directories, symlinks and `!` exclusions work like in `lfs-backup`. -- Secret files (private keys, `psk=`/`password=` lines) are left out and listed. `-f` takes them, but only from your own files. +- Secret files (private keys, `psk=`/`password=` lines) are left out, unless `SECRETS_PASS=` is set (below). + +**Secrets, encrypted.** With `SECRETS_PASS=~/.config/gitsync/secrets.pass` in the config, secret files go up +encrypted (openssl, AES-256, PBKDF2) as `secrets-user.tar.enc` (your files) and `secrets-root.tar.enc` (root/system files). +The server never sees them in plain; the web view shows them as binary files. + +```sh +(umask 077; openssl rand -base64 32 > ~/.config/gitsync/secrets.pass) # one line; keep a copy elsewhere! +``` + +Restore on another system: + +```sh +wget --user=NAME --ask-password https://git.christianimmanuel.de/private/private/lfs-backup/raw/secrets-root.tar.enc +openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in secrets-root.tar.enc | tar -xv # asks for the passphrase +``` Files only root can read (`[root:*]`, `/etc/rc.d/rc.iptables`, …) are collected as root: diff --git a/gitsync b/gitsync index e371f59..3331a58 100755 --- a/gitsync +++ b/gitsync @@ -3,10 +3,11 @@ # 100% Vibecode but tested. set -eu -VERSION="1.11.1" +VERSION="1.12.0" CONF="${GITSYNC_CONF:-$HOME/.config/gitsync/gitsync.conf}" STAGE="${GITSYNC_STAGE:-$HOME/.cache/gitsync}" -DRYRUN=0 LOCAL=0 VERBOSE=0 FORCE=0 LIST=0 QUIET=0 ROOTNOW=0 SU_TRIED=0 SSH_KEY="" COLLECT_ROOT="" +DRYRUN=0 LOCAL=0 VERBOSE=0 FORCE=0 LIST=0 QUIET=0 ROOTNOW=0 SU_TRIED=0 SSH_KEY="" COLLECT_ROOT="" SECRETS_PASS="" +TMPS=(); trap 'rm -rf "${TMPS[@]:-}"' EXIT SELF=$(readlink -f "$0") # always the real git binary, never a shell alias/function/wrapper (GIT= in config overrides) GIT=$(command -v /usr/bin/git || command -v /bin/git || echo git) @@ -38,7 +39,9 @@ A line !NAME < /etc/pkgusr/backup.conf publishes the files an lfs-backup confi Optional: SSH_KEY=~/.ssh/gitsync_ed25519 (key for the upload, no password prompt), SCRIPTS=~/Bash-Public (script collection, subdirs = groups), EXCLUDE=a,b (extra excludes), MAX_SIZE=50m (skip bigger files), - STAGE=~/other/dir (staging dir, default ~/.cache/gitsync) + STAGE=~/other/dir (staging dir, default ~/.cache/gitsync), + SECRETS_PASS=~/.config/gitsync/secrets.pass (backup.conf secrets go up + encrypted as secrets-*.tar.enc instead of being left out) USAGE } @@ -152,12 +155,29 @@ is_secret() { # file grep -qiIE -e "$SECRET_RE" -e '^[[:space:]]*(psk|password|private_key_passwd)[[:space:]]*=' "$1" 2>/dev/null } has_tty() { [ -t 2 ] && { : </dev/tty; } 2>/dev/null; } -cache_fresh() { # tarball backup.conf: younger than a day and newer than the config - [ -r "$1" ] && [ "$1" -nt "$2" ] && [ $(( $(date +%s) - $(stat -c %Y "$1") )) -lt 86400 ] +cache_fresh() { # tarball backup.conf: younger than a day and newer than the config (and passphrase) + [ -r "$1" ] && [ "$1" -nt "$2" ] && [ $(( $(date +%s) - $(stat -c %Y "$1") )) -lt 86400 ] \ + && { [ -z "$SECRETS_PASS" ] || [ ! -e "$SECRETS_PASS" ] || [ "$1" -nt "$SECRETS_PASS" ]; } +} + +# secret files go into B_SECDIR (if set) and are sealed into DST/secrets-LABEL.tar.enc +ENC="enc -aes-256-cbc -pbkdf2 -iter 600000" +secrets_dir() { # -> sets B_SECDIR when SECRETS_PASS is usable + B_SECDIR="" + [ -n "$SECRETS_PASS" ] || return 0 + [ -s "$SECRETS_PASS" ] && [ -r "$SECRETS_PASS" ] || { warn "SECRETS_PASS: $SECRETS_PASS missing or empty, secrets left out"; return 0; } + command -v openssl >/dev/null || { warn "openssl not found, secrets left out"; return 0; } + B_SECDIR=$(mktemp -d); TMPS+=("$B_SECDIR") +} +seal_secrets() { # dst label + [ -n "$B_SECDIR" ] && [ "${#B_SECRETS[@]}" -gt 0 ] || return 0 + # shellcheck disable=SC2086 + tar -C "$B_SECDIR" -cf - . | openssl $ENC -salt -pass "file:$SECRETS_PASS" -out "$1/secrets-$2.tar.enc" + chmod 600 "$1/secrets-$2.tar.enc"; rm -rf "$B_SECDIR" } collect_backup() { # conf dst kinds(all|user|rootsys) -> appends to B_SECRETS / B_UNREAD - local conf="$1" dst="$2" kinds="$3" line sec="" home="" rhome p m f rel pat d skip ex=() force="$FORCE" + local conf="$1" dst="$2" kinds="$3" line sec="" home="" rhome p m f rel pat d skip out ex=() force="$FORCE" [ "$kinds" = rootsys ] && force=0 # the root side never hands out secrets rhome=$(getent passwd root | cut -d: -f6); rhome="${rhome:-/root}" while IFS= read -r line || [ -n "$line" ]; do @@ -195,10 +215,13 @@ collect_backup() { # conf dst kinds(all|user|rootsys) -> appends to B_SECRETS / done [ "$skip" = 1 ] && continue if [ ! -r "$f" ]; then B_UNREAD+=("$f"); continue; fi - if [ "$force" = 0 ] && is_secret "$f"; then B_SECRETS+=("$f"); continue; fi + out="$dst" + if [ "$force" = 0 ] && is_secret "$f"; then + B_SECRETS+=("$f"); [ -n "$B_SECDIR" ] || continue; out="$B_SECDIR" + fi rel="${f#"$home"/}"; rel="${rel#/}" - mkdir -p "$dst/$sec/$(dirname "$rel")" - cp -L --preserve=timestamps "$f" "$dst/$sec/$rel" + mkdir -p "$out/$sec/$(dirname "$rel")" + cp -L --preserve=timestamps "$f" "$out/$sec/$rel" done < <(find -L "$m" -type f -print0 2>/dev/null) done <<< "$m" ;; esac @@ -206,8 +229,11 @@ collect_backup() { # conf dst kinds(all|user|rootsys) -> appends to B_SECRETS / shopt -u dotglob } -report_backup() { # name hint - [ "$QUIET" = 1 ] || [ "${#B_SECRETS[@]}" = 0 ] || warn "$1: ${#B_SECRETS[@]} secret file(s) left out: $(printf '%s\n' "${B_SECRETS[@]}" | sed "s|^$HOME/|~/|" | paste -sd' ' -)" +report_backup() { # name hint [sealed-file] + if [ "${#B_SECRETS[@]}" -gt 0 ] && [ -n "${3:-}" ]; then log " ${#B_SECRETS[@]} secret file(s) encrypted into $3" + elif [ "$QUIET" = 0 ] && [ "${#B_SECRETS[@]}" -gt 0 ]; then + warn "$1: ${#B_SECRETS[@]} secret file(s) left out (SECRETS_PASS= uploads them encrypted): $(printf '%s\n' "${B_SECRETS[@]}" | sed "s|^$HOME/|~/|" | paste -sd' ' -)" + fi [ "${#B_UNREAD[@]}" = 0 ] || warn "$1: not readable as $(id -un), left out: $(printf '%s\n' "${B_UNREAD[@]}" | sed "s|^$HOME/|~/|" | paste -sd' ' -)$2" } @@ -223,8 +249,11 @@ sync_backup() { # conf dst desc tags name if cache_fresh "$cache" "$conf" && tar -C "$dst" --strip-components=1 -xf "$cache" "$name" 2>/dev/null; then kinds=user; log " root/system files collected $(date -r "$cache" '+%F %H:%M')" fi + secrets_dir collect_backup "$conf" "$dst" "$kinds" - report_backup "$name" "$([ "$kinds" = all ] && echo ' (cron collects them as root; in a terminal: gitsync -r)')" + seal_secrets "$dst" user + report_backup "$name" "$([ "$kinds" = all ] && echo ' (cron collects them as root; in a terminal: gitsync -r)')" \ + "$([ -f "$dst/secrets-user.tar.enc" ] && echo secrets-user.tar.enc)" write_meta "$dst/.gitsync.meta" "$3" "$4" "" \ "$(find "$dst" -type f -not -name .gitsync.meta -printf '%T@\n' | sort -n | head -n1 | cut -d. -f1)" \ "$(find "$dst" -type f -not -name .gitsync.meta -printf '%T@\n' | sort -n | tail -n1 | cut -d. -f1)" \ @@ -245,8 +274,10 @@ collect_root() { # USER: as root, collect [root:*]/[system:*] files of USER's ba [ -L "$CACHE_DIR" ] && die "$CACHE_DIR is a symlink" install -d -m 755 -o root -g root "$CACHE_DIR" owned_by_root "$CACHE_DIR" || die "$CACHE_DIR must be owned by root and not writable by others" - CR_TMP=$(mktemp -d); trap 'rm -rf "$CR_TMP"' EXIT - B_SECRETS=() B_UNREAD=() + CR_TMP=$(mktemp -d); TMPS+=("$CR_TMP") + # passphrase file named in the user's config (only used as the key, never published) + SECRETS_PASS=$(sed -n 's/^[[:space:]]*SECRETS_PASS=//p' "$conf" | tail -n1); SECRETS_PASS=$(trim "${SECRETS_PASS%%#*}") + SECRETS_PASS="${SECRETS_PASS/#\~/$uhome}" while IFS= read -r line || [ -n "$line" ]; do line="${line%%#*}"; line=$(trim "$line"); line="${line#!}" path=$(trim "${line%%|*}"); [[ "$path" == *"<"* ]] || continue @@ -259,9 +290,11 @@ collect_root() { # USER: as root, collect [root:*]/[system:*] files of USER's ba for p in "$bconf" "$(dirname "$bconf")"; do owned_by_root "$p" || { warn "$name skipped: $p must be owned by root and not writable by others ($(stat -c '%U:%G %A' "$p"))"; continue 2; } done + B_SECRETS=() B_UNREAD=(); secrets_dir mkdir -p "$CR_TMP/$name"; collect_backup "$bconf" "$CR_TMP/$name" rootsys; n=$((n + 1)) + seal_secrets "$CR_TMP/$name" root + report_backup "$name (root)" "" "$([ -f "$CR_TMP/$name/secrets-root.tar.enc" ] && echo secrets-root.tar.enc)" done < "$conf" - report_backup "root" "" if [ "$n" = 0 ]; then rm -f "$CACHE_DIR/$u.tar"; log "no backup.conf projects for $u"; return 0; fi (cd "$CR_TMP" && tar -cf "$CACHE_DIR/.$u.tar.new" -- *) chown "$u" "$CACHE_DIR/.$u.tar.new"; chmod 600 "$CACHE_DIR/.$u.tar.new" @@ -286,6 +319,7 @@ while IFS= read -r line || [ -n "$line" ]; do GIT=*) GIT="${line#GIT=}" ;; SSH_KEY=*) SSH_KEY="${line#SSH_KEY=}"; SSH_KEY="${SSH_KEY/#\~/$HOME}" ;; SCRIPTS=*) SCRIPTS="${line#SCRIPTS=}"; SCRIPTS="${SCRIPTS/#\~/$HOME}" ;; + SECRETS_PASS=*) SECRETS_PASS="${line#SECRETS_PASS=}"; SECRETS_PASS="${SECRETS_PASS/#\~/$HOME}" ;; STAGE=*) STAGE="${line#STAGE=}"; STAGE="${STAGE/#\~/$HOME}"; mkdir -p "$STAGE" ;; EXCLUDE=*) EXCLUDES="$EXCLUDES $(printf '%s' "${line#EXCLUDE=}" | tr ',' ' ')" ;; \[*\]) diff --git a/gitsync.conf b/gitsync.conf index 6535d37..7cd3af7 100644 --- a/gitsync.conf +++ b/gitsync.conf @@ -18,6 +18,9 @@ MAX_SIZE=50m SCRIPTS=~/Bash-Public # staging dir, default ~/.cache/gitsync - move it if your home is small #STAGE=~/.cache/gitsync +# backup.conf projects: secret files (keys, wifi passwords) go up encrypted (openssl) instead of +# being left out. One line passphrase; keep a copy elsewhere, without it they are lost. +SECRETS_PASS=~/.config/gitsync/secrets.pass [Embedded] ~/Git/snake_compiler | Snake compiled to a bare-metal RISC-V microcontroller | riscv, embedded, bare-metal, c, snake diff --git a/web/index.php b/web/index.php index ca5755c..b9d3cbe 100644 --- a/web/index.php +++ b/web/index.php @@ -1,7 +1,7 @@ <?php declare(strict_types=1); /* git.christianimmanuel.de - read-only git browser. 100% Vibecode but tested. Works in lynx. */ -const VERSION = '1.11.1'; +const VERSION = '1.12.0'; const SITE = 'Nimbin[12]?'; const LEGAL = 'https://christianimmanuel.de'; $ROOT = __DIR__ . '/repos';