How to dockerize rdiff-backup with web interface

Dockerize rdiff-backup to create personal single-user instance with SSH and web interface provided by rdiffweb.

rdiffweb

Project tree

These are contents of directories in a tree-like format.

$ tree --dirsfirst
.
├── rdiffweb-ssh
│   └── Dockerfile
├── rdiffweb-ui
│   ├── docker-entrypoint.sh
│   ├── Dockerfile
│   └── rdw.conf
├── repositories
├── docker-compose.yml
└── hash_password.py

Docker compose file and two docker images at a glance.

SHA1 password hash

At first you need to know how to generate SHA1 password hash.

This code is extracted from core/passwd.py project file.
$ cat hash_password.py 
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# rdiffweb, A web interface to rdiff-backup repositories
# Copyright (C) 2019 rdiffweb contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

'''
Created on Apr. 10, 2020

@author: patrik dufresne
'''

from base64 import b64decode, b64encode
from builtins import str
import hashlib
import os
import sys

def hash_password(password):
    assert password and isinstance(password, str)
    password = password.encode(encoding='utf8')
    salt = os.urandom(4)
    h = hashlib.sha1(password)
    h.update(salt)
    return "{SSHA}" + b64encode(h.digest() + salt).decode('latin1')


number_of_parameters = len(sys.argv) 

for n in range(1, number_of_parameters):   
        print("{:30s} {:s}".format(sys.argv[n], hash_password(sys.argv[n])))

Generate SHA1 password hash.

$ python3 hash_password.py rdiff
rdiff                          {SSHA}TJrN3DS0sfu29weUUs+0Hoif+NG7V4K0

Docker for SSH access

This is a simple Docker image that provides limited SSH access for rdiff-backup user.

INITIAL_SHA1PASS is set to rdiff.
$ cat rdiffweb-ssh/Dockerfile
FROM alpine:3.12

MAINTAINER Milosz Galazka <[email protected]>

ARG DOCKER_UID=5000
ARG DOCKER_GID=5000

RUN set -eux \
 && apk add --no-cache shadow \
 && addgroup -g $DOCKER_GID -S rdiff-backup \
 && adduser -D -u $DOCKER_UID -h /rdiff-backup -s /bin/ash -G rdiff-backup -g rdiff-backup rdiff-backup \
 && usermod -p '*' rdiff-backup 
 
RUN set -eux \ 
 && apk add --no-cache rdiff-backup python3

RUN set -eux \  
 && apk add --no-cache openssh \
 && ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa \
 && date \
 && echo "PasswordAuthentication no"            | tee -a /etc/ssh/sshd_config \
 && echo "AllowTcpForwarding No"                | tee -a /etc/ssh/sshd_config \ 
 && echo "AllowUsers rdiff-backup"              | tee -a /etc/ssh/sshd_config \
 && echo "Match User rdiff-backup"              | tee -a /etc/ssh/sshd_config \
 && echo "  ForceCommand rdiff-backup --server" | tee -a /etc/ssh/sshd_config
 
RUN set -eux \
 && mkdir /rdiff-backup/.ssh \
 && touch /rdiff-backup/.ssh/authorized_keys \
 && chown rdiff-backup:rdiff-backup /rdiff-backup/.ssh /rdiff-backup/.ssh/authorized_keys \
 && chmod 700 /rdiff-backup/.ssh \ 
 && chmod 600 /rdiff-backup/.ssh/authorized_keys  

EXPOSE 22

ENTRYPOINT ["/usr/sbin/sshd", "-D"]

Docker for web interface

This Docker image serves web interface and manages for SSH keys.

$ cat rdiffweb-ui/Dockerfile
FROM alpine:3.12

MAINTAINER Milosz Galazka 

ARG DOCKER_UID=5000
ARG DOCKER_GID=5000

ENV INITIAL_SHA1PASS="{SSHA}TJrN3DS0sfu29weUUs+0Hoif+NG7V4K0"

