# Ubuntu 24.04 Server

Beschreibt, wie man bei Hetzner einen virtuellen Server mit Ubuntu 24.04 LTS anlegt, diesen grundlegend absichert und einen mit Crowdsec abgesicherten Docker Traefik Stack einrichtet.

# Bestellung

Über die [Hetzner Cloud Console](https://console.hetzner.cloud/projects) kann ein neuer Server geordert oder aus einem Snapshot erstellt werden. In der Regel werden aus Kostengründen die neuen ARM-Server bevorzugt, aber noch ist die Softwarewelt nicht so weit, ARM CPUs in allen Bereichen zu unterstützen. Für den Fall der Fälle kann auf AMD- oder Intel-Hardware ausgewichen werden.

Ein üblicher Standard-Server sieht etwa wie folgt aus:

<table id="bkmrk-standort-falkenstein"><colgroup><col></col><col></col></colgroup><tbody><tr><td>Standort

</td><td>Nürnberg, Falkenstein oder Helsinki  
</td></tr><tr><td>Image</td><td>Ubuntu 22.04</td></tr><tr><td>Typ</td><td>Shared CPU Arm64 (Ampere) CAX11 (2 Cores, 4 GB RAM, 40 GB SSD, 20 TB Traffic)  
</td></tr><tr><td>Networking</td><td>IP4 optional / IP6 optional / Private Network optional (je nach Bedarf)</td></tr><tr><td>Platzierungsgruppen</td><td>Empfehlenswert, wenn mehrere Server im Projekt laufen (z. B. Kubernetes)</td></tr><tr><td>SSH-Keys</td><td>Konfigurierte Public Keys von allen administrierenden Systemen (empfohlen)</td></tr><tr><td>Firewalls</td><td>Je nach Einsatz - für öffentliche Server ein Muss! </td></tr><tr><td>Backups  
</td><td>täglich, 7 Tage Zyklus. Empfehlenswert - Kosten: 20% vom Serverpreis  
</td></tr><tr><td>Name</td><td>nach eigenem Ermessen</td></tr><tr><td>Kosten  
</td><td>5,29€ / Monat mit IP4-Adresse und Backups aktiviert (Stand April 2024)  
</td></tr></tbody></table>

Nach Abschluss einer Bestellung wird der Server innerhalb weniger Sekunden erstellt und seine Basisdaten sowie seine IP-Adresse(n) angezeigt. Mit der IP-Adresse kann sich mittels ssh auf ihm angemeldet werden:

```bash
ssh root@<IP-Adresse>
```

Eine IPv6 Adresse wird nicht ganz korrekt angezeigt. Am Ende erscheint ein ::/64, was bedeuten würde das das letzte Paket 0000 lautet. Dem ist nicht so, denn das letzte Paket des Adressblocks lautet korrekt 0001.  
Um sich die sperrige IP-Adresse nicht merken zu müssen, empfiehlt es sich, einen passenden Namen mit der IP-Adresse in Verbindung zu bringen und in der Shell zu exportieren. Für einen dauerhaften Export benötigt es nur einen Eintrag in der Datei .zshrc (oder .bashrc):

```bash
export myserver=<IP-Adresse>
```

Damit ist dann der ssh Verbindungsaufbau mit dem Befehl ssh root@$myserver möglich.

Der Login erfordert dank des übergebenen öffentlichen SSH-Key kein Passwort, es sein denn, der ssh-Key selbst ist geschützt. Von Hetzner wird bei Angabe eines ssh-Keys kein Password generiert und per E-Mail verschickt.

Im nächsten Schritt wird der Server grundlegend abgesichert[.](https://books.rastef.de/books/serverexperimente-ii/page/grundlegende-absicherung "Grundlegende Absicherung")

# Grundlegende Absicherung

Externe Server bzw. generell alle Server, die am Internet hängen, sollten Anmeldungen auf der Konsole ausschließlich über einen SSH-Schlüssel zulassen. Zudem sollte sich der User root gar nicht von außen anmelden können. Außerdem sollte eine Firewall Zugriffe nur über explizit erlaubte Ports erlauben und eine Software installiert sein, die Angriffsversuche erkennt und blockiert. Am Beispiel eines Hetzner Cloud vServers unter Ubuntu 22.04 LTS wird hier beschrieben, wie ein solcher Server abgesichert werden kann.

#### Anlegen eines neuen Users mit root Rechten

Erfreulicherweise können bei Hetzner, wie auf der Seite [Hetzner vServer bestellen &amp; absichern](https://buch.rastef.eu/books/serverexperimente/chapter/hetzner-vserver-bestellen-absichern "Hetzner vServer bestellen & absichern") beschrieben, in der Bestellung eines Servers bereits alle SSH-Schlüssel, die auf dem Server erlaubt sein sollen, mit angegeben werden. Voraussetzung dafür ist, dass die öffentlichen Schlüssel im Hetzner-Account abgelegt wurden. Diese Option sollte unbedingt genutzt werden, denn so ist der Server gleich von Beginn an mit einem Grundschutz gegen Attacken von außen versehen, denn eine Anmeldung als root ohne einen passenden privaten Schlüssel ist nicht mehr möglich.

Nachdem der Server das erstmals gestartet wurde, erfolgt die Anmeldung mit `ssh root@<ip_adresse>`. Dazu ist es unbedingt erforderlich, dass ein gültiger öffentlicher Schlüssel im .ssh Verzeichnis des aufrufenden Users liegt. Dafür hat Hetzner bei der Anlage des Servers gesorgt. Alle in der Bestellung angegebenen öffentlichen Schlüssel sind in der Datei ~/.ssh/authorized\_keys eingetragen.

Nach einer erfolgreichen Anmeldung wird als User root auf dem Server gearbeitet. Aus diesem Grund enthalten die nachfolgend aufgeführten Kommandos auch kein vorangestelltes sudo.

Als erste Aktion nach einer Neuinstallation und der Erstanmeldung als root ist unbedingt das Betriebssystem zu aktualisieren. Dazu ist der Befehl `apt update && apt upgrade -y` auszuführen. Unter Umständen weist das Upgrade am Ende darauf hin, dass ein Reboot des Systems erforderlich ist. Das kann schnell mit dem Befehl `reboot` herbeigeführt werden.

Im nächsten Schritt ist die Firewall zu konfigurieren und aktivieren. Die Firewall ufw ist bereits installiert, aber noch nicht aktiviert. Bevor sie aktiviert werden kann, sind allerdings noch ein paar Konfigurationen erforderlich. Eingehender Verkehr ist nur auf den explizit zugelassenen Ports zulässig, ausgehender Verkehr wird generell erlaubt.

```bash
# Eingehenden Verkehr generell verbieten
sudo ufw default deny incoming

# Ausgehenden Verkehr generell eerlauben
sudo ufw default allow outgoing

# SSH Verbindungen zulassen
sudo ufw allow ssh

# Wenn auch HTTP/HTTPS erlaubt sein soll
sudo ufw allow http
sudo ufw allow https

# Firewall aktivieren
sudo ufw enable
```

Im nächsten Schritt ist ein User mit sudo Rechten einzurichten:

```bash
#
# Benutzer 'sysadmin' einrichten und zum sudoer machen.
# Dabei wird vorausgesetzt, dass es sich beim eingelogten
# user root und dem neuen user sysadmin um ein und dieselbe
# Person handelt.
#
adduser sysadmin
usermod -a -G sudo sysadmin
mkdir /home/sysadmin/.ssh
cp ~/.ssh/authorized_keys /home/sysadmin/.ssh/
chmod 700 /home/sysadmin/.ssh
chmod 400 /home/sysadmin/.ssh/authorized_keys
chown sysadmin:sysadmin /home/sysadmin/.ssh
chown sysadmin:sysadmin /home/sysadmin/.ssh/authorized_keys

```

In einem neuen Terminal sollte nun ein ssh Anmeldeversuch mit dem neuen User erfolgen. Die Anmeldung muss ohne Eingabe eines Passworts gelingen, es sei denn, das System fragt nach dem Passwort für den ssh Key, wenn dieser mit einem Passwort abgesichert ist.

Nach erfolgreicher Anmeldung muss abschließend geprüft werden, ob der neue User sysadmin mittels sudo mit root-Rechten arbeiten kann. Um dem User zukünftig die Passworteingabe nach einem sudo Befehl zu ersparen, führen folgende Schritte zum Ziel:

```bash
#
# Eine Datei im Verzeichnis /etc/sudoers.d anlegen
#
sudo vi /etc/sudoers.d/sysadmin-user

#
# In dieser Datei folgende Zeile einfügen:
#
sysadmin ALL=(ALL) NOPASSWD:ALL

#
# In einem neuen Terminalfenster testen:
#
sudo -i
```

<p class="callout warning">Der Verzicht auf das sudo Password ist eine Schwächung der Systemsicherheit!</p>

<p class="callout danger">Die Nachfolgenden Schritte dürfen nur durchgeführt werden, wenn sich der neue User per ssh ohne Password mit seinem ssh Key anmelden und root Rechte einnehmen kann. Ansonsten droht der Ausschluss aus dem Server!.</p>

#### Anmeldung des Users root verbieten

Ein nächster wichtiger Schritt hin zu einem sicheren System ist das Unterbinden des Login als User root und des Login mit Passwort. Ein Login soll nur noch mittels ssh Schlüssel möglich sein. Dazu sind in der Datei */etc/ssh/sshd\_config*folgende Einträge wie dargestellt zu ändern:

```shell
#
# Anmelden mit root verbieten und nur publickey erlauben
# 
PermitRootLogin no
AuthenticationMethods publickey
PasswordAuthentication no
```

Nach der Änderung muss der ssh Service mit dem Befehl `sudo systemctl restart sshd` neu gestartet werden.

# Docker installieren

Die Services, die von Servern bereitgestellt werden, sind meist in Form von Docker Images implementiert. Hierzu gehören z. B. Blogs, Wissensdatenbanken, Git-Repositories und viele mögliche mehr. Voraussetzung ist eine aktuelle Docker Engine, die idealerweise von Docker selbst bezogen wird, denn Erfahrungsgemäß sind die in den Ubuntu Repositorien vorhandenen Paket nicht aktuell genug. Dazu werden die Docker Repositorien in die Ubuntu-Repositorien eingebunden, so dass mit jedem apt update/upgrade die aktuellste Version installiert wird.

Das nachfolgende Skript (*install\_docker.sh*) erledigt die Aufnahme des Docker Repositories sowie die Installation der Docker Engine automatisch. Allerdings wird erwartet, das die Installation noch einmal bestätigt wird.

```bash
#/bin/bash!
#------------------------------------------------
# install_docker.sh
#
# Install docker using the apt repository
# Brings all commands of the docker side together
# in one script.
#------------------------------------------------

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

# Install the latest version
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

```

Es bietet sich an, das Skript auf der [Docker-Seite](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository "Install using the apt repository") ab und an auf Aktualität zu überprüfen.

Zur Ausführung des **docker** Kommandos werden root Rechte benötigt. Soll das Kommando ohne vorangestelltes **sudo** direkt aufgerufen werde können, ist die Gruppe Docker einzurichten und den ausgewählten Benutzern zuzuweisen.

```bash
# Create the docker group
sudo groupadd docker

# Add user to the docker group
sudo usermod -aG docker $USER

```

<p class="callout warning">Bitte beachten, dass Benutzende mit der Zuweisung der docker Gruppe root Rechte erlangen!</p>

# Traefik Crowdsec Stack einrichten

Der traefik-crowdsec-stack soll einen sicheren Betrieb von Docker Services ermöglichen. Dabei wird als Reverse-Proxy und Load-Balancer die derzeit aktuellste Version 3.x.x von Traefik eingesetzt. Die Kommunikation der von Traefik gerouteten Services wird mit Crowdsec in eigenen Netzwerken abgesichert. Schließlich wird der Stack um die von Außen erreichbaren Dienste "Ralfs Blogs", "Bines Blog", "Bücherregal" und "Gitea-Server" erweitert. Alle hierzu benötigten Docker Services können bequem über eine einzelne Docker Compose Datei gesteuert werden.

Die Basis für die hier verwendeten Texte und den Code bildet der Artikel [Traefik V3 Installation, Konfiguration und CrowdSec-Security](https://goneuland.de/traefik-v3-installation-konfiguration-und-crowdsec-security) von <span class="author-post byline"><span class="author vcard">[psycho0verload](https://goneuland.de/author/psycho0verload/ "psycho0verload"). Es sei nicht verschwiegen das einige Elemente des Artikels kostenpflichtig sind. Hierzu gehört insbesondere der Netzwerk-Abschnitt.  
</span></span>

#### Einrichten der grundlegenden Stack-Struktur

Bevor es richtig losgeht noch ein wichtiger Hinweis: Alle Shell-Befehle sind mit root Privilegien auszuführen. In den Skripten wird kein vorangestelltes sudo verwendet. Das Skript selbst ist mit sudo aufzurufen.

Um den Stack anzulegen, wird das Hauptverzeichnis traefik-crowdsec-stack unter /opt/containers/ erstellt und in dieses Verzeichnis gewechselt. Danach werden die erforderlichen Unterverzeichnisse und Dateien erzeugt, um den Stack mit crowdsec, socket-proxy und traefik zu konfigurieren. Schließlich werden noch die korrekten Berechtigungen für sensible Zertifikatsdateien gesetzt.

Anschließend werden die in der Struktur angelegten Konfigurations- und Environmentdateien mit Inhalten gefüllt. Dabei werden Parameter wie Passwörter oder API-Schlüssel mit einem in doppelt geschweiften Klammern versehenem Kommentar versehen. Diese Parameter sind im Nachgang von Hand anzupassen.

Um den Stack vorzubereiten, kann das nachfolgende Skript ausgeführt werden (zum Aufklappen auf den Namen klicken):

<details id="bkmrk-create_stack_structu"><summary>create\_stack.sh</summary>

```bash
#!/bin/bash
#
# ensure that user has root privileges
#
if [ $(id -u) -ne 0 ]
  then echo Please run this script as root or using sudo!
  exit
fi

#
# create traefic-crowdsec-stack directory structure
#
mkdir -p /opt/containers/traefik-crowdsec-stack && cd /opt/containers/traefik-crowdsec-stack
mkdir -p compose data/{crowdsec/{config,data},socket-proxy,traefik/{certs,dynamic_conf},traefik-crowdsec-bouncer,service-blog/{bine,ralf},service-bookstack/{app,db},service-gitea}

#
# Create some files and change permissions of cert related files
#
touch .env docker-compose.yml \
compose/{crowdsec.yml,networks.yml,socket-proxy.yml,traefik-crowdsec-bouncer.yml,traefik.yml} \
data/{crowdsec/.env,socket-proxy/.env,traefik/{.env,.htpasswd,traefik.yml,certs/{acme_letsencrypt.json,tls_letsencrypt.json},dynamic_conf/{http.middlewares.default.yml,http.middlewares.default-security-headers.yml,http.middlewares.gzip.yml,http.middlewares.traefik-bouncer.yml,http.middlewares.traefik-dashboard-auth.yml,tls.yml}},traefik-crowdsec-bouncer/.env}

chmod 600 data/traefik/certs/{acme_letsencrypt.json,tls_letsencrypt.json}

#
# Fill master dotenv file
#
cat << 'EOF' > /opt/containers/traefik-crowdsec-stack/.env
ABSOLUTE_PATH=${PWD}
TZ=Europe/Berlin
SERVICES_TRAEFIK_LABELS_TRAEFIK_HOST=HOST(`traefik.rastef.de`)
EOF

#
# Fill master compose file
#
echo "include:
  - compose/service-blog-bine.yml
  - compose/service-blog-ralf.yml
  - compose/service-bookstack.yml
  - compose/service-gitea.yml
  - compose/crowdsec.yml
  - compose/socket-proxy.yml
  - compose/traefik-crowdsec-bouncer.yml
  - compose/traefik.yml
  - compose/networks.yml" >> /opt/containers/traefik-crowdsec-stack/docker-compose.yml


#
# Fill Crowdsec compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/crowdsec.yml
services:
  crowdsec:
    container_name: ${SERVICES_CROWDSEC_CONTAINER_NAME:-crowdsec}
    env_file: ${ABSOLUTE_PATH}/data/crowdsec/.env
    hostname: ${SERVICES_CROWDSEC_HOSTNAME:-crowdsec}
    healthcheck:
      test: ["CMD", "cscli", "version"]
      timeout: 2s
      interval: 20s
      retries: 5
      start_period: 10s
    image: ${SERVICES_CROWDSEC_IMAGE:-crowdsecurity/crowdsec}:${SERVICES_CROWDSEC_IMAGE_VERSION:-latest}
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_CROWDSEC_NETWORKS_CROWDSEC_IPV4:-172.31.127.254}
        ipv6_address: ${SERVICES_CROWDSEC_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6fff}
      socket_proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.252}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8ffd}
    restart: unless-stopped
    security_opt:
      - no-new-privileges=true
    volumes:
      - ${ABSOLUTE_PATH}/data/crowdsec/config:/etc/crowdsec
      - ${ABSOLUTE_PATH}/data/crowdsec/data:/var/lib/crowdsec/data
      - /var/log/auth.log:/var/log/auth.log:ro
      - /var/log/traefik:/var/log/traefik:ro
EOF

#
# Fill Socket-Proxy compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/socket-proxy.yml
services:
  socket-proxy:
    container_name: ${SERVICES_SOCKET_PROXY_CONTAINER_NAME:-socket-proxy}
    env_file: ${ABSOLUTE_PATH}/data/socket-proxy/.env
    hostname: ${SERVICES_SOCKET_PROXY_HOSTNAME:-socket-proxy}
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:2375/_ping || exit 1"]
      timeout: 3s
      interval: 20s
      retries: 3
      start_period: 10s
    image: ${SERVICES_SOCKET_PROXY_IMAGE:-lscr.io/linuxserver/socket-proxy}:${SERVICES_SOCKET_PROXY_IMAGE_VERSION:-latest}
    networks:
      socket_proxy:
        ipv4_address: ${SERVICES_SOCKET_PROXY_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.254}
        ipv6_address: ${SERVICES_SOCKET_PROXY_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8fff}
    read_only: true
    restart: unless-stopped
    tmpfs:
      - /run
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
EOF

#
# Fill Traefik Crowdsec Bouncer compose file
#
cat << 'EOF' > /opt/containers/traefik-crowdsec-stack/compose/traefik-crowdsec-bouncer.yml
services:
  traefik_crowdsec_bouncer:
    container_name: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_CONTAINER_NAME:-traefik_crowdsec_bouncer}
    depends_on:
      crowdsec:
        condition: service_healthy
    env_file: ${ABSOLUTE_PATH}/data/traefik-crowdsec-bouncer/.env
    hostname: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_HOSTNAME:-traefik-crowdsec-bouncer}
    image: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE:-ghcr.io/freifunkmuc/traefik-crowdsec-bouncer}:${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE_VERSION:-latest}
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_NETWORKS_CROWDSEC_IPV4:-172.31.127.252}
        ipv6_address: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6ffd}
    restart: unless-stopped
EOF

#
# Fill Traefik compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/traefik.yml
services:
  traefik:
    container_name: ${SERVICES_TRAEFIK_CONTAINER_NAME:-traefik}
    depends_on:
      crowdsec:
        condition: service_healthy
      socket-proxy:
        condition: service_healthy
    env_file: ${ABSOLUTE_PATH}/data/traefik/.env
    hostname: ${SERVICES_TRAEFIK_HOSTNAME:-traefik}
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      timeout: 1s
      interval: 10s
      retries: 3
      start_period: 10s
    image: ${SERVICES_TRAEFIK_IMAGE:-traefik}:${SERVICES_TRAEFIK_IMAGE_VERSION:-latest}
    labels:
      traefik.enable: "true"
      traefik.http.routers.traefik-dashboad.entrypoints: websecure
      traefik.http.routers.traefik-dashboad.middlewares: traefik-dashboard-auth@file
      traefik.http.routers.traefik-dashboad.rule: ${SERVICES_TRAEFIK_LABELS_TRAEFIK_HOST:-HOST(`traefik.yourdomain.com`)}
      traefik.http.routers.traefik-dashboad.service: api@internal
      traefik.http.routers.traefik-dashboad.tls: "true"
      traefik.http.routers.traefik-dashboad.tls.certresolver: tls_resolver
      traefik.http.services.traefik-dashboad.loadbalancer.sticky.cookie.httpOnly: "true"
      traefik.http.services.traefik-dashboad.loadbalancer.sticky.cookie.secure: "true"
      traefik.http.routers.pingweb.rule: PathPrefix(`/ping`)
      traefik.http.routers.pingweb.service: ping@internal
      traefik.http.routers.pingweb.entrypoints: websecure
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_CROWDSEC_IPV4:-172.31.127.253}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6ffe}
      proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_PROXY_IPV4:-172.31.191.254}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:7fff}
      socket_proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.253}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8ffe}
    ports:
      - mode: host
        target: 80
        published: "80"
        protocol: tcp
      - mode: host
        target: 443
        published: "443"
        protocol: tcp
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ${ABSOLUTE_PATH}/data/traefik/traefik.yml:/etc/traefik/traefik.yml
      - ${ABSOLUTE_PATH}/data/traefik/.htpasswd:/etc/traefik/.htpasswd
      - ${ABSOLUTE_PATH}/data/traefik/certs/acme_letsencrypt.json:/etc/traefik//acme_letsencrypt.json
      - ${ABSOLUTE_PATH}/data/traefik/certs/tls_letsencrypt.json:/etc/traefik/tls_letsencrypt.json
      - ${ABSOLUTE_PATH}/data/traefik/dynamic_conf:/etc/traefik/dynamic_conf:ro
      - /var/log/traefik/:/var/log/traefik/
EOF

#
# Fill Crowdsec compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/networks.yml
networks:
  crowdsec:
    name: ${NETWORKS_CROWDSEC_NAME:-crowdsec}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_CROWDSEC_SUBNET_IPV4:-172.31.64.0/18}
      - subnet: ${NETWORKS_CROWDSEC_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:6000/116}
    attachable: true
  proxy:
    name: ${NETWORKS_PROXY_NAME:-proxy}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_PROXY_SUBNET_IPV4:-172.31.128.0/18}
      - subnet: ${NETWORKS_PROXY_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:7000/116}
    attachable: true
  socket_proxy:
    name: ${NETWORKS_SOCKET_PROXY_NAME:-socket_proxy}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_SOCKET_PROXY_SUBNET_IPV4:-172.31.192.0/18}
      - subnet: ${NETWORKS_SOCKET_PROXY_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:8000/116}
    attachable: true
    internal: true
EOF

#
# Fill Service Blog Bine compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-blog-bine.yml
services:
  bine:
    container_name: ${SERVICES_BINE_CONTAINER_NAME:-service-blog-bine}
    image: nginx:latest
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.bine-secure.entrypoints: websecure
      traefik.http.routers.bine-secure.rule: Host(`bine.rastef.de`)
      traefik.http.routers.bine-secure.service: service-blog-bine
      traefik.http.routers.bine-secure.tls: true
      traefik.http.routers.bine-secure.tls.certresolver: tls_resolver
      traefik.http.routers.bine.entrypoints: web
      traefik.http.routers.bine.rule: Host(`bine.rastef.de`)
      traefik.http.services.bine.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/service-blog/bine:/usr/share/nginx/html:ro
EOF

#
# Fill Service Blog Ralf compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-blog-ralf.yml
services:
  blog:
    container_name: ${SERVICES_BLOG_CONTAINER_NAME:-service-blog-ralf}
    image: nginx:latest
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.blog-secure.entrypoints: websecure
      traefik.http.routers.blog-secure.rule: Host(`rastef.de`) || Host(`www.rastef.de`)
      traefik.http.routers.blog-secure.service: service-blog-ralf
      traefik.http.routers.blog-secure.tls: true
      traefik.http.routers.blog-secure.tls.certresolver: tls_resolver
      traefik.http.routers.blog.entrypoints: web
      traefik.http.routers.blog.rule: Host(`rastef.de`) || Host(`www.rastef.de`)
      traefik.http.services.blog.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/service-blog/ralf:/usr/share/nginx/html:ro
EOF

#
# Fill Service Bookstack compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-bookstack.yml
services:
  books:
    container_name: bookstack
    image: linuxserver/bookstack:latest
    environment:
      - TZ=Europe/Berlin
      - APP_URL=https://buecher.rastef.de
      - APP_KEY={{get it from Bookstack with: docker run -it --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey}}
      - DB_HOST=bookstackdb
      - DB_DATABASE=bookstack
      - DB_USERNAME=bookstack
      - DB_PASSWORD={{--Das Bookstack DB Password--}}
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.books-secure.entrypoints: websecure
      traefik.http.routers.books-secure.rule: Host(`buecher.rastef.de`)
      traefik.http.routers.books-secure.service: bookstack
      traefik.http.routers.books-secure.tls: true
      traefik.http.routers.books-secure.tls.certresolver: tls_resolver
      traefik.http.routers.books.entrypoints: web
      traefik.http.routers.books.rule: Host(`buecher.rastef.de`)
      traefik.http.services.books.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/bookstack/app:/config
  bookstackdb:
    container_name: booksdb
    image: linuxserver/mariadb:latest
    environment:
      - TZ=Europe/Berlin
      - MYSQL_ROOT_PASSWORD={{--Das MySQL Root Password--}}
      - MYSQL_DATABASE=bookstack
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD={{--Das Bookstack DB Password--}}
    networks:
      default: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/bookstack/db:/config
EOF

#
# Fill Service Gitea compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-gitea.yml
services:
  gitea:
    container_name: ${SERVICES_GITEA_CONTAINER_NAME:-service-gitea}
    image: docker.gitea.com/gitea:latest-rootless
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.gitea-secure.entrypoints: websecure
      traefik.http.routers.gitea-secure.rule: Host(`gitea.rastef.de`)
      traefik.http.routers.gitea-secure.service: service-gitea
      traefik.http.routers.gitea-secure.tls: true
      traefik.http.routers.gitea-secure.tls.certresolver: tls_resolver
      traefik.http.routers.gitea.entrypoints: web
      traefik.http.routers.gitea.rule: Host(`gitea.rastef.de`)
      traefik.http.services.gitea.loadbalancer.server.port: "3000"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/gitea/repos:/var/lib/gitea
      - ${ABSOLUTE_PATH}/data/gitea/config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
EOF

```

</details>Mit dem ***tree*** Kommando lässt sich die Verzeichnisstruktur verifizieren. Wenn das ***tree*** Kommando fehlt, kann es schnell mit `sudo apt install tree` installiert werden.

```bash
 $ tree -L 4 -a /opt/containers/traefik-crowdsec-stack/
```

<span style="font-family:monospace;"><span style="font-weight:bold;color:#5454ff;background-color:#ffffff;"><span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">/opt/containers/traefik-crowdsec-stack/</span><span style="color:#000000;background-color:#ffffff;"> </span>  
├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">compose</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ ├── crowdsec.yml   
│ ├── networks.yml   
│ ├── service-blog-bine.yml   
│ ├── service-blog-ralf.yml   
│ ├── service-bookstack.yml   
│ ├── service-gitea.yml   
│ ├── socket-proxy.yml   
│ ├── traefik-crowdsec-bouncer.yml   
│ └── traefik.yml   
├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">data</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">crowdsec</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">config</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">data</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ └── .env   
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">service-blog</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">bine</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ └── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">ralf</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">service-bookstack</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">app</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ └── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">db</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">service-gitea</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">socket-proxy</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ └── .env   
│ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">traefik</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">certs</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ │ ├── acme\_letsencrypt.json   
│ │ │ └── tls\_letsencrypt.json   
│ │ ├── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">dynamic\_conf</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ │ │ ├── http.middlewares.default-security-headers.yml   
│ │ │ ├── http.middlewares.default.yml   
│ │ │ ├── http.middlewares.gzip.yml   
│ │ │ ├── http.middlewares.traefik-bouncer.yml   
│ │ │ ├── http.middlewares.traefik-dashboard-auth.yml   
│ │ │ └── tls.yml   
│ │ ├── .env   
│ │ ├── .htpasswd   
│ │ └── traefik.yml   
│ └── <span style="font-weight:bold;color:#5454ff;background-color:#ffffff;">traefik-crowdsec-bouncer</span><span style="color:#000000;background-color:#ffffff;"> </span>  
│ └── .env   
├── docker-compose.yml   
└── .env   
  
18 directories, 25 files</span></span>

<p class="callout info"><span style="font-family:monospace;">Bitte beachten, dass in diversen Dateien noch manuelle Korrekturen vorgenommen werden müssen. Daher bitte noch kein docker compose ausführen. Lesen sie dazu auf der nächsten Seite weiter.</span><span style="font-family:monospace;">  
</span></p>

####