Deployn

Ubuntu Homeserver einrichten (Intel-Nuc)

Ausführliche Schritt-für-Schritt Anleitung für die Einrichtung eines Ubuntu-Homeservers mit Docker und Docker-Compose.

Ubuntu Homeserver einrichten (Intel-Nuc)-heroimage

In diesem Blogpost möchte ich einen Homeserver mit Ubuntu und Docker einrichten. Dieser soll das erste Gerät hinter dem Router werden, der sich unter anderem um externe Anfragen kümmert.

Voraussetzungen

  • Server, auf dem Ubuntu installiert werden kann
  • Monitor und Tastatur für die Installation
  • Internetverbindung (bestenfalls ohne DS-Lite)
  • eigene Domain
  • USB-Stick oder SD-Karte mit mindestens 2 GB freiem Speicher
  • Computer (Client), um den Server einzurichten (zum Beispiel ein Windows-PC)
  • bestenfalls Router, der sich um das DDNS und VPN kümmert (zum Beispiel Fritz!Box 7590) (Affiliate-Link)

Ubuntu Server

Die Voraussetzungen für diese Anleitung ist ein Computer auf dem Ubuntu installiert werden könnte. Es kann prinzipiell fast jeder Desktop-PC oder Laptop sein. Ich selbst nutze einen

Intel NUC

(Amazon Affiliate-Link).

Intel Nuc

Grundsätzlich könnte man (wie auch ich in der Vergangenheit) auch einen Raspberry Pi dafür nehmen, jedoch habe ich das Gefühl, dass ein Raspberry Pi als “Hauptserver” nicht ideal ist. Ein Intel Nuc bietet demgegenüber einige Vorteile (und leider auch Nachteile).

Intel Nuc

Selbst der Nuc mit einem J4005-Prozessor ist leistungsstärker als ein Raspberry Pi 4. Wichtig ist dabei auch, dass es keine ARM-CPU wie beim Raspberry ist, wodurch einige Kompatibilitätsprobleme wegfallen.

Ein SATA Anschluss ist vorhanden, beim Raspberry Pi muss man diesen nachrüsten oder kann nur einen USB-Anschluss verwenden. Alternativ auch microSD Karten, die gefühlt sehr schnell darin kaputtgehen. Wenn man möchte, kann man in einen Intel Nuc theoretisch mehr RAM einbauen.

Ein Nachteil ist die Lautstärke, der Lüfter läuft ständig. Der Raspberry Pi ist standardmäßig passiv gekühlt.

Die Anschaffungskosten sind höher. Der Preis für ein Intel Nuc mit einem J4005, 8 GB RAM und einer 512 GB SSD liegt insgesamt bei ungefähr 240 €.

HardwarePreis (ca.)
Intel NUC*135 €
Gskill 2x4GB RAM*45 €
Crucial 500GB SSD*60 €
*Affiliate-Link

Der Stromverbrauch ist höher, jedoch nur geringfügig. Im Idle benötigt der Intel Nuc mit dem J4005 Prozessor ca. 4,5 Watt. Der Raspberry Pi 4 braucht ca. 3,5 Watt. Bei mir fungiert der Nuc als Proxy Manager, DNS-Server, außerdem laufen mehrere andere Dienste da drauf. Mit dem Brennenstuhl Energiemessgerät (Amazon Affiliate-Link) habe ich den Stromverbrauch gemessen und komme durchschnittlich auf einen Verbrauch von ca. 10-11 Watt. Mit TLP lässt er sich noch etwas reduzieren.

Stromkostenrechner

Hier Daten eintragen, um die Stromkosten zu berechnen:

Ergebnis



Andere Optionen

Natürlich gibt es weitaus mehr Optionen, die man nutzen könnte. Zum Beispiel ist es möglich einen nicht mehr genutzten Computer zum Server umzufunktionieren oder auch sich selbst einen Server zusammenzustellen.

Bei einem älteren Computer hat man eventuell das Problem, dass er nicht energieeffizient arbeitet. Beim Selbstbau hat man definitiv die höchsten Anpassungsmöglichkeiten und die potenziell höchste Leistung. Dies kann jedoch schnell sehr teuer werden. Interessant sind jedenfalls auch (ggf. gebrauchte) PCs mit einer nicht ganz so stromhungrigen CPU. Zum Beispiel die kleinen ThinkCentre von Lenovo Amazon Alliliate Link .

An dieser Stelle möchte ich nicht vergessen, auf die Quellen zu verweisen, von denen ich einige Informationen übernehme. Das ist zum einen insbesondere das “Silverbox” Repository von ovk. Außerdem auch das Docker-Traefik Repository von htpcBeginner.

Installation

Nachdem die Wahl auf ein Gerät gefallen ist, muss das Betriebsystem installiert werden.

Vorbereitung

Bei einem NUC müssen ggf. noch die Einzelteile eingebaut und das BIOS geupdated werden.



Bei einem Nuc müssen die Festplatte und der Arbeitsspeicher in das Gehäuse gepackt werden.

Eingebauter RAM

Anschließend kann der Nuc mit einem Monitor und Tastatur verbunden werden.

Beim ersten Start sollte nicht das Betriebssystem, sondern ein BIOS-Update eingespielt werden, wenn eins vorhanden ist. Dazu die BIOS-Dateien auf den USB-Stick packen.

USB-Verzeichnis für BIOS-Update

Anschließend die Anleitung vom Hersteller befolgen (im Grunde BIOS über F2 aufrufen und dort zum BIOS-Update navigieren).

Wenn man sich sowieso schon im BIOS befindet, kann man auch die Dinge ausschalten, die man nicht nutzen möchte, z. B. das WLAN.

Als Betriebssystem möchte ich Ubuntu 20.04 Server installieren. Ubuntu 20.04 (Focal Fossa) wird noch bis 2025 mit Updates versorgt. Im Gegensatz zur Desktop-Variante hat Ubuntu Server keine grafische Oberfläche.

Um einen bootbaren USB-Stick zu erstellen, muss die ISO-Datei mithilfe von Rufus auf einen USB-Stick geschrieben werden.

Rufus

Mit diesem USB-Stick kann dann der Server booten, ggf. muss beim Start das Boot-Menü aufgerufen werden (beim Nuc lässt sich das BIOS standardmäßig mit F2 öffnen).

Anschließend kann Ubuntu installiert werden.

Eventuell fragt der Installer, ob man eine aktuellere Version herunterladen möchte, dies sollte bejaht werden.

Installations Start Dialog Sprachauswahl

Feste IP-Adressenzuweisung

Bei der Interneteinstellung kann von der automatischen IP-Adressen Vergabe auf manuell gewechselt werden. Für mich ist es bei IPv4 wichtig, da ich IPv6 nicht nutzen werde. Das Subnet ist für gewöhnlich ein Block aus mehreren Netzwerkadressen. Standardmäßig ist es 192.168.0.0/24, 192.168.1.0/24 oder 192.168.178.0/24. Die 24 steht dafür, dass es sich beispielsweise um den Bereich 192.168.0.1 - 192.168.0.254 handelt. Das Gateway ist meistens die IP-Adresse des Modems.

Unter Windows kann man das Standard-Gateway und die eigene IP-Adresse herausfinden, indem man das Terminal aufruft und ipconfig eingibt. Bei macOS geht es mit route get default | grep gateway.

Gibt man manuell eine Adresse ein, ist es vorteilhaft eine zu wählen, die sich außerhalb des DHCP-Bereichs des Routers (oder anderen DHCP-Servers) befindet. Eventuell muss dafür der DHCP-Bereich verkleinert werden.