RUN set -eux \
 && addgroup -g $DOCKER_GID -S rdiff-backup \
 && adduser -D -u $DOCKER_UID -h /rdiff-backup -s /sbin/nologin -G rdiff-backup -g rdiff-backup rdiff-backup
 
RUN set -eux \ 
 && apk add --no-cache python3 py3-pip libldap rdiff-backup sqlite \
 && apk add --no-cache --virtual .build_deps build-base linux-headers python3-dev py-setuptools openldap-dev \
 && pip install rdiffweb \
 && apk del .build_deps

RUN set -eux \
 && mkdir /rdiff-backup/.ssh \
 && touch /rdiff-backup/.ssh/authorized_keys \
 && chown rdiff-backup:rdiff-backup /rdiff-backup/.ssh /rdiff-backup/.ssh/authorized_keys \
 && chmod 700 /rdiff-backup/.ssh \ 
 && chmod 600 /rdiff-backup/.ssh/authorized_keys 

RUN set -eux \
 && mkdir /etc/rdiffweb \
 && chown rdiff-backup:rdiff-backup /etc/rdiffweb

COPY rdw.conf /etc/rdiffweb/rdw.conf

RUN set -eux \
 && chown rdiff-backup:rdiff-backup /etc/rdiffweb/rdw.conf

ADD docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

#VOLUME /etc/rdiff-backup

EXPOSE 8080

USER rdiff-backup

WORKDIR /rdiff-backup

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["rdiffweb"]

It contains default rdiffweb configuration.

$ cat rdiffweb-ui/rdw.conf
# Host name or IP address to bind to.
# If you want to bind to all interface, use 0.0.0.0
# Default: 127.0.0.1
ServerHost=0.0.0.0

# TCP port on which to listen. Default: 8080
ServerPort=8080

# Define logging level. (ERROR, WARNING, INFO, DEBUG) Default: INFO
LogLevel=INFO

# Define where log stuff. Default to console.
#LogFile=/var/log/rdiffweb.log
#LogAccessFile=/var/log/rdiffweb-access.log

# Define the kind of environment where rdiffweb is running. Depending of this
# configuration, behaviours related to exception handling are different. In
# "development" environment, stacktrace might be shown. In "production"
# environment those are hidden to user.
Environment=production

# Customizing rdiffweb
#FavIcon=/etc/rdiffweb/favicon.ico
#HeaderLogo=/etc/rdiffweb/logo.jpg
#HeaderName=rdiffweb
#WelcomeMsg=
#WelcomeMsg[fr]=
#WelcomeMsg[ru]=
# Define the color scheme. Option: default, orange.
#DefaultTheme=orange

# Temporary location where rdiffweb should restore your data and create
# archive if necessary to restore. This configuration parameter may be useful
# when your /tmp folder is very small. 
#tempdir=/tmp

# The time of day when notification emails are sent out. (Default: 23:00).
#EmailNotificationTime=23:00 

# The SMTP server name (Required).
#EmailHost=smtp.server.com

# Encryption to be use if Any. Option: ssl or starttls (Default: none).
#EmailEncryption=none

# Define the 'From:' (Required)
#[email protected]

# May be blank, if the smtp server does not require authentication
#EmailUsername=email_user

# May be blank, if the smtp server does not require authentication
#EmailPassword=email_password

# Send confirmation mail if user changed his email address.
#EmailSendChangedNotification=true

# Update user repositories every 15 minutes
# AutoUpdateRepos=15

# If the user/password are valid (found in LDAP) create the user
# in the database. Default: false
#AddMissingUser=false

#----- Enable Sqlite DB Authentication.
#SQLiteDBFile=/etc/rdiffweb/rdw.db

#----- Enable LDAP Authentication
# The uri parameter may be a comma- or whitespace-separated list of URIs
# containing only the schema, the host, and the port fields.
#LdapUri=ldap://localhost:389

# An aditional Ldap query filter to limit the search
#LdapFilter=(objectClass=posixAccount)

# This directive specifies an LDAP group whose members are allowed access. It
# takes the distinguished name of the LDAP group.
#LdapRequiredGroup=cn=Administrators,dc=nodomain

