rsync を使ってバックアップシステムを作る

要件はこんな感じで。

  • ディスクをたくさん持っているファイルサーバーにバックアップサーバーの役割を持たせる
  • rsync over ssh で接続する
  • 方向はバックアップサーバーから rsync で接続して、バックアップ対象のファイルを pull してくる

ssh 接続用の鍵ペアの作成

この鍵はパスなしで作ります。

# ssh-keygen -t ed25519
Enter file in which to save the key (/root/.ssh/id_ed25519): /path/to/id_ed25519
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /path/to/id_ed25519.
Your public key has been saved in /path/to/id_ed25519.pub.
 :
 :

shell スクリプトを書く。

こんな感じですね……shell スクリプトはあまり得意じゃないので美しくないかもしれないですが、とりあえずは動きますw

backup.sh
#!/bin/bash
################################################################################
# backup_remote.sh
# create @17.04.17
################################################################################
 
. /path/to/utils.sh
 
 
##
# settings
#
LIST_DIR='/path/to/backup.d'
 
LIST_EXT='.list'
 
DEST_DIR='/path/to/dest'
 
LOG_DIR='/var/log/rsync'
 
SSH_USER='collector'
 
SSH_PRIVKEY='/path/to/id_ed25519'
 
RSYNC_CMD='/usr/bin/rsync'
 
RSYNC_DEFOPTS='--delete --exclude lost+found'
 
##
# check variables.
#
if [ ! -f "${RSYNC_CMD}" ] || [ ! -x "${RSYNC_CMD}" ]; then
        error "'${RSYNC_CMD} is not executable."
        exit 1
fi
 
if [ ! -d "${LIST_DIR}" ] || [ ! -x "${LIST_DIR}" ]; then
        error "Cannot access to '${LIST_DIR}'."
        exit 1
fi
 
if [ ! -f "${SSH_PRIVKEY}" ] || [ ! -r "${SSH_PRIVKEY}" ]; then
        error "Cannot access to '${SSH_PRIVKEY}'."
        exit 1
fi
 
if [ ! -d "${DEST_DIR}" ] || [ ! -w ${DEST_DIR} ]; then
        error "Cannot access to '${DEST_DIR}'."
        exit 1
fi
 
if [ ! -d "${LOG_DIR}" ] || [ ! -w ${LOG_DIR} ]; then
        error "Cannot access to '${LOG_DIR}'."
        exit 1
fi
 
 
##
# check argument
#
if [ -z "$1" ]; then
        error "usage: ${0} <hostname>"
        exit 1
fi
 
HOST=${1}
 
if [ "${HOST}" = 'local' ]; then
        error "Not acceptable hostname '${HOST}'."
        exit 1
fi
 
FILE="${LIST_DIR}/${HOST}${LIST_EXT}"
 
if [ ! -f "${FILE}" ] && [ ! -r "${FILE}" ]; then
        error "Cannot access to '${FILE}'."
        exit 1
fi
 
##
# start main.
#
LOG_FILE="${LOG_DIR}/${HOST}.$(date +%Y-%m-%d-%H.%M.%S).log"
 
RET=0
 
info "Backup start."
info "Rsync log is ${LOG_FILE}"
 
