Ein Fileserver mit dem Raspberry Pi

Das Ziel

Bislang hatte ich meine Daten auf einer externen USB-Festplatte, von der ich regelmäßig Backups angelegt habe. Das ging ganz gut, war aber insbesondere bezüglich der SVN-Repositories etwas lästig, weil ich sowohl von Windows als auch von Linux aus zugreifen wollte und sich die SVN-Versionen manchmal nicht über das Repository-Format einig waren. Außerdem hatte ich ein paar Dateien in einem AES-Container (dm-crypt/cryptmount) an die ich von Windows aus nicht dran kam. Dass sich vor etwa drei Jahren ein Netbook hinzugesellte, von dem aus der Zugriff auf die Dateien auch manchmal ganz nützlich wäre, hat die Situation ebenfalls nicht einfacher gemacht.

Die ganze Zeit schwelte die Idee eines intelligenten Fileservers mit SVN-Repository vor sich hin. Irgendwann legte ich mir zu diesem Zweck einen Barebone-PC zu, den ich entsprechend eingerichtet habe. Als ich allerdings mit einem Messgerät den Stromverbrauch angesehen habe, erschien mir der Nutzen nicht mehr Rechtfertigung genug zu sein: bei rund 40 Watt sind das ca. 350 kWh im Jahr (entspricht etwa 80 EUR).

Durch das Support-Ende von Windows XP ist zwar ein Faktor aus der Kalkulation verschwunden (mein Gerätezoo läuft momentan nur noch unter Linux), aber der Wunsch nach einem per Netzwerk erreichbaren Datenspeicher war immer noch vorhanden. Als mir dann Anfang des Jahres ein Raspberry Pi in die Hände fiel dachte ich mir "warum eigentlich nicht?".

Weil ich gerne schwache Hardware quäle und auch die Vorzüge meines AES-Containers nicht verlieren wollte, habe ich mir das Ziel gesetzt, auch bei der Filebox die Daten verschlüsselt abzulegen. Wenn ein solches Teil per Netzwerk erreichbar ist, dann sollte auch die Übertragung zum Zielsystem geschützt sein, weshalb sowohl für HTTP als auch FTP die zusätzliche Sicherung durch TLS eingeplant wurde. Ich war gespannt, welche Datenraten ich bei diesem ehrgeizigen Setup erreichen würde.

Die Mittel

Ich bin immer wieder erstaunt, wie wenig wirklich bedeutende Daten ich habe. In Angesicht Terabyte großer Festplatten denke ich immer ich mache etwas falsch, wenn mein persönliches Hab und Gut auf eine normale CD passt. Etwas Vereinskram, meine Repositories von Webseite und Projekten, ein paar Fotos, Briefe, sonstige Dokumente... in Summe so zwischen 200 und 300 Megabyte.

Die eingangs erwähnte USB-Festplatte verrichtet ihre Arbeit jetzt am Raspberry Pi, der wiederum für die Bereitstellung der Daten im Netzwerk dient. Den Stromverbrauch dieser Lösung habe ich noch nicht gemessen, aber wenn man die unnötigen Lichter an Steckdosenleiste, Steckernetzteil und USB-Hub totlegt sollte man auf unter 5 Watt kommen. Das Ganze findet momentan Platz in einer Holzkiste mit den Maßen 20 x 30 x 15 cm aus der nur zwei Kabel herauskommen - Strom und Netzwerk.

Die Anleitung

Im Laufe dieses Artikels wird ziemlich oft zwischen verschiedenen Benutzern und Rechnern gewechselt. Damit das nicht ganz so verwirrend ist, habe ich verschiedene Farbschemata verwendet:

Ausgabe des Raspberry Pi
$ Eingabe als normaler User
# Eingabe als root
Ausgabe des Desktop-PCs
$ Eingabe als normaler User
Konfigurationsdateien und Editor-Eingaben

Installation des Basissystems

Wir folgen der Anleitung auf Arch Linux ARM um das Basissystem auf die SD-Karte zu bekommen. Wenn ich die so präparierte SD-Karte in den Raspberry Pi stecke und aufstarte, dann erhält er via DHCP von meinem DSL-Router eine IP-Adresse. Ich nehme an das wird bei den meisten ebenso sein. In dem Fall bleibt die Frage: welche Adresse ist das?

Diese Frage beantwortet uns der freundliche Portscanner nmap (Ausgabe auf das Relevante gekürzt):

$ nmap 192.168.2.0/24