# Set to true to enable TLS (optional, default:false)
#LdapTls=true

# The DN of the branch of the directory where all searches should start from. 
#LdapBaseDn=dc=nodomain

# An optional DN used to bind to the server when searching for entries. If not
# provided, will use an anonymous bind.
#LdapBindDn=cn=admin,dc=nodomain

# A bind password to use in conjunction with the bind DN. Note that the bind
# password is probably sensitive data, and should be properly protected. You
# should only use the LdapBindDn and LdapBindPassword if you absolutely
# need them to search the directory.
#LdapBindPassword=my_secret

# Limit on waiting for a network response, in seconds. (default:10)
#LdapNetworkTimeout=10

# Limit on waiting for any response, in seconds.
#LdapTimeout=300

# Version of LDAP in use either 2 or 3.
#LdapProtocolVersion=2
#LdapProtocolVersion=3

# When set to True, allow LDAP users to update their password using rdiffweb
# web interface. Otherwise, LDAP users cannot update their password.
#LdapAllowPasswordChange=true

# Enable verification of ShadowExpire.
#LdapCheckShadowExpire=true

There is also a shell script to perform initial user configuration. You can define INITIAL_SHA1PASS, INITIAL_THEME and INITIAL_HEADER.

$ cat rdiffweb-ui/docker-entrypoint.sh
#!/bin/sh 
set -eux

RDIFFWEB_DATABASE="/etc/rdiffweb/rdw.db"
RDIFFWEB_CONFIG="/etc/rdiffweb/rdw.conf"
RDIFFWEB_HOME="/rdiff-backup"

# get password from file
if [ -n "${INITIAL_SHA1PASS_FILE+x}" ]; then
  INITIAL_SHA1PASS="$(< "$INITIAL_SHA1PASS_FILE")"
fi

if [ "$1" = 'rdiffweb' ]; then
	# set username, password
	if [ -n "$INITIAL_SHA1PASS" ]; then
		if [ -f "$RDIFFWEB_DATABASE" ]; then
			CURRENT_COUNT="$(echo "select count(*) from users where userID=1" | sqlite3 $RDIFFWEB_DATABASE)"
			CURRENT_SHA1PASS="$(echo "select Password from users where userID=1" | sqlite3 $RDIFFWEB_DATABASE)"
			if [ "$CURRENT_COUNT" -eq "1" ]; then
				if [ "$CURRENT_SHA1PASS" != "$INITIAL_SHA1PASS" ]; then
					echo "update users set Password=\"$INITIAL_SHA1PASS\" where userID=1" | sqlite3 $RDIFFWEB_DATABASE
				fi			
			else			
					echo "INSERT INTO users VALUES(1,\"admin\",\"$INITIAL_SHA1PASS\",\"\",0,\"$RDIFFWEB_HOME\",1,0)" | sqlite3 $RDIFFWEB_DATABASE
			fi
		else
			cat <<-EOF | sqlite3 $RDIFFWEB_DATABASE
				PRAGMA foreign_keys=OFF;
				BEGIN TRANSACTION;
				CREATE TABLE users (
				UserID integer primary key autoincrement,
				Username varchar (50) unique NOT NULL,
				Password varchar (40) NOT NULL DEFAULT "",
				UserRoot varchar (255) NOT NULL DEFAULT "",
				IsAdmin tinyint NOT NULL DEFAULT FALSE,
				UserEmail varchar (255) NOT NULL DEFAULT "",
				RestoreFormat tinyint NOT NULL DEFAULT TRUE, role tinyint NOT NULL DEFAULT "10");
				INSERT INTO users VALUES(1,"admin","$INITIAL_SHA1PASS","",0,"$RDIFFWEB_HOME",1,0);

				CREATE TABLE repos (
				RepoID integer primary key autoincrement,
				UserID int(11) NOT NULL,
				RepoPath varchar (255) NOT NULL,
				MaxAge tinyint NOT NULL DEFAULT 0,
				Encoding varchar (50), keepdays varchar(255) NOT NULL DEFAULT "");

				CREATE TABLE sshkeys (
				Fingerprint primary key,
				Key clob UNIQUE,
				UserID int(11) NOT NULL);
				DELETE FROM sqlite_sequence;
				INSERT INTO sqlite_sequence VALUES('users',2);
				INSERT INTO sqlite_sequence VALUES('repos',1);
				COMMIT;
			EOF
		fi
	fi

	# set home
	CURRENT_HOME="$(echo "select UserRoot from users where userID=1" | sqlite3 $RDIFFWEB_DATABASE)"
	if [ "$CURRENT_HOME" != "$RDIFFWEB_HOME" ]; then
		echo "update users set UserRoot=\"$RDIFFWEB_HOME\" where userID=1" | sqlite3 $RDIFFWEB_DATABASE
	fi			

	# set theme, header name
	if [ -n "$INITIAL_THEME" ]; then
		sed -i -e "s/^#\?DefaultTheme=.*/DefaultTheme=${INITIAL_THEME}/" $RDIFFWEB_CONFIG
	fi		
	
	if [ -n "$INITIAL_HEADER" ]; then
		sed -i -e "s/^#\?HeaderName=.*/HeaderName=${INITIAL_HEADER}/" $RDIFFWEB_CONFIG
	fi		
	
	exec /usr/bin/rdiffweb "[email protected]"
