From a0a5679117bb653c3a182c885acaffce81b86d97 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 19 Apr 2021 18:24:37 +0200 Subject: [PATCH] new: [rsync-backup-target] allow dynamic management of backup keys Signed-off-by: Valentin Lab --- rsync-backup-target/README.org | 84 ++++++++++++++ rsync-backup-target/build/entrypoint.sh | 13 +-- .../build/src/etc/sudoers.d/rsync | 1 + .../src/usr/local/sbin/ssh-admin-cmd-validate | 79 +++++++++++++ .../build/src/usr/local/sbin/ssh-key | 108 ++++++++++++++++++ .../build/src/usr/local/sbin/ssh-update-keys | 41 +++++++ rsync-backup-target/hooks/init | 11 +- .../hooks/log_rotate-relation-joined | 16 +++ rsync-backup-target/metadata.yml | 1 + .../resources/bin/compose-add-rsync-key | 4 +- 10 files changed, 337 insertions(+), 21 deletions(-) create mode 100644 rsync-backup-target/README.org create mode 100755 rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate create mode 100755 rsync-backup-target/build/src/usr/local/sbin/ssh-key create mode 100755 rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys diff --git a/rsync-backup-target/README.org b/rsync-backup-target/README.org new file mode 100644 index 0000000..e41f456 --- /dev/null +++ b/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 diff --git a/rsync-backup-target/build/entrypoint.sh b/rsync-backup-target/build/entrypoint.sh index 99fb98a..e06f68c 100755 --- a/rsync-backup-target/build/entrypoint.sh +++ b/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 diff --git a/rsync-backup-target/build/src/etc/sudoers.d/rsync b/rsync-backup-target/build/src/etc/sudoers.d/rsync index 2b8b4ee..433761d 100644 --- a/rsync-backup-target/build/src/etc/sudoers.d/rsync +++ b/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 * diff --git a/rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate b/rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate new file mode 100755 index 0000000..9f7b3b6 --- /dev/null +++ b/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. diff --git a/rsync-backup-target/build/src/usr/local/sbin/ssh-key b/rsync-backup-target/build/src/usr/local/sbin/ssh-key new file mode 100755 index 0000000..f569e51 --- /dev/null +++ b/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 + + + + diff --git a/rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys b/rsync-backup-target/build/src/usr/local/sbin/ssh-update-keys new file mode 100755 index 0000000..2c2f2ef --- /dev/null +++ b/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 + diff --git a/rsync-backup-target/hooks/init b/rsync-backup-target/hooks/init index dacc0df..3312663 100755 --- a/rsync-backup-target/hooks/init +++ b/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 " diff --git a/rsync-backup-target/hooks/log_rotate-relation-joined b/rsync-backup-target/hooks/log_rotate-relation-joined index a7372e1..767ae34 100755 --- a/rsync-backup-target/hooks/log_rotate-relation-joined +++ b/rsync-backup-target/hooks/log_rotate-relation-joined @@ -37,6 +37,22 @@ file_put "$DST" </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