Bei einer Fritz!Box befindet sich diese Einstellungsmöglichkeit unter Netzwerk > Netzwerkeinstellungen > IPv4-Einstellungen

Fritz!Box DHCP-Einstellungen DHCP Einstellung

Volumen-Aufteilung

Im nächsten Schritt muss die Festplatte partitioniert werden. Auch hier hat man wieder mehrere Möglichkeiten. Ich wähle zunächst aus, dass ich ein custom layout möchte.

Manche Computer haben Schwierigkeiten, Boot-Dateien zu erkennen, die nicht am Anfang der Festplatte gespeichert sind. Aus diesem Grund ist es manchmal notwendig, eine separate /boot-Partition am Anfang der Festplatte zu erstellen. Deshalb möchte ich eine 1 GB große ext4 Partition für das Verzeichnis /boot haben.

Anschließend nutze ich beinahe den gesamten freien Speicherplatz für ein Ubuntu - LVM. Dieser Teil des Speichers kann auf Wunsch verschlüsselt werden. Leider ist mir nicht richtig bewusst, ob eine Verschlüsselung zusätzliche Sicherheit (außer bei einem analogen Diebstahl) bietet.

Auf diesem Volumen-Manager füge ich zwei logische Volumen hinzu. Der erste Teil dient als Swap. Auf dem zweiten werden die gewöhnlichen Daten gespeichert.

Die passende Speichergröße hängt von mehreren Faktoren ab und kann nicht im Allgemeinen angegeben werden. Ubuntu Users

Für Ubuntu ist folgende Swap-Größe empfohlen:

Empfohlene SWAP-Größe:
Mindestens: 0 GB | Maximal: 0 GB

Der einzige Nachteil von mehr Swap ist es, dass der Speicherplatz dafür reserviert werden muss. Zudem ist zu berücksichtigen, dass der Zugriff auf die SSD langsamer erfolgt im Vergleich zum Arbeitsspeicher und die SSD-Zellen eine begrenzte Lebensdauer haben. Jeder Schreib-Zyklus nutzt eine Speicherzelle ab, und irgendwann funktioniert sie nicht mehr.

Braucht man eine Swap-Partition, die größer ist als das Doppelte der RAM-Größe, sollte man definitiv über ein Upgrade des Arbeitsspeichers nachdenken.

Den restlichen Platz nutze ich als gewöhnliche ext4 Partition, die unter / gebootet werden soll.

Volumen Einstellung

Im Gegensatz zu meinem GIF ist es nicht erforderlich oder sinnvoll, den gesamten Platz der “Volume Group” zuzuweisen, um leichter auf spätere Veränderungen reagieren zu können. Einige GB können auch nicht zugewiesen werden.

Abschluss

Ich nutze die Option, dass OpenSSH automatisch installiert wird. Damit möchte ich später auf den Server zugreifen können. OpenSSH Installation

Die zusätzlichen Dienste interessieren mich nicht, da ich diese nicht benötige. Zusätzliche Programme

Anschließend folge ich den Anweisungen aus dem Installer. Nachdem die Installation abgeschlossen ist, muss letztlich das bootbare Speichermedium herausgenommen werden. Der Server fährt dann normal hoch und fragt nach dem Passwort für die Festplattenverschlüsselung.

Vorsicht! Das Layout der Tastatur könnte sich inzwischen geändert haben. Sind im Passwort Sonderzeichen oder die Buchstaben Y und Z enthalten, könnten sie zu diesem Zeitpunkt anders gemappt sein.

Sagt der Server, dass das Passwort falsch sei, lohnt es sich, es unter Beachtung einer Tastatur mit Qwerty-Layout nochmal zu versuchen.

QWERTY-Tastaturlayout

Nach Eingabe des richtigen PassworTs wird die Anmeldemaske angezeigt. Wir wollen uns nicht direkt anmelden. An dieser Stelle kann zur Sicherheit im Router dem Server eine feste IP-Adresse zugewiesen werden.

Feste IP-Adresse

Die Installation ist damit fertig.

Erste Schritte

SSH-Verbindung herstellen

Jetzt sollte es möglich sein, sich mit dem Server über SSH zu verbinden. Ich empfehle die Programme Git (inklusive Git Bash) sowie VSCode (mit der Remote - SSH Erweiterung) zu installieren (beides kostenlos). Alternativ lässt sich aber auch das gewöhnliche Terminal verwenden.

Bei Windows muss der OpenSSH-Client eventuell noch über die Systemsteuerung > Apps > Optionale Features hinzugefügt werden.

OpenSSH Client

Im Terminal sollte es danach möglich sein, sich zu verbinden:

ssh benutzername@ip-adresse

Bei Visual Studio Code kann man sich mit dem Server verbinden, indem man auf das Icon unten links drückt (bei mir ist es gelb).

Leere VSCode Instanz

Danach kann man auch dort ein Terminal aufrufen.

Server updaten

Als Nächstes sollte der Server upgedatet werden.

apt update

Zugriff verweigert

sudo apt update
sudo apt upgrade

Jetzt sollte ein Upgrade durchgeführt werden. Eventuell kommt aber auch die Fehlermeldung, dass bestimmte URLs nicht aufgelöst werden konnten. Dieses Problem lässt sich vermutlich durch einen Wechsel des DHCP-Servers beheben:

sudo ls /etc/netplan

Der Befehl ls /etc/netplan gibt eine Liste aller Dateien im Verzeichnis /etc/netplan aus. Eine oder zwei Dateien sollten darin enthalten sein. Möchte man mehr Informationen sowie versteckte Dateien sehen, kann man stattdessen auch sudo ls -Al /etc/netplan ausführen.

Im Verzeichnis sollte sich mindestens eine Datei befinden, die wollen wir anpassen.

sudo vim /etc/netplan/00-installer-config.yaml

Vim ist ein Texteditor. Um eine Datei zu verlassen, muss man :q eingeben. Um eine Datei anzupassen, muss man mit I in den Insert-Modus wechseln. Nachdem die Bearbeitung fertig ist, verlässt man den Insert-Modus mit Esc wieder und kann mit :x speichern und verlassen oder mit :q! verlassen ohne zu speichern. Vorteil von Vim ist, dass es fast überall standardmäßig installiert ist.

network:
    ethernets:
        eth0:
            dhcp4: no
            addresses: [192.168.123.5/24]
            gateway4: 192.168.123.1
            nameservers:
                addresses: [8.8.8.8]
    version: 2

Der Netzwerkname (in meinem Fall “eth0”) sollte nicht verändert werden. Demgegenüber müssen aber die restlichen Adressen angepasst werden. Die Adresse des Servers ist eventuell eine andere, die “24” steht für das Subnetz. Das Gateway ist die Adresse des Routers. Beim Nameservers.addresses kann die Adresse eines DNS-Servers eingetragen werden, wie z. B. 8.8.8.8 für Google DNS oder 1.1.1.1 für Cloudflare DNS oder 176.9.93.198 für dnsforge.de. Das ist nur eine temporäre Einstellung, die wir später wieder ändern werden.

sudo netplan apply
sudo apt update
sudo apt upgrade

Diesmal müsste das Update klappen.

IPv6 deaktivieren

Obwohl mir klar ist, dass IPv6 die Zukunft ist, habe ich immer wieder damit Probleme. Bei dem letzten Server mit Unbound hat das interne Routing nicht funktioniert, bis ich IPv6 ausgeschaltet habe. Das mache ich dieses Mal gleich. Sobald ich irgendwann gezwungen bin, auf IPv4 zu verzichten, werde ich mich erneut mit IPv6 beschäftigen.