fi

exec "[email protected]"
It can be easily extended to automatically add repositories.

Docker compose

Put everything together using Docker compose.

$ cat docker-compose.yml
version: "3.3"

services:
  rdiffweb-ui:
    build: 
      context: rdiffweb-ui
      args:
        DOCKER_UID: 1000
        DOCKER_GID: 1000
    environment:
      INITIAL_SHA1PASS: "{SSHA}ErnSUDB9Za33sI23om75Odl4KaH83oKU"
      INITIAL_HEADER: "Personal backup"
      INITIAL_THEME: "orange"
    ports:
      - 8080:8080
    volumes:
      - ./repositories:/rdiff-backup/repositories
      - ssh_config:/rdiff-backup/.ssh
      - rdiffweb_config:/etc/rdiffweb
  rdiffweb-ssh:
    build: 
      context: rdiffweb-ssh
      args:
        DOCKER_UID: 1000
        DOCKER_GID: 1000
    ports:
      - "2222:22"
    volumes:
      - ./repositories:/rdiff-backup/repositories
      - ssh_config:/rdiff-backup/.ssh

volumes:
  ssh_config:
  rdiffweb_config:
Local user running this Docker compose is UID:1000/GID:1000. INITIAL_SHA1PASS is set to milosz.

Usage

SSH port 2222, WWW port 8080.

Define --remote-schema parameter to use it locally.

$ rdiff-backup --print-statistics --remote-schema "ssh -C -p 2222 %s rdiff-backup --server" ~/Projects/ansible/ [email protected]::/rdiff-backup/repositories/ansible
--------------[ Session statistics ]--------------
StartTime 1599402464.00 (Sun Sep  6 14:27:44 2020)
EndTime 1599402464.50 (Sun Sep  6 14:27:44 2020)
ElapsedTime 0.50 (0.50 seconds)
SourceFiles 12
SourceFileSize 649894 (635 KB)
MirrorFiles 1
MirrorFileSize 0 (0 bytes)
NewFiles 11
NewFileSize 649894 (635 KB)
DeletedFiles 0
DeletedFileSize 0 (0 bytes)
ChangedFiles 1
ChangedSourceSize 0 (0 bytes)
ChangedMirrorSize 0 (0 bytes)
IncrementFiles 0
IncrementFileSize 0 (0 bytes)
TotalDestinationSizeChange 649894 (635 KB)
Errors 0
--------------------------------------------------

Additional notes

Use HAProxy to install it at home on your mini-PC.

The source code is available at GitLab.

There is also Minarca which is a simple data backup application, that's enough for today.