Starting Nmap 6.46 ( http://nmap.org ) at 2014-08-09 20:15 CEST
Nmap scan report for alarmpi (192.168.2.101)
Host is up (0.016s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE
22/tcp open  ssh

Nmap done: 256 IP addresses (5 hosts up) scanned in 7.09 seconds

Jetzt können wir uns per SSH einloggen (Defaultzugang: root/root). Die Schlüssel werden übrigens bei jeder Installation neu generiert, sind also nicht weltweit bei allen Installationen gleich. ;-)

$ ssh root@192.168.2.101
The authenticity of host '192.168.2.101 (192.168.2.101)' can't be established.
ECDSA key fingerprint is ad:44:ac:07:ac:63:80:f5:86:8c:85:ee:92:22:ef:4c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.2.101' (ECDSA) to the list of known hosts.
root@192.168.2.101's password:
Welcome to Arch Linux ARM

     Website: http://archlinuxarm.org
       Forum: http://archlinuxarm.org/forum
         IRC: #archlinux-arm on irc.Freenode.net

Als Erstes ändern wir das root-Passwort, setzen einen neuen Hostnamen und fügen einen normalen User hinzu.

# echo filebox > /etc/hostname
# passwd
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
# useradd -g users -G wheel -m felix
# passwd felix
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully

Damit haben wir jetzt einen User "felix" in der Gruppe "users", der aber auch in der Gruppe "wheel" drin ist (was z.B. Voraussetzung dafür ist, mit su auf root wechseln zu dürfen).

Als Nächstes aktualisieren wir das System um Updates und Bugfixes zu bekommen. Ein Reboot danach ist meistens notwendig, weil in der Regel auch ein neuer Kernel installiert wurde. Außerdem zeigt durch den Neustart die Änderung des Hostnamens die erwünschte Wirkung.

# pacman -Syu
[...]
# reboot

Wenn alles geklappt hat können wir uns mit dem neuen User einloggen und mit su auf den root-Account wechseln.

$ ssh 192.168.2.101
felix@192.168.2.101's password: 
Welcome to Arch Linux ARM

     Website: http://archlinuxarm.org
       Forum: http://archlinuxarm.org/forum
         IRC: #archlinux-arm on irc.Freenode.net

Last login: Sun Aug 10 01:51:55 2014 from blackbox
$ su -
Password: 
# whoami
root

Wer den Remote-Zugang noch etwas sicherer machen möchte, der kann direkte Logins für root verbieten. Der Gedanke dabei ist, dass ein Angreifer sich zunächst einen normalen User-Account unter den Nagel reißen muss, bevor er sich von dort aus um den root-Account bemühen kann. Dazu wird die Datei /etc/ssh/sshd_config editiert:

# vi /etc/ssh/sshd_config
PermitRootLogin no
# systemctl restart sshd.service

Sichere Datenspeicherung mit dm-crypt und LUKS

Als Nächstes richten wir den sicheren Datenspeicher ein. Es ist ja schön und gut, wenn man später von außen nur mit einem gut gewählten Passwort an FTP/HTTP dran kommt, aber hilft nicht viel, wenn jemand einfach die USB-Festplatte abzupfen und an einen anderen Rechner anschlließen kann. Wer nur Angst vor Remote-Angriffen hat kann diesen Schritt natürlich überspringen (und wird wahrscheinlich mit einem etwas höheren Datendurchsatz belohnt).

Was ich hier mache ist eine einzelne Partition mit LUKS-Header, die mit einer Passphrase geschützt wird. Wer über Alternativen nachdenken möchte (z.B. ein wesentlich sichereres Key File zu benutzen) findet im dm-crypt-Artikel des ArchWiki jede Menge Zusatzinfos.

Als Erstes schließen wir die Festplatte oder den USB-Stick an und schauen nach, welcher Gerätenamen zugewiesen wird. (Ich verwende im Folgenden zur Demonstration einen USB-Stick mit 4 GB).

# dmesg | tail
[ 8730.902036] sd 1:0:0:0: [sda] Write Protect is off
[ 8730.902070] sd 1:0:0:0: [sda] Mode Sense: 03 00 00 00
[ 8730.902565] sd 1:0:0:0: [sda] No Caching mode page found
[ 8730.921722] sd 1:0:0:0: [sda] Assuming drive cache: write through
[ 8730.942699] sd 1:0:0:0: [sda] No Caching mode page found
[ 8730.956289] sd 1:0:0:0: [sda] Assuming drive cache: write through
[ 8731.400607]  sda: unknown partition table
[ 8731.403296] sd 1:0:0:0: [sda] No Caching mode page found
[ 8731.416931] sd 1:0:0:0: [sda] Assuming drive cache: write through
[ 8731.423258] sd 1:0:0:0: [sda] Attached SCSI removable disk

Auf diesem Gerät richten wir jetzt einen LUKS-Header ein:

# cryptsetup luksFormat /dev/sda

WARNING!
========
This will overwrite data on /dev/sda irrevocably.

Are you sure? (Type uppercase yes): YES
Enter passphrase: 
Verify passphrase: 

Für uns unsichtbar wurden an dieser Stelle etliche Entscheidungen bezüglich Schlüssellänge und Algorithmus getroffen. Wer sich für die Details interessiert, sei an den Artikel im ArchWiki verwiesen. Wer einfach nur sehen will, was passiert ist, kann den LUKS-Header dumpen:

# cryptsetup luksDump /dev/sda
LUKS header information for /dev/sda

Version:        1
Cipher name:    aes
Cipher mode:    xts-plain64
Hash spec:      sha1
Payload offset: 4096
MK bits:        256
MK digest:      b4 77 cf ef 89 ca 1b dd d5 ef 1f 8f 7c 43 3a e4 53 d6 f7 a9 
MK salt:        0f 30 a5 0b 4d c6 95 81 a7 a9 5e e6 e1 3a 39 d5 
                dc 6a 2b 18 23 52 0b 9e be 85 9e 31 06 84 4f 3a 
MK iterations:  4625
UUID:           d5069684-8f7d-49ef-9a24-2312a2923011

Key Slot 0: ENABLED
        Iterations:             18823
        Salt:                   71 56 5e 7e 98 33 19 e8 d0 17 55 76 df 78 9b e8 
                                a1 e4 d0 9d 93 07 40 56 11 b2 b3 6f 43 d4 36 f1 
        Key material offset:    8
        AF stripes:             4000
Key Slot 1: DISABLED
Key Slot 2: DISABLED
Key Slot 3: DISABLED
Key Slot 4: DISABLED
Key Slot 5: DISABLED
Key Slot 6: DISABLED
Key Slot 7: DISABLED

Als Nächstes wird die LUKS-Partition geöffnet, d.h. einem Ziel im Device Mapper zugeordnet:

# cryptsetup luksOpen /dev/sda crypt
Enter passphrase for /dev/sda: 

Jetzt ist unsere Partition unter /dev/mapper/crypt zu finden. Ab hier läuft alles wie gewohnt, die Verschlüsselung passiert vollkommen transparent. Unsere erste Tat mit der Partition ist das Anlegen eines ext4-Dateisystems.

# mkfs.ext4 /dev/mapper/crypt 
mke2fs 1.42.11 (09-Jul-2014)
Creating filesystem with 1002496 4k blocks and 250976 inodes
Filesystem UUID: aa906fff-8ab6-4b2b-b332-66adbdb5c031
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

Danach können wir das Dateisystem wie gewohnt mounten. Mit df schauen wir nach, ob die Größe stimmt.

# mount /dev/mapper/crypt /mnt
# df -h /mnt/
Filesystem         Size  Used Avail Use% Mounted on
/dev/mapper/crypt  3.8G  7.7M  3.5G   1% /mnt

Um die Festplatte/den USB-Stick wieder entfernen zu können, sind die letzten Schritte in umgekehrter Reihenfolge auszuführen:

# umount /mnt
# cryptsetup luksClose crypt

Zum erneuten Mounten sind nur noch luksOpen und mount notwendig, das andere Brimborium diente zur initialen Einrichtung.

# cryptsetup luksOpen /dev/sda crypt
Enter passphrase for /dev/sda: 
# mount /dev/mapper/crypt /mnt

Hier muss lediglich beim Öffnen der Partition die Passphrase angegeben werden. Damit eignet sich das Prinzip auch prima um USB-Sticks für den Einsatz an verschiedenen Rechnern zu verschlüsseln (solange auf dem Zielsystem Linux mit Kernel 2.6 oder später zur Verfügung steht und man sich als root einloggen kann).

Dateiablage mittels FTP + TLS

Als Nächstes widmen wir uns der Frage, wie die Daten auf den neu eingerichteten Server gelangen sollen. Wie bereits verraten soll zum Hochladen von Dateien FTP genutzt werden. Während das ursprüngliche FTP sowohl Login-Daten als Nutzdaten im Klartext überträgt, möchten wir eine sichere Übertragung implementieren. Dazu verwenden wir TLS (der Nachfolger von SSL). Eine grobe Übersicht zur Funktionsweise vermittelt der Wikipedia-Artikel zum Thema Transport Layer Security.

Ich habe mich für vsftpd als FTP-Server entschieden, weil ich mit diesem schon gute Erfahrung gemacht habe und die Einrichtung relativ simpel ist. Zunächst installieren wir also dieses Paket:

# pacman -S vsftpd

Als Nächstes legen wir ein SSL-Zertifikat und einen Server-Schlüssel an. Weil wir das Zertifikat nur für unsere eigenen Zwecke nutzen wollen, benötigen wir keine Signatur einer CA, sondern signieren es einfach mit dem eigenen Server-Schlüssel.

# openssl req -new -x509 -days 365 -sha1 -newkey rsa:1024 -nodes -keyout /etc/ssl/certs/vsftpd.pem -out /etc/ssl/certs/vsftpd.pem
[...]
# chmod 600 /etc/ssl/certs/vsftpd.pem
# openssl x509 -fingerprint -in /etc/ssl/certs/vsftpd.pem -noout
SHA1 Fingerprint=7C:E2:A8:C4:85:40:9E:1A:42:0D:1F:56:56:85:DC:FC:E3:DA:66:8F

Nach dem ersten Schritt werden einem viele Fragen gestellt, z.B. nach Land/Staat/Stadt/Organisation, die eigentlich keine Rolle spielen, solange man das Zertifikat nicht "für echt" nutzen, also von einer CA unterzeichnen lassen will.

Weil die Vielzahl an Parametern etwas verwirrend ist, hier eine kurze Erklärung:

req PKCS#10 X.509 Certificate Signing Request (CSR) Management
Über diesen Parameter sagt man openssl, was man überhaupt vorhat.
-x509 Hiermit drücken wir aus, dass ein selbst-signiertes Zertifikat erstellt werden soll, und kein Signing Request das an eine CA gesendet wird.
-days 365 Das ist die Gültigkeitsdauer des Zertifikats. Default-Wert ist 30 Tage, wir legen 1 Jahr fest.
-sha1 Gibt den Digest-Algorithmus an, mit dem das Zertifikat unterschrieben wird.
-newkey rsa:1024 Wir wollen auch einen neuen Schlüssel erzeugen. Dieser verwendet RSA mit 1024 Bits. 1024 habe ich deshalb genommen, weil ich das auch in allen anderen Tutorials so gesehen habe.
-nodes Gibt an, dass der neu erzeugte Schlüssel selbst unverschlüsselt abgelegt werden soll. Das ist notwendig, damit er automatisiert vom Server benutzt werden kann, anstatt jedesmal mit einer Passphrase entsperrt werden zu müssen.
-keyout <path> Gibt die Datei an, in der unser Schlüssel abgelegt wird.
-out <path> Gibt die Datei an, in der unser Zertifikat abgelegt wird.
-fingerprint Gibt zusätzlich den Fingerabdruck des Zertifikats aus.
-in <path> Gibt die Datei an, aus der das Zertifikat gelesen wird.
-noout Gibt das eigentliche Zertifikat nicht mit aus.

Das mit dem -noout ist etwas unerwartet. Gibt man es nicht an, spuckt openssl x509 immer das gesamte Zertifikat mit aus. Da wir aber nur am Fingerprint interessiert sind, geben wir -noout an.

Wir haben hier übrigens sowohl den Schlüssel als auch das Zertifikat in der selben Datei abgelegt. Je nach Tutorial wird das unterschiedlich gehandhabt. Für uns macht das eigentlich keinen Unterschied, weil wir das Zertifikat ohnehin nicht separat verteilen wollen und durch das chmod jetzt beide Teile gleich gut geschützt haben.

Jetzt nehmen wir uns die Konfiguration des FTP-Servers vor:

# vi /etc/vsftpd.conf
anonymous_enable=NO
local_enable=YES
write_enable=YES
userlist_enable=YES
userlist_file=/etc/vsftpd.user_list
userlist_deny=NO
local_root=/mnt/files
ssl_enable=YES
force_local_logins_ssl=YES
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO
rsa_cert_file=/etc/ssl/certs/vsftpd.pem
rsa_private_key_file=/etc/ssl/certs/vsftpd.pem

Die Manpage vsftpd.conf(5) ist ein gutes Beispiel für eine ordentliche Dokumentation. Hier sind alle Anweisungen ausführlich beschrieben. Die folgende Tabelle liefert eine kurze Übersicht zu dem, das ich hinzugefügt habe.

anonymous_enable=NO Nur Benutzer mit gültigem Account können sich einloggen (kein Anonymous FTP).
local_enable=YES Benutzer mit Account im lokalen System (z.B. felix) können sich mit diesen Daten anmelden.
write_enable=YES Schreibzugriffe werden grundsätzlich unterstützt.
userlist_enable=YES Etwas verwirrend: diese Option besagt nur, dass die als userlist_file angegebene Datei ausgewertet wird. Wie diese wirkt wird allerdings durch userlist_deny definiert.
userlist_file Datei mit Benutzernamen.
userlist_deny=NO Steht diese Option auf NO, wird userlist_file als Positiv-Liste ausgewertet, d.h. nur dort aufgeführte Benutzer dürfen sich einloggen. Steht die Option auf YES (was Default ist) werden die Benutzer aus der Liste abgelehnt und alle anderen erlaubt.
local_root Gibt das Verzeichnis an, in das beim Einloggen automatisch gewechselt wird. Wenn das nicht klappt stellt dies keinen Fehler dar. Außerdem kann der Benutzer in jedes beliebige höherliegende Verzeichnis wechseln (es ist also nicht mit einer Chroot-Umgebung zu verwechseln).
ssl_enable=YES SSL aktivieren.
force_local_logins_ssl=YES Erzwingt SSL bzw. TLS. Versucht der Client sich mit "normalem" FTP anzumelden erhält er die Meldung "530 Non-anonymous sessions must use encryption.".
ssl_tlsv1=YES TLSv1-Verbindungen erlauben.
ssl_sslv2=NO SSLv2 nicht zulassen (wir wollen TLSv1 erzwingen).
ssl_sslv3=NO SSLv3 nicht zulassen (wir wollen TLSv1 erzwingen).
rsa_cert_file Pfad zum Zertifikat.
rsa_private_key_file Pfad zum Private Key.

Damit das Ganze jetzt auch funktioniert, legen wir ein passendes Verzeichnis an. Dieses soll nur für den Benutzer felix les- und schreibbar sein. Die Default-Einstellung für local_umask in vsftpd ist 077, d.h. neu angelegte Dateien und Verzeichnisse werden ebenfalls maximal 700 (les-, schreib- und ausführbar für den Besitzer).

# mkdir /mnt/files
# chown felix:users /mnt/files
# chmod 700 /mnt/files/

Als Nächstes wird noch die Benutzerliste angelegt und der Service gestartet. Danach sollte der Zugriff für den Benutzer felix mit dem lokalen Unix-Passwort funktionieren.

# echo felix > /etc/vsftpd.user_list
# systemctl start vsftpd.service

Weitere Informationen zum Einrichten von vsftpd findet man im passenden ArchWiki-Artikel

Dateien browsen mittels HTTPS

Als Nächstes steht die Konfiguration des Webservers auf dem Plan. Dazu wird dieser zuerst installiert:

# pacman -S apache

Die Konfiguration liegt im Verzeichnis /etc/httpd/conf/. In dem Tutorial Step by Step: Configuring SSL Under Apache werden Zertifikat und Server-Key getrennt voneinander dort abgelegt, weil dies der Default-Konfiguration des Webservers entspricht. Ob man das am Ende genauso macht bleibt letztlich jedem selbst überlassen. Der Konsistenz halber mache ich das genauso wie bei vsftpd (im Prinzip kann man sogar dasselbe Zertifikat benutzen).

# openssl req -new -x509 -days 365 -sha1 -newkey rsa:1024 -nodes -keyout /etc/ssl/certs/httpd.pem -out /etc/ssl/certs/httpd.pem
[...]
# chmod 600 /etc/ssl/certs/httpd.pem
# openssl x509 -fingerprint -in /etc/ssl/certs/httpd.pem -noout
SHA1 Fingerprint=8C:E6:C1:A1:D8:69:D3:1D:6A:86:07:1E:7B:6E:67:BA:3B:80:49:90
# vi /etc/httpd/conf/httpd.conf

Die Beispiel-Konfiguration ist bereits ein wahres Monster. Wenn man dieser folgt, dann würde man die Konfigurationsdatei extra/httpd-ssl.conf einbinden und zusätzlich zum normalen Webserver auf Port 80 einen virtuellen Webserver auf Port 443 erhalten, der SSL macht. Das ist jedoch für unseren Anwendungsfall Quatsch und vergrößert nur die Angriffsfläche. Stattdessen werden wir die bestehende Konfiguration umbauen.

#Listen 80
User felix
Group users
DocumentRoot "/srv/http"
<Directory "/srv/http">
    Options Indexes FollowSymLinks
    AllowOverride None
#Require all granted
    AuthName "My files"
    AuthType Basic
    AuthUserFile /etc/httpd/conf/auth_file
    Require valid-user
</Directory>

Zuerst kommentieren wir die Listen-Anweisung für Port 80 aus. Danach werden die existierenden Einträge User und Group geädert. Damit läuft der Webserver mit genau den Privilegien, die der normale User-Account hat -- das ist notwendig, weil sämtliche Dateizugriffsrechte ebenfalls auf diesen User laufen.

DocumentRoot und der dazugehörige Directory-Eintrag bleiben auf /srv/http stehen. Details siehe unten.

Der nächste Abschnitt sorgt für den Zugriffsschutz. Zwar wollen wir, dass die Dateien per HTTPS durchsuchbar sind, aber natürlich soll das nicht jeder können. Für diesen Zweck reicht Basic Authentication dicke aus, denn die Übertragung der Login-Daten ist ja durch das SSL geschützt. (Das sieht übrigens auch die Apache-Dokumentation zu mod_auth_digest so.)

Als Nächstes habe ich die Konfiguration aus extra/httpd-ssl.conf direkt unterhalb des auskommentierten "Listen 80" eingefügt und auf das Folgende abgespeckt:

Listen 443

SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5
SSLPassPhraseDialog  builtin
SSLSessionCache        "shmcb:/run/httpd/ssl_scache(512000)"
SSLSessionCacheTimeout  300
SSLEngine on
SSLCertificateFile "/etc/ssl/certs/httpd.pem"
SSLCertificateKeyFile "/etc/ssl/certs/httpd.pem"
BrowserMatch "MSIE [2-5]" \
         nokeepalive ssl-unclean-shutdown \
         downgrade-1.0 force-response-1.0

Jetzt müssen noch die folgenden Module geladen werden:

LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so

Als letzter Schritt wird die in der Konfiguration angekündigte Datei /etc/httpd/conf/auth_file angelegt und der Service gestartet. (Die Passwort-Datei sollte mit normalen Benutzerrechten angelegt werden, damit sie dem richtigen Benutzer gehört).

# htpasswd -cs /etc/httpd/conf/auth_file felix
New password: 
Re-type new password: 
Adding password for user felix
# chown felix:users /etc/httpd/conf/auth_file
# chmod 600 /etc/httpd/conf/auth_file
# systemctl start httpd.service

Wir werden später die SVN-Repositories über den URL https://filebox/svn zugängig machen. Damit es nicht zu Überdeckungen von Verzeichnissen bzw. Dateien kommt, sollen die auf dem Server abgelegten Dateien über https://filebox/files erreichbar sein. Außerdem soll für den Fall, dass nur https://filebox/ angegeben wird, eine Art Startseite mit Links auf die (logischen) Unterverzeichnisse angezeigt werden.

Zu diesem Zwecke legen wir in /srv/http, das per Directory-Eintrag bzw. DocumentRoot als "/" angezeigt wird, einen symbolischen Link auf /mnt/files an. Außerdem wird eine Datei mit Namen index.html angelegt, die als Startseite dient.

# cd /srv/http
# ln -s /mnt/files
# vi index.html
<html>
  <head>
    <title>My stuff</title>
  </head>
  <body>
    <h1>My stuff</h1>
    <ul>
      <li><a href="files/">files/</a></li>
      <li><a href="svn/">svn/</a></li>
    </ul>
  </body>
</html>

Damit ist die grundlegende Konfiguration abgeschlossen. Wer allerdings etwas mehr Zeit investieren kann, der sollte sich die httpd.conf noch mal genau ansehen und evtl. nicht benötigen Krempel entfernen. Die Dokumentation auf der Apache-Webseite ist ziemlich gut, aber das Thema ist auch sehr umfangreich.

Einrichten von Subversion

Das Einrichten von Subversion ist jetzt relativ einfach. Zunächst wird wieder das entsprechende Paket installiert:

# pacman -S subversion

Danach wird wieder /etc/httpd/conf/httpd.conf editiert:

# vi /etc/httpd/conf/httpd.conf
<Location /svn>
    DAV svn
    SVNParentPath /mnt/svn
    SVNListParentPath On

    AuthName "My svn repository"
    AuthType Basic
    AuthUserFile /etc/httpd/conf/auth_file
    Require valid-user
</Location>

Mit SVNParentPath weisen wir das SVN-Modul an, alle im genannten Verzeichnis befindlichen Repositories zur Verfügung zu stellen. Damit spart man sich das Anpassen der Konfiguration, wenn mal ein Repository hinzukommt oder wegfällt. Die Option SVNListParentPath bewirkt, dass beim Aufrufen von https://filebox/svn/ eine Liste der verfügbaren Repositories angezeigt wird. Andernfalls gibt es eine Fehlermeldung und man muss den kompletten URL inklusive Repository-Name angeben.

Damit das Ganze funktioniert müssen noch die richtigen Module aktiviert werden.

LoadModule dav_module modules/mod_dav.so
LoadModule dav_svn_module modules/mod_dav_svn.so

Jetzt noch den Pfad anlegen und ein Beispiel-Repository erstellen:

# mkdir /mnt/svn
# chown felix:users /mnt/svn
# chmod 700 /mnt/svn/
# logout
$ cd /mnt/svn/
$ svnadmin create test
$ su -
# systemctl restart httpd.service

Jetzt noch ein kurzer Test vom Desktop-PC aus...

$ svn co https://192.168.2.101/svn/test
Error validating server certificate for 'https://192.168.2.101:443':
 - The certificate is not issued by a trusted authority. Use the
   fingerprint to validate the certificate manually!
 - The certificate hostname does not match.
Certificate information:
 - Hostname: felix
 - Valid: from Aug  9 19:27:07 2014 GMT until Aug  9 19:27:07 2015 GMT
 - Issuer: home, Bad Homburg, Hessen, DE
 - Fingerprint: 8C:E6:C1:A1:D8:69:D3:1D:6A:86:07:1E:7B:6E:67:BA:3B:80:49:90
(R)eject, accept (t)emporarily or accept (p)ermanently? p
Authentication realm:  My svn repository
Password for 'felix': ********


-----------------------------------------------------------------------
ATTENTION!  Your password for authentication realm:

   <https://192.168.2.101:443> My svn repository

can only be stored to disk unencrypted!  You are advised to configure
your system so that Subversion can store passwords encrypted, if
possible.  See the documentation for details.

You can avoid future appearances of this warning by setting the value
of the 'store-plaintext-passwords' option to either 'yes' or 'no' in
'/home/felix/.subversion/servers'.
-----------------------------------------------------------------------
Store password unencrypted (yes/no)? yes
Checked out revision 0.
$ cd test/
$ echo "Hello, World!" > hello.txt
$ svn add hello.txt 
A         hello.txt
$ svn ci -m "Erster Check-in!"
Adding         hello.txt
Transmitting file data .
Committed revision 1.
$ rm hello.txt 
$ svn up
Updating '.':
Restored 'hello.txt'
At revision 1.
$ cat hello.txt 
Hello, World!

Sieht doch prima aus. Naja, bis auf das "Your password can only be stored to disk unencrypted!". Die Dokumentation spricht, dass man dafür gnome-keyring und kwallet nutzen kann. Alternativ legt man sich ein verschlüsseltes Home-Verzeichnis zu, was ohnehin sinnvoll ist, weil man die Arbeitskopie ja auch irgendwo liegen haben muss. ;-)