Ich deaktiviere IPv6 in meinem Router. Bei meiner Fritz!Box befindet sich die Möglichkeit dazu unter Internet > Zugangsdaten > IPv6. IPv6 im Router deaktivieren

Danach deaktiviere ich das auch auf meinem Server.

sudo vim /etc/default/grub

In dieser Datei ändere ich die folgenden zwei Zeilen von

GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX=""

auf

GRUB_CMDLINE_LINUX_DEFAULT="ipv6.disable=1"
GRUB_CMDLINE_LINUX="ipv6.disable=1"

wende die neue Konfiguration an und starte den Server neu.

sudo update-grub
sudo reboot

Das Blöde ist, dass man wieder am Server das Entschlüsselungspassword eingeben muss, bevor man sich über SSH mit ihm verbinden kann. Das Tastaturlayout sollte dieses Mal das der Tastatur sein, weil es inzwischen nachgeladen sein sollte.

Dropbear

Ich würde gerne nicht jedes Mal eine Tastatur und Monitor anschließen müssen, wenn ich den Server neu starte, sondern ihn auch von meinem Client-PC entschlüsseln können. Dazu installiere ich das Programm Dropbear.

sudo apt install dropbear-initramfs

Während der Installation erscheinen einige Fehlermeldungen, die zu diesem Zeitpunkt irrelevant sind. Danach ändere ich den Port des Dropbear Servers:

sudo vim /etc/dropbear-initramfs/config

Die Zeile

#DROPBEAR_OPTIONS=""

muss auskommentiert (das # am Anfang der Zeile, dass es nur ein Kommentar ist) und um ein paar Optionen ergänzt werden.

DROPBEAR_OPTIONS="-I 180 -j -k -p 2222 -s"

Jetzt brauche ich einen SSH-Schlüssel zur Authentifizierung. Dazu erstelle ich auf meinem Client-PC eine neue Datei im Verzeichnis ~/.ssh (~ ist das Home-Verzeichnis) und nenne sie zum Beispiel “nuc_dropbear” (ohne Dateiendung). Dafür müssen Dateiendungen sowie die Anzeige versteckter Ordner aktiviert werden.

Nun rufe ich ein neues Terminal auf.

ssh-keygen -t rsa -f ~/.ssh/nuc_dropbear

Bei der Frage, ob die Datei überschrieben werden soll, antworte ich mit ja. Unter Windows sagt das Terminal eventuell, dass es diese Datei nicht gibt. In diesem Fall stattdessen den kompletten Pfad eingeben:

ssh-keygen -t rsa -f C:\Users\benutzername\.ssh\nuc_dropbear

Die Datei mit der .pub-Endung öffne ich (mit einem Texteditor, nicht mit Microsoft Publisher) und kopiere den Inhalt.

Zurück auf dem Server:

sudo vim /etc/dropbear-initramfs/authorized_keys

In diese neue Datei füge ich den Inhalt der Datei ...\nuc_dropbear.pub ein.

Danach update ich den initrams.

sudo update-initramfs -u

Jetzt erfolgt der Moment, in dem die neue Konfiguration getestet werden kann.

sudo reboot

Diesmal verbinden wir uns mit dem Server aber nicht über Port 22, sondern über Port 2222. Außerdem mit dem Benutzer root und nicht unserem gewöhnlichen Benutzerkonto.

ssh -o "HostKeyAlgorithms ssh-rsa" -i ~/.ssh/nuc_dropbear -p 2222 root@ip-adresse

Nachdem die Verbindung dann hergestellt wurde:

cryptroot-unlock

Nachdem das Passwort zur Entschlüsselung eingegeben wurde, wird die Verbindung getrennt. Anschließend kann man sich wieder auf gewohntem Weg anmelden.

ICMP Weiterleitungen unterbinden

Als Nächstes deaktiviere ich ICMP Weiterleitungen aus Sicherheitsgründen.

sudo vim /etc/sysctl.conf

Dort kommentiere ich die folgende Zeile aus:

net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0

ICMP Weiterleitungen unterbinden

Deinstallation von Snapd

Ich deinstalliere Snapd

sudo apt autoremove --purge snapd

TRIM zur verschlüsselten Partition hinzufügen

Ich füge die TRIM-Funktion zur verschlüsselten Partition hinzu. Dies ist jedoch mit Sicherheitsrisiken verbunden.

Wenn es zwingend erforderlich ist, dass Informationen über ungenutzte Sektoren für Angreifer nicht verfügbar sein dürfen, muss TRIM immer deaktiviert werden. Quelle

sudo vim /etc/crypttab

Hinter das Wort “luks” füge ich “,discard” ein.

Auslagerung von temporären Dateien

Ich verschiebe den Ordner /tmp von der SSD in den RAM. Das bringt Vorteile, weil RAM schneller ist und die SSD nicht so stark durch viele kleine Schreibvorgänge belastet wird. Man sollte auch die Nachteile kennen, der RAM kann überlaufen, die Dateien im Verzeichnis verschwinden beim Neustart und es gibt ein Sicherheitsrisiko bei Mehrbenutzersystemen, da die temporären Dateien ggf. durch einen anderen Nutzer ausgelesen werden könnten. Näheres zur Auslagerung lässt sich in diesem Wiki nachlesen.

sudo vim /etc/fstab

Ich füge eine neue Zeile hinzu:

tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,mode=1777,size=2G 0 0

Mit dieser Einstellung darf das Verzeichnis höchstens 2 GB groß werden. Hat man weniger als 8 GB RAM eingebaut, sind 2 GB eher zu viel.

Zeit, den Server wieder neu zu starten.

sudo reboot

SSH-Verbindung ändern

Ich möchte einen Schlüssel für die gewöhnliche Anmeldung erstellen, wie auch bei Dropbear. Dazu öffne ich wieder ein neues Terminal auf meinem Client-Rechner.

ssh-keygen -t rsa -f ~/.ssh/nuc

Den nuc.pub Inhalt kopiere ich und füge ihn beim Server ein.

cd ~
mkdir .ssh
sudo vim ~/.ssh/authorized_keys

Mit cd wechsle ich das Verzeichnis auf ~. Mit mkdir erstelle ich ein neues Verzeichnis “.ssh”. In die neue Datei füge ich den kopierten Inhalt ein.

Bei der Nutzung von VSCode passe ich die SSH-Konfigurationsdatei an. Aufgerufen wird sie auch über das SSH-Menü.

Host Nuc
  HostName ip-adresse des Servers
  IdentityFile ~/.ssh/nuc
  User benutzername
[...]

Jetzt sollte ich mich ohne Passwort anmelden können.

Sollte das nicht klappen, kann das kopieren auch mit cat probieren. Dazu zunächst den Ordner auf dem Server wieder löschen.

sudo rm -r ~/.ssh

Dann auf dem Client Compouter ein Terminal starten.

cat ~/.ssh/nuc.pub | ssh benutzer@ip-adresse "mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod -R go= ~/.ssh && cat >> ~/.ssh/authorized_keys"

Nun sollten noch die SSH-Einstellungen des Servers angepasst werden.

sudo vim /etc/ssh/sshd_config

Ich ändere einige Einstellungen:

#	$OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $

# This is the sshd server system-wide configuration file.  See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented.  Uncommented options override the
# default value.

Include /etc/ssh/sshd_config.d/*.conf

#Port 22
AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

# Ciphers and keying
#RekeyLimit default none

# Logging
#SyslogFacility AUTH
#LogLevel INFO

# Authentication:

LoginGraceTime 1m
PermitRootLogin no
#StrictModes yes
MaxAuthTries 4
MaxSessions 5

PubkeyAuthentication yes

# Expect .ssh/authorized_keys2 to be disregarded by default in future.
#AuthorizedKeysFile	.ssh/authorized_keys .ssh/authorized_keys2

#AuthorizedPrincipalsFile none

#AuthorizedKeysCommand none
#AuthorizedKeysCommandUser nobody

# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
HostbasedAuthentication no
# Change to yes if you don't trust ~/.ssh/known_hosts for
# HostbasedAuthentication
#IgnoreUserKnownHosts no
# Don't read the user's ~/.rhosts and ~/.shosts files
IgnoreRhosts yes

# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
PermitEmptyPasswords no

# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
ChallengeResponseAuthentication no

# Kerberos options
#KerberosAuthentication no
#KerberosOrLocalPasswd yes
#KerberosTicketCleanup yes
#KerberosGetAFSToken no

# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes
#GSSAPIStrictAcceptorCheck yes
#GSSAPIKeyExchange no

# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication.  Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
UsePAM yes

AllowAgentForwarding no
AllowTcpForwarding local
#GatewayPorts no
X11Forwarding no
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes
PrintMotd no
#PrintLastLog yes
TCPKeepAlive no
#PermitUserEnvironment no
#Compression delayed
ClientAliveInterval 60
ClientAliveCountMax 2
#UseDNS no
#PidFile /var/run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none

# no default banner path
Banner none

# Allow client to pass locale environment variables
AcceptEnv LANG LC_*

# override default of no subsystems
Subsystem sftp	/usr/lib/openssh/sftp-server

# Example of overriding settings on a per-user basis
#Match User anoncvs
#	X11Forwarding no
#	AllowTcpForwarding no
#	PermitTTY no
#	ForceCommand cvs server
PasswordAuthentication yes

Damit eine Verbindung hergestellt werden kann, muss ggf. der Server aus den known_hosts im Verzeichnis ~/.ssh/ auf dem Client-PC entfernt werden.

Docker-Installation

Wir installieren Docker.

Installation Docker Engine

sudo apt install ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt-get install docker-ce docker-ce-cli containerd.io

Installation Docker Compose

sudo mkdir -p /usr/local/lib/docker/cli-plugins/
sudo curl -SL https://github.com/docker/compose/releases/download/v2.23.3/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose

Auf der Release-Page in Github kann man sehen, ob es eine neuere Version gibt. In diesem Fall müsste die URL angepasst werden.

Test Docker

Wir schauen uns mal an, ob Docker funktioniert.

sudo docker run hello-world
docker compose version

Wenn keine Fehler aufgekommen sind, löschen wir den soeben erstellen Container und das Image wieder.

sudo docker container prune
sudo docker image prune -a

DNS

Der Server soll unseren DNS managen. Adguard Home wird dafür zuständig sein.

Installation Adguard Home

Als Erstes installieren wir AdGuard Home (eine Alternative zu pi-hole, NextDNS oder Blocky). Ich möchte das über Docker laufen lassen. Für meine Container erstelle ich mir ein neues Verzeichnis.

sudo mkdir -p /root/docker/containers
sudo chmod 700 /root/docker/containers
sudo mkdir /root/docker/containers/adguard
sudo chmod 700 /root/docker/containers/adguard

Und für die Daten erstelle ich ein neues Verzeichnis.

sudo mkdir -p /srv/adguard/data
sudo mkdir /srv/adguard/data/work
sudo mkdir /srv/adguard/data/conf
sudo chmod -R 750 /srv/adguard

Nun erstelle ich eine neue Docker-Compose Datei.

sudo vim /root/docker/containers/adguard/docker-compose.yml

Da kommt folgender Inhalt rein:

version: "3"

services:
    adguard:
        image: adguard/adguardhome:latest
        container_name: adguard
        restart: unless-stopped
        ports:
            - "53:53/tcp"
            - "53:53/udp"
            - "80:80/tcp"
            - "3000:3000/tcp"
        volumes:
            - /srv/adguard/data/work:/opt/adguardhome/work
            - /srv/adguard/data/conf:/opt/adguardhome/conf
        environment:
            TZ: Europe/Berlin

Zeit, den Container zu starten.

sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml up -d

Dies sollte mit einem Fehler enden, weil Port 53 nicht mehr frei ist.

Port 53 wird bereits verwendet

Mit sudo netstat -lnptu kann man sehen, welche Ports verwendet werden. 53 wird vom systemd-resolv verwendet. Das ändern wir.

sudo vim /etc/systemd/resolved.conf

Hier die Zeile #DNSStubListener=yes auskommentieren und zu DNSStubListener=no ändern. Danach den Resolver und den Container neu starten.

sudo systemctl restart systemd-resolved
sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml up -d

Sollte kein Fehler aufgetreten sein, kann man versuchen über einen Webbrowser das Dashboard zu erreichen (unter Port 3000), also zum Beispiel 192.168.123.150:3000.

Hier sollte einen bereits der Installer begrüßen.

Adguard Installer

Die zweite Seite lasse ich so wie sie ist, auf der dritten erstelle ich ein Benutzerkonto. Andere Einstellungen nehme ich vorerst nicht vor, sondern melde mich mit meinem gerade erstellten Konto an.

Unter Filter > DNS-Sperrliste lassen sich Sperrlisten hinzufügen, einige Listen werden bereits vorgegeben. Nachdem man alle Listen ausgewählt hat, die man nutzen möchte, sollte man einmal auf “Nach Updates suchen” drücken. Die Aktivierung im Router nehmen wir später vor. Es ist vermutlich auch sinnvoll eine Whitelist hinzuzufügen oder sich selbst eine zu erstellen. Im Internet finden sich einige, zum Beispiel die Whitelist von Hl2Guide oder eine kürzere Whitelist in meinem Repository.

Ich stoppe den Adguard Container wieder und bearbeite ihn.

sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml down
sudo vim /root/docker/containers/adguard/docker-compose.yml
version: "3"

services:
    adguard:
        image: adguard/adguardhome:latest
        container_name: adguard
        restart: unless-stopped
        ports:
            - "53:53/tcp"
            - "53:53/udp"
            - "85:80/tcp"
        volumes:
            - /srv/adguard/data/work:/opt/adguardhome/work
            - /srv/adguard/data/conf:/opt/adguardhome/conf
        environment:
            TZ: Europe/Berlin

Ich habe Port 3000 wieder entfernt, und den öffentlichen Port von 80 auf 85 geändert (80 brauche ich später für andere Dienste).

Danach ändern wir den Resolver vor dem Start in der Konfiguration.

sudo mkdir /etc/systemd/resolved.conf.d
sudo vim /etc/systemd/resolved.conf.d/adguard.conf

In die neue Datei kommt folgender Text:

[Resolve]
DNS=127.0.0.1
DNSStubListener=no

Diese Konfiguration müssen wir noch aktivieren:

mv /etc/resolv.conf /etc/resolv.conf.backup
ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved

Wir ändern auch die Datei, die wir ganz am Anfang geändert haben.

sudo vim /etc/netplan/00-installer-config.yaml

Hier kommt als Nameserver 127.0.0.1 rein.

sudo netplan apply

Nun kann der Container gestartet werden.

sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml up -d

Die Weboberfläche sollte nun auf Port 85 zu erreichen sein.

Router-Einstellung

Damit der DNS-Server auch von den Geräten in meinem Netzwerk verwendet wird, muss ich die Einstellungen im Router ändern.

Lokaler DNS-Server Bei der Fritz!Box findet sich die Einstellung bei Netzwerk > Netzwerkeinstellungen > IPv4-Adressen. Dort trage ich die IP-Adresse des Servers ein.

Auch unter Internet > Zugangsdaten > DNS-Server trage ich als bevorzugten DNS-Server die IP-Adresse des Adguard-Servers ein.

Im Dashboard von AdGuard sollte indessen zu sehen sein, dass DNS-Abfragen vom Server bearbeitet wurden.

AdGuard Dashboard

Proxy-Server

Als Nächstes möchte ich einen Proxyserver einrichten. Zur Wahl stehen Traefik, Caddy, Nginx und Apache und weitere.

Installation Nginx Proxy Manager

Ich möchte dieses Mal den Nginx Proxy Manager benutzen. Dafür brauche ich ein neues Verzeichnis für den Container und für die Daten.

sudo mkdir /srv/nginxproxymanager
sudo mkdir /srv/nginxproxymanager/data
sudo mkdir /srv/nginxproxymanager/certs
sudo mkdir /srv/nginxproxymanager/db
sudo chmod -R 750 /srv/nginxproxymanager
sudo mkdir /root/docker/containers/nginxproxymanager
sudo chmod 700 /root/docker/containers/nginxproxymanager

Ich erstelle ein Netzwerk für den Proxy-Server.

sudo docker network create npm

Dann erstelle ich noch einen Ordner für meine Docker Secrets.

sudo mkdir /root/docker/secrets/
sudo chown root:root /root/docker/secrets/
sudo chmod 600 /root/docker/secrets/

Da kommen mein Passwort und mein Root-Passwort für die Datenbank rein.

sudo bash -c 'echo $(openssl rand -base64 32) > /root/docker/secrets/npm-password.txt'
sudo bash -c 'echo $(openssl rand -base64 32) > /root/docker/secrets/npm-root-password.txt'

Jetzt muss die Docker Compose Datei erstellt werden.

sudo vim /root/docker/containers/nginxproxymanager/docker-compose.yml
version: "3.7"

networks:
    npm:
        name: npm
    internal:
        external: false

secrets:
    npm-password:
        file: /root/docker/secrets/npm-password.txt
    npm-root-password:
        file: /root/docker/secrets/npm-root-password.txt

services:
    npm-app:
        image: "jc21/nginx-proxy-manager"
        container_name: npm-app
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
            - "81:81"
        environment:
            DB_MYSQL_HOST: "npm-db"
            DB_MYSQL_PORT: 3306
            DB_MYSQL_USER: "npm"
            DB_MYSQL_NAME: "npm"
            DB_MYSQL_PASSWORD__FILE: /run/secrets/npm-password
            DISABLE_IPV6: "true"
        networks:
            - npm
            - internal
        volumes:
            - /srv/nginxproxymanager/data:/data
            - /srv/nginxproxymanager/certs:/etc/letsencrypt
        secrets:
            - npm-password

    npm-db:
        image: "jc21/mariadb-aria"
        container_name: npm-db
        restart: unless-stopped
        environment:
            MYSQL_ROOT_PASSWORD__FILE: /run/secrets/npm-root-password
            MYSQL_DATABASE: "npm"
            MYSQL_USER: "npm"
            MYSQL_PASSWORD__FILE: /run/secrets/npm-password
        networks:
            - internal
        volumes:
            - /srv/nginxproxymanager/db:/var/lib/mysql
        secrets:
            - npm-password
            - npm-root-password

Anschließend muss dieser Stack gestartet werden.

sudo docker compose -f /root/docker/containers/nginxproxymanager/docker-compose.yml up -d

Am besten schaut man noch, ob alles funktioniert, oder bereits Fehlermeldungen auftauchen.

sudo docker logs npm-app

Sollte alles soweit ok sein, kann man sich unter der IP-Adresse des Servers und dem Port 81 mit dem Standardkonto anmelden.

Benutzername: admin@example.com
Password: changeme

Am Ende sollte man im Dashboard landen.

Dashboard des Nginx Proxy Managers

DDNS

Als Nächstes sollte eine Domain eingerichtet werden, die auf das Heimnetzwerk geroutet wird. Die dynamische Anpassung der DNS-Einträge kann die Fritz!Box übernehmen. Ist das nicht möglich, kann man auch ein Script auf dem Ubuntu-Server nutzen. Ich fasse hier kurz die Schritte zusammen.

Domain Update Script

Zunächst sollte eine Domain gekauft werden. Der Anbieter meiner Wahl ist netcup (Affiliate Link). Hier kostet eine .de-Domain dauerhaft 5 Euro im Jahr.

Anschließend werden ein API-Key und das API-Password von netcup benötigt. Dies erhält man im CCP

Wenn wir keinen Webspace bei Netcup haben, brauchen wir PHP um die DNS Einträge zu aktualisieren. Statt das Skript von Lars-Sören Steck direkt auszuführen, packe ich es in einen Docker Container.

sudo mkdir /root/docker/containers/ddns
sudo chmod 700 /root/docker/containers/ddns
sudo git clone https://github.com/stecklars/dynamic-dns-netcup-api /root/docker/containers/ddns/script
sudo vim /root/docker/containers/ddns/Dockerfile

Jetzt erstellen wir das Dockerfile.

FROM php:8.1-cli
COPY ./script .
ENTRYPOINT ["php", "./update.php"]

Es ist kurz, sollte aber den Zweck erfüllen.

Danach muss noch das config angepasst werden.

sudo cp /root/docker/containers/ddns/script/config.dist.php /root/docker/containers/ddns/script/config.php
sudo vim /root/docker/containers/ddns/script/config.php

Hier muss die Kundennummer geändert werden, der API-Key und das API-Password. Die Zeile mit DOMAINLIST muss am Ende ungefähr so aussehen:

...
define('DOMAINLIST', 'meinedomain.de: *');
...

Damit werden alle Subdomains der Domain meinedomain.de aktualisiert, die nicht eine spezifischere Zuordnung haben.

Dann brauchen wir noch die Docker Compose Datei.

sudo vim /root/docker/containers/ddns/docker-compose.yml
version: "3.7"

services:
    ddns:
        container_name: ddns
        build:
            context: /root/docker/containers/ddns
            dockerfile: Dockerfile
        image: ddns

Wir testen den Container mit und schauen uns die Logs an:

sudo docker compose -f /root/docker/containers/ddns/docker-compose.yml up --build

Erfolgreiches Update

Automatisches DDNS-Update

Nun muss der Container noch automatisch gestartet werden.

sudo vim /etc/systemd/system/ddns.service
[Unit]
Description=Dynamischer DNS Service
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
ExecStart=/usr/bin/docker compose -f /root/docker/containers/ddns/docker-compose.yml up --build

Auch hier testen wir, ob alles funktioniert:

sudo systemctl daemon-reload
sudo systemctl start ddns.service
sudo systemctl status ddns.service

Es dürfte im Log stehen, dass die IP-Adresse nicht aktualisiert wurde, sondern gleich geblieben ist. Jetzt lässt sich ein Timer erstellen, der denn Container in einem bestimmten Intervall neu startet.

sudo vim /etc/systemd/system/ddns.timer
[Unit]
Description=Dynamischer DNS Service

[Timer]
OnBootSec=5min
OnUnitInactiveSec=30min

[Install]
WantedBy=timers.target

OnUnitInacticeSec definiert einen Zeitgeber relativ zu dem Zeitpunkt, an dem die Einheit, die der Zeitgeber aktiviert, zuletzt deaktiviert wurde. Das heißt, der Container wird neu gestartet, wenn er in den letzten 30 Minuten nicht aktiv war.

sudo systemctl enable ddns.timer
sudo systemctl start ddns.timer
sudo systemctl status ddns.timer
sudo systemctl list-timers

Routing

Unsere Domain zweigt jetzt auf die öffentliche IP-Adresse unseres Heimnetzwerks. Die Anfrage wird aber noch nicht an den Proxy weitergegeben.

Freigabe HTTP und HTTPS

Deshalb richten wir eine Portweiterleitung ein. Im Router müssen Port 80 sowie 443 an den Server weitergeleitet werden.

Portfreigabe in der Fritz!Box

Damit landen alle http und https Anfragen auf Port 80/443 beim Server. Dort werden Sie dann vom Proxy weitergeleitet.

Lokales Routing

Wenn ich mich im Heimnetzwerk befinde, möchte ich, dass die Anfrage auf eine Subdomain zum einen nicht unterbunden wird. Außerdem hätte ich lieber den direkten Weg zum Server.

Der DNS-Rebind-Schutz lässt sich im Router unterbinden. DNS-Rebind-Schutz in der Fritz!Box

Danach erstellte ich noch in Adguard eine benutzerdefinierte Filterregel. Adguard Filterregel

Alternativ lässt sich das auch mit einer DNS-Umschreibung bewerkstelligen. Dieser Weg ist vorteilhafter, wenn man die Domain nicht anderweitig verwendet. DNS-Umschreibung

Die Regel sorgt dafür, dass alle Anfragen auf die Subdomains direkt an den Server weitergeleitet werden. Wenn wir Filterregeln nutzen, können wir einstellen, dass nicht nur proxy.domain.de, sondern auch adguard.domain.de auf die interne Domain des Servers weitergeleitet werden.

Das können wir entweder so eintragen:

192.168.178.100 adguard.domain.de proxy.domain.de

Oder folgendermaßen:

192.168.178.100 adguard.domain.de
192.168.178.100 proxy.domain.de

Proxy Host

Wir rufen wieder das Dashboard vom Nginx Proxy Manager auf. Zunächst wollen wir eine neue Filterregel, um nur lokalen Zugriff zu erlauben.

Neue Filterregel

Die einzige Einstellung, die geändert werden sollte, ist der Filter nach IP-Adressen. Wenn die IP-Adresse des Clients sich nicht im Heimnetz befindet, soll die Anfrage abgelehnt werden. Das Subnetz muss an das eigene Netzwerk angepasst werden.

IP-Adressen Filterregel

Anschließend benötigen wir einen neuen Proxy-Host.

Neuer Proxy-Host

Wir leiten die Anfragen an den Host “npm-app” weiter. Wenn der Host in der Docker-Compose Datei anders benannt wurde, muss der Eintrag angepasst werden. Unter Access List kann die gerade erstellte Filterregel ausgewählt werden. Nachdem der Proxy Host erstellt wurde, muss er nochmal bearbeitet werden. Diesmal im Bereich SSL. Hier kann ausgewählt werden, dass ein neues Zertifikat erstellt werden soll. Außerdem möchten wir SSL erzwingen.

SSL-Zertifikat erstellen

Wir testen, ob das funktioniert, indem im Browser die eben angegebene Domain aufgerufen wird. Danach testen wir das am besten auch noch auf einem Handy, welches nicht mit dem WLAN verbunden ist. Das erhoffte Verhalten ist nun, dass sich der Nginx Proxy Manager auf dem Gerät im Netzwerk öffnet. Zudem wird die Domain jetzt auf https:// geleitet und es erscheint ein Schloss-Symbol in der Adresszeile. Sobald wir es mit einem Gerät außerhalb des Netzwerkes testen, erscheint der Fehler 403.

Wir richten auch einen Proxy Host für Adguard nach dem gleichen Prinzip ein.

Nginx Proxy-Hosts

Sobald wir die Funktionalität testen, merken wir, dass es nicht funktioniert. Das liegt zum einen daran, dass Adguard intern auf Port 80 und nicht 85 hört (außer man hat es bei der Einrichtung geändert). Zum Anderen befindet sich Adguard auf einem isolierten Netzwerk. Wir müssen also im Proxy Manager den Port von 85 auf 80 umstellen und in der Docker-Compose.yml das Netzwerk anpassen. Dazu stoppen wir den Container und bearbeiten die Compose Datei.

sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml down
sudo vim /root/docker/containers/adguard/docker-compose.yml
version: "3"

networks:
    npm:
        name: npm

services:
    adguard:
        image: adguard/adguardhome:latest
        container_name: adguard
        restart: unless-stopped
        ports:
            - "53:53/tcp"
            - "53:53/udp"
        volumes:
            - /srv/adguard/data/work:/opt/adguardhome/work
            - /srv/adguard/data/conf:/opt/adguardhome/conf
        environment:
            TZ: Europe/Berlin
        networks:
            - npm

Die Veröffentlichung des öffentlichen Ports 85 zur Weiterleitung an den internen Port 80 ist nicht mehr notwendig. Adguard Home lässt sich dadurch nur noch über die eingerichtete Domain erreichen, nicht mehr direkt über die IP-Adresse inklusive Port.

sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml up -d

Jetzt sollte der Proxy Host funktionieren. Wir können ebenso den Nginx Proxy Manager selbst ändern.

sudo docker compose -f /root/docker/containers/nginxproxymanager/docker-compose.yml down
sudo vim /root/docker/containers/nginxproxymanager/docker-compose.yml
version: "3.7"

networks:
    npm:
        name: npm
    internal:
        external: false

secrets:
    npm-password:
        file: /root/docker/secrets/npm-password.txt
    npm-root-password:
        file: /root/docker/secrets/npm-root-password.txt

services:
    npm-app:
        image: "jc21/nginx-proxy-manager"
        container_name: npm-app
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        environment:
            DB_MYSQL_HOST: "npm-db"
            DB_MYSQL_PORT: 3306
            DB_MYSQL_USER: "npm"
            DB_MYSQL_NAME: "npm"
            DB_MYSQL_PASSWORD__FILE: /run/secrets/npm-password
            DISABLE_IPV6: "true"
        networks:
            - npm
            - internal
        volumes:
            - /srv/nginxproxymanager/data:/data
            - /srv/nginxproxymanager/certs:/etc/letsencrypt
        secrets:
            - npm-password

    npm-db:
        image: "jc21/mariadb-aria"
        container_name: npm-db
        restart: unless-stopped
        environment:
            MYSQL_ROOT_PASSWORD__FILE: /run/secrets/npm-root-password
            MYSQL_DATABASE: "npm"
            MYSQL_USER: "npm"
            MYSQL_PASSWORD__FILE: /run/secrets/npm-password
        networks:
            - internal
        volumes:
            - /srv/nginxproxymanager/db:/var/lib/mysql
        secrets:
            - npm-password
            - npm-root-password
sudo docker compose -f /root/docker/containers/nginxproxymanager/docker-compose.yml up -d

Jetzt sollte alles über die Domains erreichbar sein. Der Proxy Manager ist eingerichtet.

Firewall

Was jetzt eingerichtet werden kann, ist eine Firewall. Der einfachste Weg ist hier UFW.

sudo ufw limit ssh comment "SSH"
sudo ufw allow proto tcp from any to any port 80 comment "Nginx Proxy Manager"
sudo ufw allow proto tcp from any to any port 443 comment "Nginx Proxy Manager"
sudo ufw route allow proto tcp from any to any port 80 comment "Nginx Proxy Manager"
sudo ufw route allow proto tcp from any to any port 443 comment "Nginx Proxy Manager"
sudo ufw allow proto tcp from 192.168.123.0/24 to any port 53 comment "DNS TCP"
sudo ufw allow proto udp from 192.168.123.0/24 to any port 53 comment "DNS UDP"
sudo ufw route allow proto tcp from 192.168.123.0/24 to any port 53 comment "DNS TCP"
sudo ufw route allow proto udp from 192.168.123.0/24 to any port 53 comment "DNS UDP"
sudo ufw logging off
sudo ufw enable

Natürlich muss auch hier das Subnetz für den Port 53 angepasst werden.

Docker sind diese Regeln aber egal, sobald man einen Port darüber freigibt, wird die Firewall ausgehebelt. Deshalb gehe ich weiter nach der Anleitung aus diesem Repository vor.

sudo vim /etc/ufw/after.rules

Hier an das Ende der Datei einfügen:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP

COMMIT
# END UFW AND DOCKER
sudo systemctl restart ufw

Falls ein Fehler kommen sollte, kann man nachsehen, was passiert ist.

sudo systemctl status ufw

Sollte es einen Fehler mit der Zeile geben, in der *filter steht, muss das COMMIT am ursprünglichen Ende der Datei in #COMMIT geändert werden.

Die Firewall sollte nun funktionieren.

Docker Socket Proxy

Um die Sicherheit hoffentlich noch etwas zu erhöhen, erstelle ich einen Docker Socket Proxy. Mit diesem können sich dann die Apps, die Zugriff auf den Socket brauchen, verbinden, statt den richtigen zu nutzen. Wir benötigen ein neues Netzwerk für diesen Proxy.

sudo docker network create socket_proxy

Dann können wir die Docker Compose Datei erstellen.

sudo mkdir /root/docker/containers/socketproxy/
sudo chmod 700 /root/docker/containers/socketproxy/
sudo vim /root/docker/containers/socketproxy/docker-compose.yml
version: "3.7"

networks:
    socket_proxy:
        name: socket_proxy

services:
    socketproxy:
        image: "fluencelabs/docker-socket-proxy"
        container_name: socketproxy
        restart: unless-stopped
        networks:
            - socket_proxy
        ports:
            - "127.0.0.1:2375:2375"
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        privileged: true
        environment:
            - LOG_LEVEL=info
            # 0: Kein Zugriff auf die API.
            # 1: Zugriff auf die API.
            - EVENTS=1
            - PING=1
            - VERSION=1
            - AUTH=1
            - SECRETS=0
            - POST=1
            - BUILD=1
            - COMMIT=1
            - CONFIGS=1
            - CONTAINERS=1
            - DISTRIBUTION=1
            - EXEC=1
            - IMAGES=1
            - INFO=1
            - NETWORKS=1
            - NODES=0
            - PLUGINS=0
            - SERVICES=1
            - SESSION=0
            - SWARM=0
            - SYSTEM=0
            - TASKS=1
            - VOLUMES=1

Wichtig ist es den Port 2375 nicht freizugeben. Möchte man verschiedene andere Geräte im Netzwerk den Socket Proxy nutzen lassen (dann muss auch - 127.0.0.1:2375:2375 in- 2375:2375`geändert werden), sollte man darauf aufpassen, dass der Port wirklich nur von den Geräten erreicht werden kann, die man erwartet. Ggf. muss dann die Firewall nochmal restriktiver konfiguriert werden. Mir reicht es erstmal, dass zumindest ein paar Funktionen nicht erreicht werden können.

sudo docker compose -f /root/docker/containers/socketproxy/docker-compose.yml up -d

Wir passen auch die Firewall an.

sudo ufw allow proto tcp from 127.0.0.1 to any port 2375 comment "Docker Socket"
sudo ufw route allow proto tcp from 127.0.0.1 to any port 2375 comment "Docker Socket"
sudo ufw reload
sudo ufw enable

Flame

Als Nächstes wäre ein Dashboard nett, von dem aus die internen Dienste oder andere Lesezeichen erreicht werden können. Quasi eine eigene Startseite für das “Internet”. Statt Homer nutze ich dieses Mal Flame.

Ich möchte ein neues Secret für das Password. Das Passwort natürlich ändern, es sollte kein ' enthalten.

sudo bash -c 'echo irgendEinPasswort > /root/docker/secrets/flame-password.txt'
sudo mkdir -p /srv/flame/data
sudo chmod -R 750 /srv/flame
sudo mkdir /root/docker/containers/flame/
sudo chmod 700 /root/docker/containers/flame/
sudo vim /root/docker/containers/flame/docker-compose.yml

Bei dieser Docker Compose Datei können wir gleich den soeben erstellten Socket nutzen.

version: '3.6'

networks:
  npm:
    name: npm
  socket_proxy:
    name: socket_proxy

services:
  flame:
    image: pawelmalak/flame
    container_name: flame
    restart: unless-stopped
    networks:
      - npm
      - socket_proxy
    volumes:
      - /srv/flame/data:/app/data
    secrets:
      - password
    environment:
      - PASSWORD__FILE=/run/secrets/password
      - DOCKER_HOST=tcp://socketproxy:2375
    restart: unless-stopped

secrets:
  password:
    file: /root/docker/secrets/flame-password.txt
sudo docker compose -f /root/docker/containers/flame/docker-compose.yml up -d

Jetzt sollte ein Eintrag in Adguard angelegt werden. Der Proxy Host sollte auf Host: flame; Port: 5005 weiterleiten. Eventuell ist es nötig, das Netzwerk nochmal neu zu betreten, bevor die Änderungen wirksam werden.

In den den Einstellungen von Flame ist es nötig den Docker Socket für die Integration mit Docker zu ändern.

Docker Einstellungen in Flame

Jetzt können wir die Integration testen.

sudo vim /root/docker/containers/adguard/docker-compose.yml

Hier kommt nun der Label-Abschnitt hinzu.

version: "3"

services:
    adguard:
        image: adguard/adguardhome:latest
        container_name: adguard
        restart: unless-stopped
        ports:
            - "53:53/tcp"
            - "53:53/udp"
        volumes:
            - /srv/adguard/data/work:/opt/adguardhome/work
            - /srv/adguard/data/conf:/opt/adguardhome/conf
        environment:
            TZ: Europe/Berlin
        labels:
            - flame.type=app
            - flame.name=Adguard Home
            - flame.url=https://adguard.meinedomain.de
            - flame.icon=advertisements
sudo vim /root/docker/containers/nginxproxymanager/docker-compose.yml
version: "3.7"

networks:
    npm:
        name: npm
    internal:
        external: false

secrets:
    npm-password:
        file: /root/docker/secrets/npm-password.txt
    npm-root-password:
        file: /root/docker/secrets/npm-root-password.txt

services:
    npm-app:
        image: "jc21/nginx-proxy-manager"
        container_name: npm-app
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        environment:
            DB_MYSQL_HOST: "npm-db"
            DB_MYSQL_PORT: 3306
            DB_MYSQL_USER: "npm"
            DB_MYSQL_NAME: "npm"
            DB_MYSQL_PASSWORD__FILE: /run/secrets/npm-password
            DISABLE_IPV6: "true"
        networks:
            - npm
            - internal
        volumes:
            - /srv/nginxproxymanager/data:/data
            - /srv/nginxproxymanager/certs:/etc/letsencrypt
        secrets:
            - npm-password
        labels:
            - flame.type=app
            - flame.name=Nginx Proxy Manager
            - flame.url=https://proxy.meinedomain.de
            - flame.icon=arrow-decision-outline

    npm-db:
        image: "jc21/mariadb-aria"
        container_name: npm-db
        restart: unless-stopped
        environment:
            MYSQL_ROOT_PASSWORD__FILE: /run/secrets/npm-root-password
            MYSQL_DATABASE: "npm"
            MYSQL_USER: "npm"
            MYSQL_PASSWORD__FILE: /run/secrets/npm-password
        networks:
            - internal
        volumes:
            - /srv/nginxproxymanager/db:/var/lib/mysql
        secrets:
            - npm-password
            - npm-root-password
sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml down
sudo docker compose -f /root/docker/containers/adguard/docker-compose.yml up -d
sudo docker compose -f /root/docker/containers/nginxproxymanager/docker-compose.yml down
sudo docker compose -f /root/docker/containers/nginxproxymanager/docker-compose.yml up -d

Wenn Flame aktualisiert wurde, sollten Adguard sowie der Nginx Proxy Manager dort unter Applications auftauchen.

Flame Dashboard

Git Server

Als Nächstes benötige ich einen Git Server. In der Vergangenheit habe ich gerne Gitea dafür installiert und für einen Raspberry Pi würde ich das immer noch empfehlen. Diesmal möchte ich aber ein integriertes CI/CD testen. Deshalb installiere ich jetzt OneDev.

Der Prozess sollte inzwischen weitgehend bekannt sein. Ich möchte eine PostgreSQL Datenbank für Onedev verwenden. Dafür brauche ich ein Password. Leider ist dieses Mal das Problem, dass Docker Secrets nicht unterstützt werden.

sudo bash -c 'echo "DB_PWD=$(openssl rand -base64 32)" > /root/docker/containers/onedev/.env'
sudo mkdir /root/docker/containers/onedev
sudo chmod 700 /root/docker/containers/onedev
sudo mkdir -p /root/docker/containers/onedev/data
sudo mkdir /root/docker/containers/onedev/db
sudo chmod -R 750 /root/docker/containers/onedev
sudo vim /root/docker/containers/onedev/docker-compose.yml
version: "3.7"

networks:
    npm:
        name: npm
    socket_proxy:
        name: socket_proxy
    internal:
        external: false

services:
    onedev-app:
        container_name: onedev-app
        image: 1dev/server
        restart: unless-stopped
        networks:
            - npm
            - internal
            - socket_proxy
        volumes:
            - /srv/onedev/data:/opt/onedev
        environment:
            DOCKER_HOST: tcp://socket_proxy:2375
            hibernate_dialect: io.onedev.server.persistence.PostgreSQLDialect
            hibernate_connection_driver_class: org.postgresql.Driver
            hibernate_connection_url: jdbc:postgresql://onedev-db/onedev
            hibernate_connection_username: onedev
            hibernate_connection_password: $DB_PWD
        labels:
            - flame.type=app
            - flame.name=OneDev
            - flame.url=https://onedev.meinedomain.de
            - flame.icon=git
        depends_on:
            - onedev-db

    onedev-db:
        container_name: onedev-db
        image: postgres:14
        restart: unless-stopped
        networks:
            - internal
        volumes:
            - /srv/onedev/db:/var/lib/postgresql/data
        environment:
            POSTGRES_PASSWORD: $DB_PWD
            POSTGRES_USER: onedev
            POSTGRES_DB: onedev
sudo docker compose -f /root/docker/containers/onedev/docker-compose.yml up -d

Anschließend einen Eintrag in Adguard für OneDev hinzufügen. Und dann einen Proxy Host im Nginx Proxy Manager (mit Websocket Support) anlegen. Der Proxy Host hat in meinem Beispiel den Host “onedev-app” und die Portnummer 6610. Benötigt man noch SSH Zugriff, muss ein weiterer Proxy Host auf Portnummer 6611 geroutet werden.

Sobald die Onedev-Seite aufgerufen wird, muss ein Administratorkonto erstellt werden.

Möchte man den anonymen Zugriff auf das eigene OneDev verhindern, lässt sich das in den Sicherheitseinstellungen unterbinden.

OneDev Sicherheitseinstellungen

Möchte man anschließend, dass der Git Server auch von außen erreicht werden kann, muss man die Beschränkung auf das lokale Netzwerk aus dem Proxy Host im Nginx Proxy Manager herausnehmen.

Zeittracker

Ich hätte gerne meine eigene Zeiterfassung auf dem Server. Dafür sollte Kimai geeignet sein. Im Grunde geht aber hier um die Speicherung von Daten in Docker. Unsere bisherigen Docker-Compose Dateien haben Bind Mounts genutzt. Das “Problem” bei einem Bind-Mount ist aber, dass der Inhalt des Containers nicht automatisch auf den Rechner kopiert wird, im Gegensatz zu einem benannten Docker Volume.

Einige Images sind so konzipiert, dass sie mit einem Bind-Mount nicht funktionieren werden.

Zunächst brauchen wir aber Passwörter.

sudo mkdir /root/docker/containers/kimai
sudo chmod 700 /root/docker/containers/kimai
sudo bash -c 'echo "DB_PWD=$(openssl rand -base64 32)" > /root/docker/containers/kimai/.env'
sudo mkdir -p /srv/kimai/db
sudo chmod -R 750 /srv/kimai
sudo vim /root/docker/containers/kimai/docker-compose.yml
version: "3.7"

networks:
    npm:
        name: npm
    internal:
        external: false

services:
    kimai-app:
        container_name: kimai-app
        image: kimai/kimai2:apache
        restart: unless-stopped
        networks:
            - internal
            - npm
        volumes:
            - data:/opt/kimai/var
        environment:
            ADMINMAIL: mailadresse@domain.de
            ADMINPASS: dasPasswortZurAnmeldung
            DATABASE_URL: mysql://kimai:einSicheresPassword@kimai-db:3306/kimai
            TRUSTED_HOSTS: nginx,localhost,127.0.0.1,kimai.meinedomain.de
        depends_on:
            - kimai-db
        labels:
            - flame.type=app
            - flame.name=Kimai
            - flame.url=https://kimai.meinedomain.de
            - flame.icon=timer

    kimai-db:
        container_name: kimai-db
        image: mariadb:10.6
        restart: unless-stopped
        networks:
            - internal
        volumes:
            - /srv/kimai/db:/var/lib/mysql
        environment:
            MARIADB_ROOT_PASSWORD: $DB_PWD
            MARIADB_DATABASE: kimai
            MARIADB_USER: kimai
            MARIADB_PASSWORD: einSicheresPasswort

volumes:
    data:
sudo docker compose -f /root/docker/containers/kimai/docker-compose.yml up -d

Nachdem dafür auch ein Eintrag in AdGuard bzw. ein Proxy Host Hostname: kimai-app; Port: 8001 auch dafür angelegt wurde, kann man sich schon bei Kimai anmelden.

Container updaten

Das Update eines Containers ist mit drei Zeilen Code erledigt. Als Beispiel möchte ich meinen Nginx-Proxy-Manager updaten. Dafür muss ich nur das neuste Image pullen und den Container neu starten.

sudo docker compose -f /root/docker/containers/nginx/docker-compose.yml pull
sudo docker compose -f /root/docker/containers/nginx/docker-compose.yml down
sudo docker compose -f /root/docker/containers/nginx/docker-compose.yml up -d

Wenn ich auch noch das alte Image loswerden möchte, kann ich das mit prune tun.

sudo docker image prune

Mit sudo docker system df kann man sich anschauen, wie viel Speicherplatz Docker verbraucht.

Wenn Fragen offen geblieben sind oder bei sonstigen Anmerkungen / Verbesserungsvorschlägen, kann gerne der Kommentarbereich genutzt werden.


Diese Website verwendet Cookies. Diese sind notwendig, um die Funktionalität der Website zu gewährleisten. Weitere Informationen finden Sie in der Datenschutzerklärung