Skip to content

Instantly share code, notes, and snippets.

@pgaskin
Last active November 16, 2023 17:16
Show Gist options
  • Save pgaskin/0c214a30ff70fc8d9841cdeceab8ddd7 to your computer and use it in GitHub Desktop.
Save pgaskin/0c214a30ff70fc8d9841cdeceab8ddd7 to your computer and use it in GitHub Desktop.

setup

mkdir /opt/rsync-backup
cd /opt/rsync-backup
go mod init rsync-backup
go mod tidy
go build .
mkdir -p /etc/sysusers.d /etc/tmpfiles.d
ln -sf /opt/rsync-backup/rsync-backup.sysusers /etc/sysusers.d/rsync-backup.conf
ln -sf /opt/rsync-backup/rsync-backup.tmpfiles /etc/tmpfiles.d/rsync-backup.conf
ln -sf /opt/rsync-backup/rsync-backup.sshd_config /etc/ssh/sshd_config.d/rsync-backup.conf
systemd-sysusers /etc/sysusers.d/rsync-backup.conf
systemd-tmpfiles --create /etc/tmpfiles.d/rsync-backup.conf
systemctl restart ssh
chmod 550 rsync-backup
chown rsync-backup:rsync-backup rsync-backup
setcap CAP_DAC_READ_SEARCH+p rsync-backup
echo '...' >> authorized_keys

rsync

./rsync \
	--human-readable --info=progress2 --info=stats2 --info=name0 \
	--compress \
	--recursive --one-file-system --delete --delete-excluded \
	--sparse --links --perms --times --xattrs \
	--group --owner --devices --specials --acls --numeric-ids \
	--filter=". rsync-filter" --exclude='*' \
	HOSTNAME:/ /path/to/backup/

rsync + btrfs + snappr

export TZ=America/New_York

for x in 1 2 3
do ./rsync \
        --itemize-changes --info=stats2 --human-readable \
        --compress \
        --recursive --one-file-system --delete --delete-excluded \
        --sparse --links --perms --times --xattrs \
        --group --owner --devices --specials --acls --numeric-ids \
        --filter=". HOSTNAME.filter" --exclude='*' \
        HOSTNAME:/ HOSTNAME/
done

mkdir -p HOSTNAME.snapshots
btrfs subvolume snapshot -r HOSTNAME HOSTNAME.snapshots/$(date +%Y-%m-%d_%H:%M:%S)
btrfs subvolume list -r . |
go run github.com/pgaskin/snappr/cmd/snappr@latest -sw \
	-z "local" \
	-oe "path HOSTNAME\.snapshots/([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]:[0-9][0-9]:[0-9][0-9])\$" \
        -qp "2006-01-02_15:04:05" \
        1@last 6@secondly:1h 7@daily 4@daily:7 6@monthly 6@monthly:2 yearly |