Einrichten von Git

Das Einrichten von Git verdient diese Überschrift eigentlich kaum: prinzipiell ist schon alles da. Wir müssen nur noch das Paket installieren, Verzeichnis und Symlink anlegen sowie ein Repository erzeugen.

# pacman -S git
# mkdir /mnt/git
# chown felix:users /mnt/git
# chmod 700 /mnt/git
# logout
$ git init --bare /mnt/git/test.git
Initialized empty Git repository in /mnt/git/test.git/
$ git clone felix@192.168.2.101:/mnt/git/test.git
Cloning into 'test'...
felix@192.168.2.101's password: 
warning: You appear to have cloned an empty repository.
Checking connectivity... done.
$ cd test
$ echo "Hello, World!" > hello.txt
$ git add hello.txt
$ git commit -m "Erster Check-in!"
[master (root-commit) 5874773] Erster Check-in!
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
$ git push
felix@192.168.2.101's password: 
Counting objects: 3, done.
Writing objects: 100% (3/3), 235 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To felix@192.168.2.101:~/git/test.git
 * [new branch]      master -> master
$ cd ..
$ rm -rf test
$ git clone felix@192.168.2.101:/mnt/git/test.git
Cloning into 'test'...
felix@192.168.2.101's password: 
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), 235 bytes | 0 bytes/s, done.
Checking connectivity... done.
$ cat test/hello.txt 
Hello, World!

Sehr zufriedenstellend.

Nach einem Reboot...

Kommen wir zum Alptraum eines jeden (Hobby-)Administrators: es funktioniert alles, man lehnt sich entspannt zurück. Drei Wochen später muss eine Leuchte aufgehängt werden, man wirft die Sicherung raus und wird sich wenig später gewiss: nach einem Neustart funktioniert irgendwas nicht mehr. Natürlich sind die Details längst vergessen. Mal ehrlich: wer hat so etwas ähnliches nicht auch schon mal erlebt?

Blöderweise können wir nicht alles automatisch beim Bootvorgang starten:

Also bauen wir uns ein paar Skripts. Der Faulheit halber lassen wir sie gleich im Home-Verzeichnis von root liegen:

# cd /root
# vi startup.sh
#!/bin/sh

./mount-disk.sh
echo "Start httpd.service"
systemctl start httpd.service
echo "Start vsftpd.service"
systemctl start vsftpd.service
echo "Done."
# vi mount-disk.sh
#!/bin/sh

echo "Open LUKS partition"
cryptsetup luksOpen /dev/sda crypt
echo "Mount partition"
mount /dev/mapper/crypt /mnt
# vi unmount-disk.sh
#!/bin/sh

echo "Unmount partition"
umount /mnt
echo "Close LUKS partition"
cryptsetup luksClose crypt
echo "Disk can be removed now"
# chmod 744 startup.sh mount-disk.sh unmount-disk.sh 

