Browse Source

new: [rsync-backup-target] allow dynamic management of backup keys

Signed-off-by: Valentin Lab <valentin.lab@kalysto.org>
pull/1/head
Valentin Lab 3 years ago
parent
commit
a0a5679117
  1. 84
      rsync-backup-target/README.org
  2. 13
      rsync-backup-target/build/entrypoint.sh
  3. 1
      rsync-backup-target/build/src/etc/sudoers.d/rsync
  4. 79
      rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate
  5. 108
      rsync-backup-target/build/src/usr/local/sbin/ssh-key
  6. 41
      rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys
  7. 11
      rsync-backup-target/hooks/init
  8. 16
      rsync-backup-target/hooks/log_rotate-relation-joined
  9. 1
      rsync-backup-target/metadata.yml
  10. 4
      rsync-backup-target/resources/bin/compose-add-rsync-key

84
rsync-backup-target/README.org

@ -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

13
rsync-backup-target/build/entrypoint.sh

@ -12,18 +12,7 @@ RSYNC_HOME=/var/lib/rsync
mkdir -p "$RSYNC_HOME/.ssh"
for f in "$KEYS"/*.pub; do
[ -e "$f" ] || continue
content=$(cat "$f")
ident="${f##*/}"
ident="${ident%.pub}"
if ! [[ "$ident" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "bad: '$ident'"
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
chown rsync:rsync -R "$RSYNC_HOME"/.ssh -R
ssh-update-keys
## Give back PID 1 so that ssh can receive signals
exec /usr/sbin/sshd -D -e

1
rsync-backup-target/build/src/etc/sudoers.d/rsync

@ -2,3 +2,4 @@
## the real check is done on the ``ssh-cmd-validate`` side.
rsync ALL=(root) NOPASSWD: /usr/bin/rsync --server * . /var/mirror/*
rsync ALL=(root) NOPASSWD: /usr/local/sbin/ssh-key *

79
rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate

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

108
rsync-backup-target/build/src/usr/local/sbin/ssh-key

@ -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

41
rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys

@ -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

11
rsync-backup-target/hooks/init

@ -15,7 +15,7 @@ set -e
service_def=$(get_compose_service_def "$SERVICE_NAME")
keys=$(echo "$service_def" | shyaml -y get-value options.keys 2>/dev/null) || {
keys=$(echo "$service_def" | shyaml -y get-value options.admin 2>/dev/null) || {
err "You must specify a ${WHITE}keys${NORMAL} struct to use this service"
exit 1
}
@ -26,8 +26,7 @@ keys=$(echo "$service_def" | shyaml -y get-value options.keys 2>/dev/null) || {
}
local_path_key=/etc/rsync/keys
host_path_key="$SERVICE_CONFIGSTORE${local_path_key}"
key_nb=0
host_path_key="$SERVICE_DATASTORE${local_path_key}"
## ident are unique by construction (they are struct keys)
## but keys need to be also unique
@ -43,7 +42,7 @@ while read-0 ident key; do
exit 1
fi
debug "Creating access key for ${ident}" || true
echo "$key" | file_put "$host_path_key/${ident}.pub"
echo "$key" | file_put "$host_path_key/admin/${ident}.pub"
keys["$key"]="$ident"
done < <(echo "$keys" | shyaml key-values-0)
@ -51,14 +50,12 @@ debug "Adding config hash to enable recreating upon config change."
config_hash=$({
## XXXvlab: ``env -i`` sole purpose is to protect find
## against big shell environments, and prevent it to fail.
env -i find "${host_path_key}" \
env -i find "${host_path_key}/admin" \
-name \*.pub -exec md5sum {} \;
} | md5_compat) || exit 1
init-config-add "\
$SERVICE_NAME:
volumes:
- $host_path_key:$local_path_key:ro
labels:
- compose.config_hash=$config_hash
"

16
rsync-backup-target/hooks/log_rotate-relation-joined

@ -37,6 +37,22 @@ file_put "$DST" <<EOF
sharedscripts
}
/var/log/docker/$SERVICE_NAME/ssh-admin-cmd-validate.log
{
weekly
missingok
dateext
dateyesterday
dateformat _%Y-%m-%d
extension .log
rotate $rotated_count
compress
delaycompress
notifempty
create 660 $uid
sharedscripts
}
/var/log/docker/$SERVICE_NAME/target_*_rsync.log
{
weekly

1
rsync-backup-target/metadata.yml

@ -1,5 +1,6 @@
description: Backup Rsync over SSH Target
data-resources:
- /etc/rsync/keys
- /var/mirror
- /var/log/rsync

4
rsync-backup-target/resources/bin/compose-add-rsync-key

@ -61,7 +61,7 @@ cmdline.spec::cmd:__main__:run() {
local service_name compose_file
service_name=${opt_service_name:-rsync-backup-target}
compose_file=${opt_compose_file:-/etc/compose/compose.yml}
if ! existing_domains=$(shyaml keys "${service_name//./\\.}.options.keys" < "$compose_file"); then
if ! existing_domains=$(shyaml keys "${service_name//./\\.}.options.admin" < "$compose_file"); then
err "Couldn't query file '$compose_file' for keys of" \
"service ${DARKYELLOW}${service_name}${NORMAL}."
exit 1
@ -70,7 +70,7 @@ cmdline.spec::cmd:__main__:run() {
content=$(cat "$compose_file")
if echo "$existing_domains" | grep "^${DOMAIN}$" >/dev/null 2>&1; then
if ! prev_key=$(shyaml get-value "${service_name//./\\.}.options.keys.${DOMAIN//./\\.}" \
if ! prev_key=$(shyaml get-value "${service_name//./\\.}.options.admin.${DOMAIN//./\\.}" \
< "$compose_file"); then
err "Couldn't query file '$compose_file' for key of domain '$DOMAIN'."
exit 1

Loading…
Cancel
Save