Browse Source
new: [rsync-backup-target] allow dynamic management of backup keys
new: [rsync-backup-target] allow dynamic management of backup keys
Signed-off-by: Valentin Lab <valentin.lab@kalysto.org>upd-docker
Valentin Lab
4 years ago
10 changed files with 337 additions and 21 deletions
-
84rsync-backup-target/README.org
-
13rsync-backup-target/build/entrypoint.sh
-
1rsync-backup-target/build/src/etc/sudoers.d/rsync
-
79rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate
-
108rsync-backup-target/build/src/usr/local/sbin/ssh-key
-
41rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys
-
11rsync-backup-target/hooks/init
-
16rsync-backup-target/hooks/log_rotate-relation-joined
-
1rsync-backup-target/metadata.yml
-
4rsync-backup-target/resources/bin/compose-add-rsync-key
@ -0,0 +1,84 @@ |
|||||
|
#+PROPERTY: Effort_ALL 0 0:30 1:00 2:00 0.5d 1d 1.5d 2d 3d 4d 5d |
||||
|
#+PROPERTY: Max_effort_ALL 0 0:30 1:00 2:00 0.5d 1d 1.5d 2d 3d 4d 5d |
||||
|
#+PROPERTY: header-args:python :var filename=(buffer-file-name) |
||||
|
#+PROPERTY: header-args:sh :var filename=(buffer-file-name) |
||||
|
#+TODO: TODO WIP BLOCKED | DONE CANCELED |
||||
|
#+LATEX_HEADER: \usepackage[margin=0.5in]{geometry} |
||||
|
#+LaTeX_HEADER: \hypersetup{linktoc = all, colorlinks = true, urlcolor = DodgerBlue4, citecolor = PaleGreen1, linkcolor = blue} |
||||
|
#+LaTeX_CLASS: article |
||||
|
#+OPTIONS: H:8 ^:nil prop:("Effort" "Max_effort") tags:not-in-toc |
||||
|
#+COLUMNS: %50ITEM %Effort(Min Effort) %Max_effort(Max Effort) |
||||
|
|
||||
|
#+TITLE: rsync-backup-target |
||||
|
|
||||
|
#+LATEX: \pagebreak |
||||
|
|
||||
|
Usage of this service |
||||
|
|
||||
|
#+LATEX: \pagebreak |
||||
|
|
||||
|
#+LATEX: \pagebreak |
||||
|
|
||||
|
|
||||
|
* Configuration example |
||||
|
|
||||
|
|
||||
|
#+begin_src yaml |
||||
|
rsync-backup-target: |
||||
|
# docker-compose: |
||||
|
# ports: |
||||
|
# - "10023:22" |
||||
|
options: |
||||
|
admin: ## These keys are for the allowed rsync-backup to write stuff with rsync |
||||
|
myadmin: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDESdz8bWtVcDQJ68IE/KpuZM9tAq\ |
||||
|
ZDXGbvEVnTg16/yWqBGQg0QZdDjISsPn7D3Zr64g2qgD9n7EZghfGP9TkitvfrBYx8p\ |
||||
|
7JkkUyt8nxklwOlKZFD5b3PF2bHloSsmjnP8ZMp5Ar7E+tn1guGrCrTcFIebpVGR3qF\ |
||||
|
hRN9AlWNR+ekWo88ZlLJIrqD26jbWRJZm4nPCgqwhJwfHE3aVwfWGOqjSp4ij+jr2ac\ |
||||
|
Arg7eD4clBPYIqKlqbfNRD5MFAH9sbB6jkebQCAUwNRwV7pKwCEt79HnCMoMjnZh6Ww\ |
||||
|
6TlHIFw936C2ZiTBuofMx7yoAeqpifyzz/T5wsFLYWwSnX rsync@zen" |
||||
|
#+end_src |
||||
|
|
||||
|
** Adding new keys for backup |
||||
|
|
||||
|
This can be done through the admin accounts configured in =compose.yml=. |
||||
|
|
||||
|
You can use then =ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key=: |
||||
|
|
||||
|
#+begin_example |
||||
|
$ ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key ls |
||||
|
$ ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key add "ssh-rsa AAA...Jdhwhv rsync@sourcelabel" |
||||
|
$ ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key ls |
||||
|
..Jdhwhv sourcelabel |
||||
|
$ ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key rm sourcelabel |
||||
|
$ ssh myadmin@$RSYNC_BACKUP_TARGET ssh-key ls |
||||
|
$ |
||||
|
#+end_example |
||||
|
|
||||
|
|
||||
|
* Troubleshooting |
||||
|
|
||||
|
** Faking access from client |
||||
|
|
||||
|
This should work: |
||||
|
|
||||
|
#+begin_src sh |
||||
|
RSYNC_BACKUP_TARGET_IP=172.18.0.2 |
||||
|
rsync -azvA -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ |
||||
|
/tmp/toto "$RSYNC_BACKUP_TARGET":/var/mirror/client1 |
||||
|
#+end_src |
||||
|
|
||||
|
** Direct ssh access should be refused |
||||
|
|
||||
|
#+begin_src sh |
||||
|
RSYNC_BACKUP_TARGET_IP=172.18.0.2 |
||||
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ |
||||
|
"$RSYNC_BACKUP_TARGET" |
||||
|
#+end_src |
||||
|
|
||||
|
** Wrong directory should be refused |
||||
|
|
||||
|
#+begin_src sh |
||||
|
RSYNC_BACKUP_TARGET_IP=172.18.0.2 |
||||
|
rsync -azvA -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ |
||||
|
/tmp/toto "$RSYNC_BACKUP_TARGET":/var/mirror/client2 |
||||
|
#+end_src |
@ -0,0 +1,79 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
## Note that the shebang is not used, but it's the login shell that |
||||
|
## will execute this command. |
||||
|
|
||||
|
exname=$(basename "$0") |
||||
|
|
||||
|
mkdir -p /var/log/rsync |
||||
|
|
||||
|
LOG="/var/log/rsync/$exname.log" |
||||
|
|
||||
|
|
||||
|
ssh_connection=(${SSH_CONNECTION}) |
||||
|
SSH_SOURCE_IP="${ssh_connection[0]}:${ssh_connection[1]}" |
||||
|
|
||||
|
log() { |
||||
|
printf "%s [%s] %s - %s\n" \ |
||||
|
"$(date --rfc-3339=seconds)" "$$" "$SSH_SOURCE_IP" "$*" \ |
||||
|
>> "$LOG" |
||||
|
} |
||||
|
|
||||
|
log "NEW ADMIN CONNECTION" |
||||
|
|
||||
|
reject() { |
||||
|
log "REJECTED: $SSH_ORIGINAL_COMMAND" |
||||
|
# echo "ORIG: $SSH_ORIGINAL_COMMAND" >&2 |
||||
|
echo "Your command has been rejected and reported to sys admin." >&2 |
||||
|
exit 1 |
||||
|
} |
||||
|
|
||||
|
|
||||
|
if [[ "$SSH_ORIGINAL_COMMAND" =~ [\&\(\{\;\<\>\`\$\}] ]]; then |
||||
|
log "BAD CHARS DETECTED" |
||||
|
# echo "Bad chars: $SSH_ORIGINAL_COMMAND" >&2 |
||||
|
reject |
||||
|
fi |
||||
|
|
||||
|
if [[ "$SSH_ORIGINAL_COMMAND" =~ ^"ssh-key add ssh-rsa "[a-zA-Z0-9/+]+" "[a-zA-Z0-9._-]+"@"[a-zA-Z0-9._-]+""$ ]]; then |
||||
|
log "ACCEPTED: $SSH_ORIGINAL_COMMAND" |
||||
|
|
||||
|
## Interpret \ to allow passing spaces (want to avoid possible issue with \n) |
||||
|
#read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}" |
||||
|
ssh_args=(${SSH_ORIGINAL_COMMAND}) |
||||
|
|
||||
|
# echo "Would accept: $SSH_ORIGINAL_COMMAND" >&2 |
||||
|
exec sudo /usr/local/sbin/ssh-key "${ssh_args[@]:1}" |
||||
|
elif [[ "$SSH_ORIGINAL_COMMAND" =~ ^"ssh-key ls"$ ]]; then |
||||
|
log "ACCEPTED: $SSH_ORIGINAL_COMMAND" |
||||
|
|
||||
|
## Interpret \ to allow passing spaces (want to avoid possible issue with \n) |
||||
|
#read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}" |
||||
|
ssh_args=(${SSH_ORIGINAL_COMMAND}) |
||||
|
|
||||
|
# echo "Would accept: $SSH_ORIGINAL_COMMAND" >&2 |
||||
|
exec /usr/local/sbin/ssh-key "${ssh_args[@]:1}" |
||||
|
elif [[ "$SSH_ORIGINAL_COMMAND" =~ ^"ssh-key rm "[a-zA-Z0-9._-]+$ ]]; then |
||||
|
log "ACCEPTED: $SSH_ORIGINAL_COMMAND" |
||||
|
|
||||
|
## Interpret \ to allow passing spaces (want to avoid possible issue with \n) |
||||
|
#read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}" |
||||
|
ssh_args=(${SSH_ORIGINAL_COMMAND}) |
||||
|
|
||||
|
# echo "Would accept: $SSH_ORIGINAL_COMMAND" >&2 |
||||
|
exec sudo /usr/local/sbin/ssh-key "${ssh_args[@]:1}" |
||||
|
else |
||||
|
|
||||
|
log "NOT MATCHING ANY ALLOWED COMMAND" |
||||
|
reject |
||||
|
fi |
||||
|
|
||||
|
## For other commands, like `find` or `md5`, that could be used to |
||||
|
## challenge the backups and check that archive is actually |
||||
|
## functional, I would suggest to write a simple command that takes no |
||||
|
## arguments, so as to prevent allowing wildcards or suspicious |
||||
|
## contents. Letting `find` go through is dangerous for instance |
||||
|
## because of the `-exec`. And path traversal can be done also when |
||||
|
## allowing /my/path/* by using '..'. This is why a fixed purpose |
||||
|
## embedded executable will be much simpler to handle, and to be honest |
||||
|
## we don't need much more. |
@ -0,0 +1,108 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
RSYNC_KEY_PATH=/etc/rsync/keys |
||||
|
|
||||
|
|
||||
|
ANSI_ESC=$'\e[' |
||||
|
|
||||
|
NORMAL="${ANSI_ESC}0m" |
||||
|
|
||||
|
GRAY="${ANSI_ESC}1;30m" |
||||
|
RED="${ANSI_ESC}1;31m" |
||||
|
GREEN="${ANSI_ESC}1;32m" |
||||
|
YELLOW="${ANSI_ESC}1;33m" |
||||
|
BLUE="${ANSI_ESC}1;34m" |
||||
|
PINK="${ANSI_ESC}1;35m" |
||||
|
CYAN="${ANSI_ESC}1;36m" |
||||
|
WHITE="${ANSI_ESC}1;37m" |
||||
|
|
||||
|
DARKGRAY="${ANSI_ESC}0;30m" |
||||
|
DARKRED="${ANSI_ESC}0;31m" |
||||
|
DARKGREEN="${ANSI_ESC}0;32m" |
||||
|
DARKYELLOW="${ANSI_ESC}0;33m" |
||||
|
DARKBLUE="${ANSI_ESC}0;34m" |
||||
|
DARKPINK="${ANSI_ESC}0;35m" |
||||
|
DARKCYAN="${ANSI_ESC}0;36m" |
||||
|
DARKWHITE="${ANSI_ESC}0;37m" |
||||
|
|
||||
|
|
||||
|
ssh-key-ls() { |
||||
|
local f content |
||||
|
for f in "${RSYNC_KEY_PATH}"/backup/*.pub; do |
||||
|
[ -e "$f" ] || continue |
||||
|
ident=${f##*/} |
||||
|
ident=${ident%.pub} |
||||
|
content=$(cat "$f") |
||||
|
key=${content#* } |
||||
|
key=${key% *} |
||||
|
printf "${DARKGRAY}..${NORMAL}%24s ${DARKCYAN}%s${NORMAL}\n" "${key: -24}" "$ident" |
||||
|
done |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ssh-key-rm() { |
||||
|
local ident="$1" delete |
||||
|
|
||||
|
delete="${RSYNC_KEY_PATH}/backup/$ident.pub" |
||||
|
if ! [ -e "$delete" ]; then |
||||
|
echo "Error: key '$ident' not found." >&2 |
||||
|
return 1 |
||||
|
fi |
||||
|
rm "$delete" |
||||
|
|
||||
|
/usr/local/sbin/ssh-update-keys |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ssh-key-add() { |
||||
|
local type="$1" key="$2" email="$3" |
||||
|
|
||||
|
[ "$1" == "ssh-rsa" ] || { |
||||
|
echo "Error: expecting ssh-rsa key type" >&2 |
||||
|
return 1 |
||||
|
} |
||||
|
|
||||
|
## ident are unique by construction (they are struct keys) |
||||
|
## but keys need to be also unique |
||||
|
declare -A keys |
||||
|
mkdir -p "${RSYNC_KEY_PATH}/backup" |
||||
|
content="$type $key $email" |
||||
|
ident="${email##*@}" |
||||
|
target="${RSYNC_KEY_PATH}/backup/$ident.pub" |
||||
|
if [ -e "$target" ]; then |
||||
|
old_content=$(cat "$target") |
||||
|
if [ "$content" == "$old_content" ]; then |
||||
|
echo "Provided key already present for '$ident'." >&2 |
||||
|
return 0 |
||||
|
fi |
||||
|
echo "Replacing key for '$ident'." >&2 |
||||
|
fi |
||||
|
echo "$content" > "$target" |
||||
|
|
||||
|
/usr/local/sbin/ssh-update-keys |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
case "$1" in |
||||
|
"add") |
||||
|
shift |
||||
|
ssh-key-add "$@" |
||||
|
;; |
||||
|
"rm") |
||||
|
shift |
||||
|
ssh-key-rm "$@" |
||||
|
;; |
||||
|
"ls") |
||||
|
shift |
||||
|
ssh-key-ls "$@" |
||||
|
;; |
||||
|
*) |
||||
|
echo "Unknown command '$1'." |
||||
|
;; |
||||
|
esac |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,41 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
|
||||
|
## |
||||
|
## code |
||||
|
## |
||||
|
|
||||
|
KEYS=/etc/rsync/keys |
||||
|
RSYNC_HOME=/var/lib/rsync |
||||
|
|
||||
|
mkdir -p "$RSYNC_HOME/.ssh" |
||||
|
|
||||
|
## |
||||
|
## New |
||||
|
## |
||||
|
|
||||
|
touch "$RSYNC_HOME"/.ssh/authorized_keys.new |
||||
|
|
||||
|
for f in "$KEYS"/backup/*.pub; do |
||||
|
[ -e "$f" ] || continue |
||||
|
content=$(cat "$f") |
||||
|
ident="${f##*/}" |
||||
|
ident="${ident%.pub}" |
||||
|
if ! [[ "$ident" =~ ^[a-zA-Z0-9._-]+$ ]]; then |
||||
|
echo "bad: '$ident'" >&2 |
||||
|
continue |
||||
|
fi |
||||
|
echo "command=\"/usr/local/sbin/ssh-cmd-validate \\\"$ident\\\"\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $content" |
||||
|
done >> "$RSYNC_HOME"/.ssh/authorized_keys.new |
||||
|
|
||||
|
for f in "$KEYS"/admin/*.pub; do |
||||
|
[ -e "$f" ] || continue |
||||
|
content=$(cat "$f") |
||||
|
echo "command=\"/usr/local/sbin/ssh-admin-cmd-validate\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $content" |
||||
|
done >> "$RSYNC_HOME"/.ssh/authorized_keys.new |
||||
|
|
||||
|
mv "$RSYNC_HOME"/.ssh/authorized_keys{,.old} |
||||
|
mv "$RSYNC_HOME"/.ssh/authorized_keys{.new,} |
||||
|
|
||||
|
chown rsync:rsync -R "$RSYNC_HOME"/.ssh -R |
||||
|
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue