Nimbin[12]?Web & Tools / Git-Server / commits / 1cd70f9

Git-Server git · main

gitsync + this read-only git browser

git php bash apache self-hosted · first commit 2026-10-05 · last commit 2026-10-06 (3 days ago) · synced 3 days ago

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.gz

Fix backup root

Christian Immanuel · 2026-10-06 10:05 · 1cd70f917afa0159ee9c0887f928d2fe1be2eb32

 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';