Nach einem Reboot macht man jetzt folgendes:

$ ssh 192.168.2.101
felix@192.168.2.101's password: 
Welcome to Arch Linux ARM

     Website: http://archlinuxarm.org
       Forum: http://archlinuxarm.org/forum
         IRC: #archlinux-arm on irc.Freenode.net

Last login: Sun Aug 10 10:03:12 2014 from blackbox
$ su -
Password: 
# ./startup.sh 
Open LUKS partition
Enter passphrase for /dev/sda: 
Mount partition
Done.

Das sollte man doch hinkriegen. :-)

Das Thema Datensicherung

Bis jetzt haben wir uns viel Mühe gegeben, einen Single Point Of Failure zu bauen: wenn die Festplatte kaputt geht, die Passphrase vergessen wird oder sich der Admin irgendwo vertut und die falsche Partition löscht sind alle Daten futsch. Um das zu vermeiden sind regelmäße Backups das Mittel der Wahl.

Weil wir uns ja Mühe geben, dass nicht jeder die Daten lesen kann, ist es nur konsequent dies bei der Backup-Strategie ebenfalls zu berücksichtigen. Die Daten sollten also nie im Klartext das System verlassen. Ich verwende für meine Backups normalerweise GnuPG mit symmetrischer Verschlüsselung. Allerdings unterstützt GnuPG diese nicht im Batch-Modus. Deshalb verwendet das folgende Skript die "normale" asymmetrische Verschlüsselung mit meinem öffentlichen GnuPG-Key.

#!/bin/sh

BASEDIR=/mnt/
BACKUP=backup/backup_`date "+%Y_%m_%d"`
GPG="gpg --batch -e -r felix"
ISO="genisoimage -r -quiet -o $BASEDIR$BACKUP.iso $BASEDIR$BACKUP"

FILES=files/
GIT=git/
SVN=svn/

echo "Create working directory"
echo "=> $BASEDIR$BACKUP"
mkdir -p $BASEDIR$BACKUP

echo "Archive files from $FILES"
mkdir -p $BASEDIR$BACKUP/files
for path in $BASEDIR$FILES*
do
        THIS=`basename $path`
        TAR="tar cf - -C $BASEDIR$FILES $THIS"
        echo " * $THIS"
        $TAR | $GPG > $BASEDIR$BACKUP/files/$THIS.tar.gpg
done

echo "Archive GIT repositories"
mkdir -p $BASEDIR$BACKUP/git
for path in $BASEDIR$GIT*
do
        THIS=`basename $path`
        TAR="tar cf - -C $BASEDIR$GIT $THIS"
        echo " * $THIS"
        $TAR | $GPG > $BASEDIR$BACKUP/git/$THIS.tar.gpg
done

echo "Archive SVN repositories"
mkdir -p $BASEDIR$BACKUP/svn
for path in $BASEDIR$SVN*
do
        THIS=`basename $path`
        DUMP="svnadmin dump --quiet $path"
        echo " * $THIS"
        $DUMP | $GPG > $BASEDIR$BACKUP/svn/$THIS.dump.gpg
done

echo "Make ISO image"
echo "=> $BASEDIR$BACKUP.iso"
$ISO

echo "Clean up working directory"
rm -rf $BASEDIR$BACKUP

Folgende Anpassungen sind notwendig:

Zunächst werden alle Einzelteile eingepackt und in einem Verzeichnis abgelegt, dessen Name nach dem Schema "backup_YYYY-MM-DD" gebildet wird. Die Dateien aus dem Verzeichnis files/ werden direkt mit tar eingepackt und mit GnuPG verschlüsselt. Ebenso wird mit den GIT-Repositories verfahren. Bei den SVN-Repositories hingegen wird mit svnadmin ein Dump angelegt. Während tar die Dateien direkt und unverändert archiviert, wandelt svnadmin dump die Repositories in ein textbasiertes Format um, das später mit svnadmin load wieder geladen werden kann. Dies bietet einen guten Schutz vor inkompatiblen Repository-Formaten, wie sie manchmal bei größeren Versionssprüngen von Subversion auftreten.

Anstatt alle Dateien bzw. alle Repositories in ein großes Backup zu werfen, legt das Skript für jedes Verzeichnis auf oberster Ebene bzw. jedes Repository ein eigenes Archiv an. Das erleichtert das spätere Durchsuchen des Backups gewaltig und hat eigentlich nur den Nachteil, dass für einen Außenstehenden die Namen sichtbar werden. Am Ende wird aus dem Verzeichnis ein ISO-Image nach dem Schema "backup_YYYY-MM-DD.iso" gebildet und das Arbeitsverzeichnis gelöscht. Das ISO-Image kann dann per FTP runtergeladen und auf einem anderen Rechner archiviert oder auf eine CD/DVD gebrannt werden.

Selbstredend ist es wichtig, dass der Private Key für das Entschlüsseln des Backups sicher verwahrt wird.

Das Ergebnis

Als ich angefangen habe PCs zu benutzen waren CDROM-Laufwerke mit 300 KB/s aktuell und Leute die ein Triple-Speed-Laufwerk hatten wurden schon komisch angesehen ("Wozu braucht man denn so etwas?"). Von daher hätte ich mich mit ähnlichen Datenraten zufrieden gegeben, schließlich sind die einzelnen Dateien mit denen ich arbeite so zwischen 100 Kilobyte bis vielleicht 10 Megabyte groß. Umso angenehmer war ich überrascht: Lesen mit 3,3 MB/s und Schreiben mit 2,4 MByte/s! Und das war gemessen via FTP durch TLS auf die durch dm-crypt verschlüsselte Platte. Alle Achtung. Deutlich langsamer hingegen läuft das Anfertigen des Backups. Für 100 Megabyte kann man mit 3 Minuten rechnen, das Gigabyte also für eine halbe Stunde. Aber auch das stört mich nicht, das kann ja irgendwann nebenher angestoßen werden.

Alles in allem bin ich mit der Lösung zufrieden. Sie erfüllt meine momentanten Ansprüche und kann leicht auf eine schnellere Hardware (z.B. einen Banana Pi mit Gigabit-LAN und SATA-Anschluß) übertragen werden. Einzig und alleine über einen komfortableren Zugang wie etwa NFS denke ich nach, nur lässt sich das wohl nicht so einfach durch TLS leiten. Vielleicht wird der Raspberry Pi demnächst noch IPsec lernen...