while read DIR OPTS; do
 
        # skip blank line.
        if [[ "${DIR}" =~ ^\s*$ ]]; then
                continue
        fi
 
        # skip comment line.
        if [[ "${DIR}" =~ ^\s*# ]]; then
                continue
        fi
 
        if [[ "${DIR}" =~ [^/]$ ]]; then
                warn "Directory path should ends with '/'."
                DIR="${DIR}/"
        fi
 
        SRC="${HOST}:${DIR}"
        DST="${DEST_DIR}/${HOST}${DIR}"
 
        # create directory tree
        /bin/mkdir -p ${DST}
 
        if [ $? -ne 0 ]; then
                error "Cannot create directory: '${DST}'."
                RET=2
                continue
        fi
 
        info "Start sync from '${SRC}' to '${DST}'."
 
        ${RSYNC_CMD} -avvr \
                --rsh "ssh -l ${SSH_USER} -i ${SSH_PRIVKEY} -o strictHostKeyChecking=no" \
                --rsync-path="sudo rsync" \
                ${RSYNC_DEFOPTS} ${OPTS} \
                ${SRC} ${DST} \
                >> ${LOG_FILE} 2>&1
 
        if [ $? -ne 0 ]; then
                error "Failed to rsync from '${SRC}' to '${DST}': $!"
                RET=3
        else
                info "Succeeded to rsync from '${SRC}' to '${DST}'."
        fi
 
done < ${FILE}
 
 
info "Backup finished."
 
exit ${RET}

utils.sh は、info やら warn やらの function を定義しているだけなので割愛。<hostname> に local を使えないようにしているのは、もう1つこのバックアップサーバー内でローカル rsync する shell スクリプトがありまして、そのリストファイルが 'local.list' で作ってあるからです。ちょっと書式が違うので……思いっきり俺環のはなしです。

で、この shell スクリプトはこんな風に使います。

# ./backup.sh <hostname>

${LIST_DIR} に設定したディレクトリには <hostname>.list というテキストをこんな感じで置いておきます。

/var/log/nginx/                  --exclude=error_log --bwlimit=8192
/var/log/portage/
/var/log/hogefuga/               --bwlimit=8192

${IFS} で区切って1番目にはバックアップを取得するディレクトリを。2番目以降は rsync のオプションです。指定できるのはディレクトリのみで、ファイルを指定することはできません。

バックアップサーバー側はこれで準備okですね。

ここまでやる必要はないですね……っていう程度には作り込みます。

バックアップ専用ユーザーの作成

ssh で接続してきて、極力 rsync 以外のことはできないようなユーザーを作成します。

useradd --create-home --uid 1999 --groups users --shell /bin/rbash --comment rsyncBackupCollector collector

--uid を指定しているのは、複数あるバックアップ対象のサーバーの全てで同じ uid を使うようにするためです。こうしておけば、以降の記事で作る /home/collector/ 以下の環境を tar で固めて展開できたりするので、多少ですが省力化になります。

この時点で --shell で rbash を指定しているので、これだけでもできることはかなり制限されているのですが……趣味とノリと勢いでもっともっとこのユーザー collector でできることを絞っていきます。

コマンドサーチパスの制限

collector ユーザー専用の bin ディレクトリを作成します。

# mkdir /home/collector/bin

作成した専用 bin に、collector ユーザーが使用できるコマンドのシンボリックリンクを作成します。

# ln -sf /usr/bin/sudo /home/collector/bin/sudo
# ln -sf /usr/bin/rsync /home/collector/bin/rsync
# ls -l /home/collector/bin
total 0
lrwxrwxrwx 1 root root 14 Apr 17 12:03 rsync -> /usr/bin/rsync
lrwxrwxrwx 1 root root 13 Apr 17 12:03 sudo -> /usr/bin/sudo

/home/collector/.bashrc を編集して、コマンドサーチパスを作成した専用 bin ディレクトリに限定します。

PATH=/home/collector/bin

ついでに .bashrc と .bash_profile を collector 自身で変更できないように owner を変更してしまいます。

# chown root:root /home/collector/.bashrc /home/collector/.bash_profile

ちなみに rbash では、環境変数 ${PATH} はリードオンリーになっていて変更できません。

 # ssh -i /path/to/id_ed25519 collector@server 'PATH=/usr/bin'
rbash: PATH: readonly variable

また cd は禁止されている上に、コマンドに / を含めることも許されないため、フルパスでコマンドを打つこともできません。

# ssh -i /path/to/id_ed25519 collector@server 'cd /bin;ls'
rbash: line 0: cd: restricted
rbash: ls: command not found
# ssh -i /path/to/id_ed25519 collector@server '/bin/ls'
rbash: /bin/ls: restricted: cannot specify `/' in command names

ssh 接続の制限

/home/collector/.ssh/authorized_keys に、バックアップサーバーから接続するための公開鍵を追加して、色々な制限を加えます。

from="192.168.ip.addr",environment="PATH=/home/collector/bin",no-agent-forwarding,no-port-forwarding,no-user-rc,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKu83AydT6QgO7x/4VnuMSpTaaHvr6V8CZpsYXQQg7l+ collector

このへんの設定は man sshd に書いてありますね。from= で接続元を制限しています。

また environment=1)でコマンドサーチパスを限定しています。これは、通常の ssh ログインの場合は .bashrc の PATH=/home/collector/bin の設定が効くのですが、ssh collector@host 'echo $PATH' のように直接コマンド実行をする場合はログインシェルを取らないので .bashrc 等が適用されない(/etc/env.d/あたりで決定するデフォルトが適用される)ためです。

あと no- と付くものはとりあえず全部設定しておきます。

ちなみにこの時点で1度 no-pty を外して、ssh でログインしてみるのも良いかもしれません。結構絶望的になにもできない環境になっていますが……やっぱり完璧ではなくどこかに穴があるんでしょうねw

sudoers の設定

collector ユーザーの権限では触れないファイル等もあるので、rsync は sudo コマンドを経由して root 権限で実行させます。そのために sudoers にこんな設定を入れます。

collector ALL=(root) NOPASSWD:/home/collector/bin/rsync

接続確認

ここまでできたら、バックアップサーバー側から秘密鍵を使って ssh 経由で sudo rsync が起動できるかどうかを確認してみます。

# ssh -i /path/to/id_ed25519 collector@server 'sudo rsync --version'
rsync  version 3.1.2  protocol version 31
Copyright (C) 1996-2015 by Andrew Tridgell, Wayne Davison, and others.
Web site: http://rsync.samba.org/
Capabilities:
    64-bit files, 64-bit inums, 64-bit timestamps, 64-bit long ints,
    socketpairs, hardlinks, symlinks, IPv6, batchfiles, inplace,
    append, ACLs, xattrs, iconv, symtimes, prealloc

rsync comes with ABSOLUTELY NO WARRANTY.  This is free software, and you
are welcome to redistribute it under certain conditions.  See the GNU
General Public Licence for details.

こんな感じで rsync のバージョン情報が表示されれば、バックアップ対象となるサーバーの設定は完了です。

というわけで、バックアップサーバー側で /path/to/backup.sh <hostname> を好きなだけ並べた shell スクリプトを作って、そいつをキックする backup.service やら backup.timer やらを作って完成です。うちの環境では、リモート rsync で引っ張ってきたログをさらにバックアップサーバー上で別のディスクに rsync するローカルバックアップ用の shell スクリプトと、さらにリモートとローカルのバックアップを順次実行、リターンコードを見て logger コマンドで syslog に投げるような上位 shell スクリプトを作って、そいつを timer 起動しています。

……ムダに手間かけて作りこんじゃいました。

1)
sshd_config で PermitUserEnvironment yes を設定しておく必要があります。
https://manimani.cc/lib/plugins/linkback/exe/trackback.php/wiki:linux:rsync_backup