cut -d ' ' -f 2- |
xargs --no-run-if-empty btrfs subvolume delete
#!/bin/sh
id_rsa="$(set -e && cd "$(set -e; dirname -- "$0")" && readlink -e id_rsa)" && test -r "$id_rsa" || { echo "$0: failed to read ssh key" >&2; exit 126; }
ssh="ssh -i ${id_rsa@Q} -l rsync-backup -o StrictHostKeyChecking=no"
exec rsync -e "$ssh" "$@"
package main
import (
"fmt"
"os"
"os/exec"
"slices"
"strings"
"syscall"
"unsafe"
"github.com/buildkite/shellwords"
)
func main() {
if err := os.Chdir("/"); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(126)
}
var st syscall.Stat_t
if err := syscall.Stat("/proc/self/exe", &st); err != nil {
fmt.Fprintf(os.Stderr, "%s: failed to stat wrapper: %v\n", os.Args[0], err)
os.Exit(126)
}
if st.Mode&^0o0100550 != 0 { // S_IFREG | S_IWUSR | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP
fmt.Fprintf(os.Stderr, "%s: wrapper permissions are too open: must not be greater than -r-xr-x--- (550)\n", os.Args[0])
os.Exit(126)
}
if err := usecap(2); err != nil { // CAP_DAC_READ_SEARCH
fmt.Fprintf(os.Stderr, "%s: failed to use capability CAP_DAC_READ_SEARCH: %v\n", os.Args[0], err)
os.Exit(126)
}
if cmd := os.Getenv("SSH_ORIGINAL_COMMAND"); cmd == "" {
fmt.Fprintf(os.Stderr, "%s: must be run over ssh\n", os.Args[0])
os.Exit(126)
} else if err := rrsync(cmd); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v (SSH_ORIGINAL_COMMAND: %q)\n", os.Args[0], err, cmd)
os.Exit(126)
}
}
func rrsync(cmd string) error {
// based on https://download.samba.org/pub/rsync/rrsync.1
argv, err := shellwords.SplitPosix(cmd)
if err != nil {
return err
}
if len(argv) <= 0 || argv[0] != "rsync" {
return fmt.Errorf("only rsync is allowed")
}
if len(argv) <= 1 || argv[1] != "--server" {
return fmt.Errorf("only rsync server is allowed")
}
if len(argv) <= 2 || argv[2] != "--sender" {
return fmt.Errorf("sending to read-only server is not allowed")
}
if dotarg := slices.Index(argv, "."); dotarg >= 0 {
argv = slices.Insert(argv, dotarg, "--") // forcefully terminate option parsing
} else {
return fmt.Errorf("rsync server command line missing dot argument")
}
for _, arg := range argv[3:] {
if arg == "--" {
break
}
if arg[0] != '-' {
continue
}
if arg[1] != '-' {
const (
stateOption = iota
stateNumber
stateE
)
var state int
opt:
for i, opt := range []byte(arg) {
if i == 0 {
continue
}
switch state {
case stateOption:
switch {
case slices.Contains([]byte("ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz"), opt): // short options without args
if opt != 's' {
continue opt
}
case slices.Contains([]byte("@B"), opt): // short options with number
state = stateNumber
continue opt
case opt == 'e': // short e option (value: [0-9]*[.].*)
state = stateE
continue opt
}
return fmt.Errorf("option -%c is not allowed", opt)
case stateNumber:
if opt >= '0' && opt <= '9' {
continue opt
}
case stateE:
if opt >= '0' && opt <= '9' {
continue opt
}
if opt == '.' {
break opt
}
}
return fmt.Errorf("invalid argument %s", arg)
}
continue
}
switch tmp, _, _ := strings.Cut(arg[2:], "="); tmp {
case "append", "backup-dir", "block-size", "bwlimit", "checksum-choice",
"checksum-seed", "compare-dest", "compress-choice", "compress-level",
"copy-dest", "copy-unsafe-links", "debug", "delay-updates", "delete",
"delete-after", "delete-before", "delete-delay", "delete-during",
"delete-excluded", "delete-missing-args", "existing", "fake-super",
"files-from", "force", "from0", "fsync", "fuzzy", "group", "groupmap",
"hard-links", "iconv", "ignore-errors", "ignore-existing",
"ignore-missing-args", "ignore-times", "info", "inplace", "link-dest",
"links", "list-only", "log-file", "log-format", "max-alloc", "max-delete",
"max-size", "min-size", "mkpath", "modify-window", "msgs2stderr",
"munge-links", "new-compress", "no-W", "no-implied-dirs", "no-msgs2stderr",
"no-r", "no-relative", "no-specials", "numeric-ids", "old-compress",
"one-file-system", "only-write-batch", "open-noatime", "owner",
"partial", "partial-dir", "perms", "preallocate", "recursive",
"remove-sent-files", "remove-source-files", "safe-links", "sender",
"server", "size-only", "skip-compress", "specials", "stats", "stderr",
"suffix", "super", "temp-dir", "timeout", "times", "use-qsort", "usermap":
default:
return fmt.Errorf("option %s is not allowed", arg)
}
if arg == "--log-file" || strings.HasPrefix(arg, "--delete") || strings.HasPrefix(arg, "--remove") {
return fmt.Errorf("option %s is not allowed on read-only server", arg)
}
}
rsync, err := exec.LookPath("rsync")
if err != nil {
return err
}
return syscall.Exec(rsync, argv, os.Environ())
}
func usecap(cap uint) error {
// https://man7.org/linux/man-pages/man2/capset.2.html
var caps struct {
hdr struct {
version uint32
pid int
}
data [2]struct {
effective uint32
permitted uint32
inheritable uint32
}
}
caps.hdr.version = 0x20080522 // _LINUX_CAPABILITY_VERSION_3
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&caps)), uintptr(unsafe.Pointer(&caps.data[0])), 0); errno != 0 {
return fmt.Errorf("capget: %w", errno)
}
if caps.data[0].permitted&(1<<cap) == 0 {
return fmt.Errorf("capability %d is not permitted (try setcap %d+p ...)", cap, cap)
}
caps.data[0].effective |= 1 << cap
caps.data[0].inheritable |= 1 << cap
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&caps.hdr)), uintptr(unsafe.Pointer(&caps.data[0])), 0); errno != 0 {
return fmt.Errorf("capset: %w", errno)
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, 47, 2, uintptr(cap)); errno != 0 {
return fmt.Errorf("prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, %d): %w", cap, errno)
}
return nil
}
Match User rsync-backup
PasswordAuthentication no
PubkeyAuthentication yes
DisableForwarding yes
PermitUserRC no
PermitTTY no
PermitTunnel no
AuthorizedKeysFile /opt/rsync-backup/authorized_keys
ForceCommand /opt/rsync-backup/rsync-backup
u rsync-backup - "rsync backup" /opt/rsync-backup /bin/sh
f /opt/rsync-backup/authorized_keys 0600 rsync-backup rsync-backup -
+ /opt/***
+ /etc/***
+ /var/
+ /var/lib/***
+ /var/www/***
+ /var/backups/***
+ /home/***
+ /root/***
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment