Einführung
Diese Webseite zops.top wird momentan über einen kleinen Provider names wint.global
in die schöne, weite Welt übertragen. Dazwischen sitzt momentan Cloudflare
als nameserver
& damit kann ich direkt über dash.cloudflare.com auch auf meine DNS-Einträge zugreifen. So wird der gesamte Traffic durch Cloudflare geleitet (Proxy) und die “echte” IP-Adresse des Servers wird nicht bekannt gegeben.
Macht man ein traceroute
oder tracert
(Windows) auf zops.top
erhält man die IP-Adresse eines Cloudflare
-Servers.
▶ traceroute zops.top
traceroute: Warning: zops.top has multiple addresses; using 104.31.93.242
traceroute to zops.top (104.31.93.242), 64 hops max, 52 byte packets
1 fritz.box (192.168.178.1) 3.652 ms 2.836 ms 3.249 ms
2 lo1.pop107-asr.ipv4.wtnet.de (_____________) 4.197 ms 4.061 ms 4.007 ms
3 b107.graf-zahl.ipv4.wtnet.de (_____________) 3.150 ms 4.047 ms 4.249 ms
4 cloudflare.ham.ecix.net (193.42.155.58) 4.165 ms
ipv4.de-cix.ham.de.as13335.cloudflare.com (80.81.203.10) 4.115 ms 3.012 ms
5 104.31.93.242 (104.31.93.242) 4.388 ms 4.765 ms 3.415 ms
Nun habe ich einen zweiten Server gemietet, möchte jedoch meine Domain weiterhin bei wint.global
behalten.
Projektidee und -ziel
Mein Ziel ist es, verschiedene subdomains
unter meiner Domain zops.top
zu haben, die nicht auf den Server von wint.global
verweisen, sondern auf den neuen Server bei einem anderen Anbieter.
Bei dem anderen Anbieter habe ich eine simplen VPS (Virtual Private Server). Wenn ich darauf z. B. eine Webseite hosten möchte, muss ich die nötigen Pakete installieren & die gesamte Konfiguration selbst machen.
Die simpelste Lösung verschiede Services unter einer Domain anzubieten, wäre einfach alles auf verschiedenen Ports laufen zu lassen - das ist aber meiner Meinung nach ziemlich unangenehm. “Versteckte” Services kann man ja so laufen lassen, aber sobald man z. B. Nextcloud
nutzen möchte, macht es schon Sinn, dass die Domain dann auch cloud.example.com
o. ä. heißt, anstatt example.com:6464
.
Die Frage ist nun, wie kann ich am besten mehrere Subdomains mit der selben IP beliefern ohne kostenpflichtige Tools wie Plex
zu nutzen?
DNS (Cloudflare)
Ganz grob gesehen, sieht jede Anfrage eines Nutzers in etwa so aus:
Der Nameserver, den ich momentan nutze, ist Cloudflare. Meine Domain
ist bei wint.global
registriert, jedoch verwalte ich alle DNS-Einträge über Cloudflare.
Daher würde ein Browser den Weg gehen, den wir oben via traceroute
gegangen sind; von eurem Endgerät -> Router -> Cloudflare -> Cloudflare Proxy
.
Dass die Proxy
dann meinen Server kontaktiert, Caching
und DDOS-Sicherung
betreibt, bemerkt ein Endnutzer gar nicht. Ist ja auch eigentlich gar nicht wichtig.
Die Domain zops.top
wird über die Cloudflare Proxy in die große weite Welt geschickt,
da die Einstellung in meinem Account so vorgenommen wurde.
Einige Subdomains jedoch werden auf einen anderen Server weitergeleitet - mein neuer VPS.
Dieser benutzt Cloudflare nicht als Proxy - daher ist die IP auch bei einem traceroute
direkt bekannt.
▶ traceroute server.zops.top
traceroute to server.zops.top (161.97.81.52), 64 hops max, 52 byte packets
1 fritz.box (192.168.178.1) 3.114 ms 2.880 ms 2.165 ms
2 lo1.pop107-asr.ipv4.wtnet.de (_________) 6.768 ms 4.007 ms 3.719 ms
3 b107.graf-zahl.ipv4.wtnet.de (_________) 4.883 ms 6.003 ms 3.956 ms
4 b379.bert.ipv4.wtnet.de (_________) 4.780 ms 9.000 ms 5.282 ms
5 ae60.edge1.hamburg1.level3.net (62.67.25.117) 5.600 ms 6.018 ms 4.385 ms
6 * * *
7 * * *
8 vmd57207.contaboserver.net (161.97.81.52) 17.931 ms !Z 17.919 ms !Z 17.092 ms !Z
Wie man sieht, gehört der Server zu contabo
.
Ändern der DNS-Einträge
Im Normalfall haben wir die Möglichkeit unsere DNS-Einträge anzupassen. Da Cloudflare diese Aufgabe für mich übernimmt, tue ich das auch bei Cloudflare.
Dafür müssen wir mindestens einen A
-Eintrag hinzufügen - dieser verweist eine domain
auf eine IPv4
-Adresse. Für IPv6
-Weiterleitung benötigen wir noch einen AAAA
-Eintrag.
Danach können wir CNAME
-Einträge definieren, um z. B. www.example.com
auf example.com
weiterzuleiten.
So siehen z. B. die Einträge für server.zops.top
aus.
Type | Name | Inhalt | TTL | Beschreibung |
---|---|---|---|---|
A | cloud | 161.97.81.52 | auto | cloud.zops.top -> 161.97.81.52 |
AAAA | cloud | IPv6-Adresse.. | auto | cloud.zops.top -> IPv6-Adresse |
CNAME | www.cloud | cloud.zops.top | auto | www.cloud.zops.top -> cloud.zops.top -> 161.97.81.52 |
Nachdem das ganze eingestellt ist, muss unser Server damit umgehen können. Die DNS-Einträge bringen alle Nutzer erstmal einfach direkt auf die IP unseres Servers - bis jetzt ist noch kein Service da.
Da der Browser bei http
-Anfragen grundsätzlich auf Port 80
schaut & bei https
auf Port 443
, würde ein Nutzer, der jetzt https://server.zops.top/
aufruft, einfach nach 161.97.81.52:443
geschickt werden.
Die offene Frage wäre jetzt:
Was machen wir, wenn wir noch weitere Subdomains haben, aber keine weiteren Server kaufen wollen? - Gleichzeitig, möchte ich immer Port 80 & 443 nutzen
An einem Server, kann man nicht den selben Port doppelt belegen, daher muss ein proxy
her.
Proxy / Edge router
Es gibt einige gute Lösungen, die wir problemlos mit docker
nutzen könnten. Folgende z. B.
Zuerst habe ich das Projekt mit Nginx reverse-proxy
gelöst, aber mir gefiel die Anbindung an Let's Encrypt
nicht und außerdem gibt es wenige Anpassungen, die ich am reverse-proxy
hinzufügen konnte (ohne es unnötig komplex zu machen), weshalb ich mich für Traefik
entschieden habe.
In naher Zukunft würden wir es aber höchstwahrscheinlich auf Envoy
ummünzen, da es im Benchmark besser abgeschnitten hat.
Ein reverse-proxy
- bzw. im Falle von Traefik
ein Edge Router
- beantwortet die große Frage von oben.
Alle Anfragen treffen zuerst auf Traefik
& Traefik
schaut sich dann die Domain an, bewertet wohin die Anfrage weitergeleitet werden soll und antwortet danach dem Nutzer.
Ich nutze Traefik
mit Docker Swarm
um eine Nextcloud
-Instanz und zwei Instanzen meiner Webseite zu erstellen. Die Instanzen der Webseite bekommen über den Traefik
-Loadbalancer die passende Menge an Anfragen zugespielt.
Docker Swarm
hilft uns dabei immer eine bestimmte Menge an Instanzen zu haben - so wird sofort eine neue Instanz erstellt, sollte die alte Instanz kaputt gehen. Natürlich muss man darauf achten, dass ein Container niemals so wichtig ist, dass wir es nicht einfach wegwerfen können - Pets vs. Cattle
.
Installation
Docker & Docker swarm
Ich denke mittlerweile sollten alle Docker installiert haben, falls nicht, dann einfach den Anweisungen hier folgen: https://docs.docker.com/get-docker/)
Docker swarm
kann man nutzen, wenn man möchte - muss man aber nicht! Falls also keine Replicas o. ä. benutzt werden sollen, reicht es einfach Docker
im normalen Modus zu belassen.
Hier die aktuelle Dokumentation dazu: https://docs.docker.com/engine/swarm/swarm-mode/
Grundsätzlich, muss man nur folgenden Befehl eingeben & sich den Token irgendwo aufschreiben.
docker swarm init
Sollte man mehrere Nodes haben, kann man über
docker swarm join --token ${TOKEN} ${IP}:${PORT}
dem swarm
beitreten.
docker-compose Dateien
Grundsätzlich sieht mein Ordneraufbau so aus:
.
|____traefik
| |____providers
| | |____file-provider.toml
| |____traefik.toml
| |____passwordfile
| |____tls.env
| |____Readme.md
| |____acme.json
| |____docker-compose.yaml
|
|____zrezai-dev
| |____dev
| | |____...
| | |____...
| |____Dockerfile.Hugo
| |____docker-compose.yaml
|
|____nexworking
| |____db.env
| |____redis.env
| |____nextcloud.env
| |____docker-compose.yaml
|
|____Taskfile.yml
nexworking
steht für Nextcloud
. In den nächstne Schritten gehen wir einmal alle Daten durch.
Der Gesamtaufbau sieht in etwa so aus:
Webseite (Hugo)
Für meine Webseite nutze ich Hugo, falls ihr etwas anderes benutzt, muss hier natürlich eines angepasst werden. Wenn ihr nur statisches html
habt, könnt ihr eigentlich alles genau so kopieren - nur der blog_builder
ist dann nicht mehr nötig.
ARG ALPINE_VERSION=3.12
FROM alpine:${ALPINE_VERSION} as hugobase
ENV HUGO_VERSION=0.74.3
WORKDIR /tmp
VOLUME [ "/tmp" ]
# Install HUGO
RUN set -x \
&& apk add --update wget ca-certificates libstdc++ libc6-compat \
&& wget -q -O hugo.tar.gz https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz \
&& tar xzf hugo.tar.gz hugo \
&& mv hugo /usr/bin \
## Cleanup
&& rm -r hugo.tar.gz \
&& apk del wget ca-certificates \
&& rm /var/cache/apk/*
Das Dockerfile ist recht minimalistisch - basierend auf Alpine, wird nur die aktuelle Version von Hugo
heruntergeladen und kann dann ausgeführt werden.
Als Volume
definiere ich hier /tmp
- darin können wir mit der docker-compose
unsere Datein mounten & dann mit dem hugo
-command statische html
-Dateien erstellen.
Das Image ist im Dockerhub hochgeladen und als xcalizorz:hugo:1.0-alpine
veröffentlicht - so können wir es mir Docker Swarm
oder Kubernetes
nutzen.
Im nächsten Schritt beschreiben wir unsere docker-compose.yaml
blog_builder
version: "3.8"
services:
blog_builder:
image: xcalizorz/hugo:1.0-alpine
deploy:
replicas: 1
placement:
max_replicas_per_node: 1
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: none
labels:
- traefik.enable=false
volumes:
- ./dev:/tmp
command: hugo --minify --baseUrl https://server.zops.top/
networks:
- web
...
..
.
Der erste Service ist der blog_builder
, mit dem gegebenen Command hugo --minify --baseUrl https://server.zops.top/
werden alle html
-Dateien erstellt und unter dem Ordner public
abgeworfen. So können dann andere Services auf den Ordner public
zugreifen, um dessen Inhalt zu veröffentlichen.
Die Labels sind hier eigentlich unwichtig, da bei beiden einfach gesagt wird bitte ignorieren. Dieser Service ist kurzlebig & endet, sobald der hugo
-Befehl beendet wurde.
blog
...
..
.
blog:
image: nginx:alpine
deploy:
replicas: 2
placement:
# sinnvoll bei vielen nodes & replicas
max_replicas_per_node: 2
resources:
# es ist besonders wichtig resourcen einzuschränken
limits:
cpus: '0.05'
memory: 50M
update_config:
# updates immer nur nacheinander & mit 10 sek. Wartezeit
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 30s
labels:
# traefik wird diesen Service beachten/einbinden
- traefik.enable=true
# Füge dem Router einen Service hinzu
- traefik.http.routers.blog.service=blogService
# Verlange tls-Verschlüsselung
- traefik.http.routers.blog.tls=true
# zrezaiResolver wird in der traefik-config definiert
- traefik.http.routers.blog.tls.certresolver=zrezaiResolver
# Die Außenwelt greift über Port 443 (websecure) auf diesen Service zu
- traefik.http.routers.blog.entrypoints=websecure
# Das Docker Netzwerk wird an traefik weitergegeben
- traefik.docker.network=web
# Da wir aktuell die simpelste Form von `nginx` nutzen, veröffentlichen wir nur auf Port 80 - `tls` etc. wird von `traefik` gemacht.
- traefik.http.services.blogService.loadbalancer.server.port=80
- traefik.http.services.blogService.loadbalancer.passhostheader=true
- traefik.http.services.blogService.loadbalancer.sticky.cookie=true
# Die Domain, die hier her führen soll
- traefik.http.routers.blog.rule=Host(`server.zops.top`)
# Alle Middlewares - werden bei Traefik definiert
- "traefik.http.routers.blog.middlewares=blogHeaders@file, simpleRatelimiter@file, simpleInflightreq@file"
depends_on:
- blog_builder
volumes:
# Die generierten Daten unter `public` werden an nginx weitergegeben
- ./dev/public:/usr/share/nginx/html
networks:
- web
networks:
# Es wird ein overlay Netzwerk kreiert
web:
driver: overlay
# Wenn der Name fehlt, bekommt das Netzwerk einen Präfix!
name: web
driver_opts:
# Das Netzwerk wird verschlüsselt
encrypted: "true"
Resourcen einzuschränken ist enorm wichtig. Sollte es z. B. jemals dazu kommen, dass eines der Container eingenommen wird & man nicht automatisiert oder sofort reagiert, könnte der Container die gesamten Resourcen des Servers auffressen.
In weiteren Posts werde ich mich etwas mehr mit Sicherheit befassen. Grundsätzlich sind die Dinge, die wir jetzt sehen nicht unbedingt super “unsicher”, da alles auf isolierten Containern basiert und wir auf viele best practices achten, aber es geht natürlich immer besser.
Weitere Daten brauchen wir für den Blog nicht.
Nextcloud
version: "3.8"
services:
db:
image: postgres:alpine
deploy:
replicas: 1
placement:
max_replicas_per_node: 1
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
labels:
# traefik soll diesen Service ignorieren
- traefik.enable=false
volumes:
- nc_db:/var/lib/postgresql/data
env_file:
# alle environment variablen sind in db.env definiert
- db.env
networks:
- nextcloud
nc_redis:
image: redis:alpine
deploy:
replicas: 1
placement:
max_replicas_per_node: 1
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
labels:
# traefik soll diesen Service ignorieren
- traefik.enable=false
ports:
- 6379
volumes:
- nc_redis:/data
- /etc/localtime:/etc/localtime:ro
networks:
- nextcloud
nextcloud:
image: nextcloud:apache
# für Apache
hostname: cloud.zops.top
deploy:
replicas: 1
placement:
max_replicas_per_node: 2
resources:
limits:
cpus: '0.50'
# Nextcloud hätte gerne 128MB bis 512MB RAM
memory: 512M
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
labels:
# traefik soll diesen service beachten
- traefik.enable=true
# es soll ein Zertifikat ausgestellt werden
- traefik.http.routers.nextcloud.service=nextcloudService
- traefik.http.routers.nextcloud.tls=true
- traefik.http.routers.nextcloud.tls.certresolver=zrezaiResolver
# die Außenwelt greift über Port 443 (websecure) auf diesen Service zu
- traefik.http.routers.nextcloud.entrypoints=websecure
# das Docker Netzwerk wird an traefik weitergegeben
- traefik.docker.network=nextcloud
- traefik.http.services.nextcloudService.loadbalancer.server.port=80
- traefik.http.services.nextcloudService.loadbalancer.passhostheader=true
- traefik.http.services.nextcloudService.loadbalancer.sticky.cookie=true
# Einstellungen damit caldav funktioniert
- traefik.http.middlewares.nextcloud-caldav.redirectregex.permanent=true
- traefik.http.middlewares.nextcloud-caldav.redirectregex.regex=^https://(.*)/.well-known/(card|cal)dav
- traefik.http.middlewares.nextcloud-caldav.redirectregex.replacement=https://$${1}/remote.php/dav/
# alle Middlewares - werden bei Traefik definiert
- "traefik.http.routers.nextcloud.middlewares=nextcloud-caldav@docker, nextcloudHeaders@file, nextcloudRatelimiter@file"
# die Domain, die hier her führen soll
- traefik.http.routers.nextcloud.rule=Host(`cloud.zops.top`)
volumes:
- nextcloud:/var/www/html
env_file:
# alle wichtigen env. variablen
- db.env
- redis.env
- nextcloud.env
depends_on:
- db
- nc_redis
networks:
- nextcloud
volumes:
nextcloud:
nc_db:
nc_redis:
networks:
# es wird ein overlay Netzwerk kreiert
nextcloud:
name: nextcloud
driver: overlay
driver_opts:
# verschlüsselt
encrypted: "true"
Env. files
Es gibt insgesamt drei .env
-Dateien.
|____nexworking
| |____db.env
| |____redis.env
| |____nextcloud.env
| |____docker-compose.yaml
# db.env
POSTGRES_PASSWORD=SamplePassword123
POSTGRES_DB=db_name
POSTGRES_USER=db_user
POSTGRES_HOST=nextcloud-stack_nc_db
Beim POSTGRES_HOST
kommt es darauf an wie der Datenbank Service und der docker swarm
stack heißt.
Bei mir heißt der Datenbank Service nc_db
und der stack
heißt nextcloud-stack
-> nextcloud-stack_nc_db
# redis.env
REDIS_HOST=nextcloud-stack_nc_redis
Hier gilt das gleiche wie bei POSTGRES_HOST
# nextcloud.env
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=NextcloudPassword
# alle Domänen mit Leerzeichen getrennt
NEXTCLOUD_TRUSTED_DOMAINS=www.cloud.zops.top cloud.zops.top
# wir wollen nur über https kommunizieren
OVERWRITEPROTOCOL=https
# hier kann man noch vielese mehr hinzufügen
## z. B. SMTP Server, um E-Mails zu versenden
## Gerne in die Nextcloud Doku. schauen
Traefik
Jetzt kommen wir zum aller wichtigsten.
.
|____traefik
| |____providers
| | |____file-provider.toml
| |____traefik.toml
| |____passwordfile
| |____tls.env
| |____acme.json
| |____docker-compose.yaml
Gehen wir schrittweise durch docker-compose.yaml
, um zu verstehen wozu die ganzen anderen Daten gut sind.
docker-compose.yaml
version: "3.8"
services:
traefikRouter:
image: traefik:v2.2
depends_on:
- dockerproxy
deploy:
placement:
constraints:
# traefik sollte auf der manager node laufen
- node.role == manager
# aktuell möchte ich es nur einmal haben
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 30s
labels:
- traefik.enable=true
- traefik.docker.network=traefik-network
# das Dashboard soll unter `traefik.zops.top` erreichbar sein
- traefik.http.routers.traefikRouter.rule=Host(`traefik.zops.top`)
# entrypoint ist `traefik` (Port 8080)
- traefik.http.routers.traefikRouter.entrypoints=traefik
# ein Zertifikat soll erstellt werden
- traefik.http.routers.traefikRouter.tls=true
- traefik.http.routers.traefikRouter.tls.certresolver=zrezaiResolver
# api@interal ist ein interner Service, den wir nutzen wollen
- traefik.http.routers.traefikRouter.service=api@internal
# Alle middlewares - definition kommt gleich
- "traefik.http.routers.traefikRouter.middlewares=auth@file, simpleHeaders@file"
# dummy service für Swarm port detection. Der Port ist egal.
- traefik.http.services.dummy-svc.loadbalancer.server.port=9999
# globaler redirect to https
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
ports:
# wir wollen unter Port 80, 443 und 8080 kontaktiert werden (entrypoints)
- 80:80
- 443:443
- 8080:8080
volumes:
# die Konfigurationsdatei für traefik
- ./traefik.toml:/traefik.toml
# hierin werden die tls Zertifikate gespeichert
- ./acme.json:/acme.json
# unser admin user & gehashtes Password für das Dashbord
- ./passwordfile:/var/passwordfile
# extra Konfigurationen (für z. B. Middlewares)
- ./providers:/providers
env_file:
# env. variablen für Cloudflare TLS (nicht nötig, wenn wir HTTP Verifikation nutzen)
- ./tls.env
networks:
- traefik-network
- web
- nextcloud
# ein docker proxy - dieser ist von der Außenwelt nicht erreichbar
# nur der dockerproxy hat Zugriff auf unseren Docker Socket (Sicherheitsrisiko)
# Traefik greift über Anfragen auf diesen Container dann auf unser Socket zu
# Sollte Traefik je angegriffen werden, ist es etwas schwieriger vollen
# Zugriff auf unseren Docker Socket zu bekommen
##
# Wenn jemand Zugriff auf Docker hat, hat er auch quasi vollen Zugriff
# auf den Hostserver (!)
dockerproxy:
environment:
CONTAINERS: 1
NETWORKS: 1
SERVICES: 1
TASKS: 1
image: tecnativa/docker-socket-proxy
labels:
- traefik.enable=true
networks:
- traefik-network
ports:
- 2375
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
# Integration der beiden anderen Netzwerke
# Traefik kann sonst nicht auf die Services darin zugreifen
web:
external: true
nextcloud:
external: true
traefik-network:
driver: overlay
name: traefik-network
driver_opts:
encrypted: "true"
traefik.toml
Ich habe einige Kommentare entfernt, damit es nicht zu lang wird.
################################################################
# Global configuration
################################################################
[global]
checkNewVersion = true
sendAnonymousUsage = false
################################################################
# Entrypoints configuration
################################################################
# Entrypoints definition
#
# Optional
# Default:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
################################################################
# Traefik logs configuration
################################################################
# Traefik logs
# Enabled by default and log to stdout
#
# Optional
#
[log]
level = "ERROR"
################################################################
# Access logs configuration
################################################################
# Enable access logs
# By default it will write to stdout and produce logs in the textual
# Common Log Format (CLF), extended with additional fields.
#
# Optional
#
[accessLog]
################################################################
# API and dashboard configuration
################################################################
# Enable API and dashboard
[api]
################################################################
# Ping configuration
################################################################
# Enable ping
[ping]
################################################################
# Docker configuration backend
################################################################
# Enable Docker configuration backend
[providers.docker]
endpoint = "tcp://dockerproxy:2375"
network = "traefik-network"
swarmMode = true
swarmModeRefreshSeconds = "60s"
useBindPortIP = true
exposedByDefault = false
# Enable file configuration backend
[providers.file]
directory = "/providers"
# Enable ACME (Let's Encrypt): automatic SSL.
[certificatesResolvers.zrezaiResolver.acme]
email = "[email protected]"
storage = "acme.json"
# CA server to use.
# Uncomment the line to use Let's Encrypt's staging server,
# leave commented to go to prod.
#
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
caServer = "https://acme-v02.api.letsencrypt.org/directory"
# KeyType to use.
#
# Optional
# Default: "RSA4096"
#
# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
#
keyType = "RSA4096"
# Use a TLS-ALPN-01 ACME challenge.
#
# Optional (but recommended)
#
# [certificatesResolvers.zrezaiResolver.acme.tlsChallenge]
# Use a HTTP-01 ACME challenge.
#
# Optional
#
# [certificatesResolvers.zrezaiResolver.acme.httpChallenge]
# EntryPoint to use for the HTTP-01 challenges.
#
# Required
#
# entryPoint = "web"
# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
# Note: mandatory for wildcard certificate generation.
#
# Optional
#
[certificatesResolvers.zrezaiResolver.acme.dnsChallenge]
provider = "cloudflare"
Im groben sagen die Kommentare eigentlich schon alles.
Da meine DNS Cloudflare ist, habe ich als let's encrypt
-challenge die dnsChallenge
ausgewählt.
Falls ihr auch die den acme
-test über DNS
machen wollt, benötigt ihr eine tls.env
, welche im aktuellen docker-compose.yaml
angesprochen wird.
Die tls.env
sieht wie folgt aus:
CF_API_EMAIL=[email protected]
CF_ZONE_API_TOKEN=
CF_DNS_API_TOKEN=
Die Zone
und DNS
API muss im Cloudflare Dashboard mit folgenden Rechten erstellt werden:
Typ | Description |
---|---|
CF_DNS_API_TOKEN | API token with DNS:Edit permission (since v3.1.0) |
CF_ZONE_API_TOKEN | API token with Zone:Read permission (since v3.1.0) |
Weitere Informationen hier: https://go-acme.github.io/lego/dns/cloudflare/
Wenn man die überprüfung nicht über DNS machen will, können wir auch ganz einfach die HTTP-01 ACME challenge
oder die TLS-ALPN-01 ACME challenge
nutzen.
Bensonders interessant ist folgenden Teil der Konfiguration:
# Enable Docker configuration backend
[providers.docker]
endpoint = "tcp://dockerproxy:2375"
network = "traefik-network"
swarmMode = true
swarmModeRefreshSeconds = "60s"
useBindPortIP = true
exposedByDefault = false
Da wir die dockerproxy
nutzen, um unser Docker Socket nicht offen für den traefik
-Container rumliegen zu lassen, müssen wir natürlich traefik
sagen wo unser docker endpoint
ist.
exposedByDefault
ist auf falsch gesetzt, damit traefik
nicht alle Container exposed
, sondern nur jene die wir über das traefik.enabled
-label veröffentlichen wollen.
Der andere besonders wichtige Teil ist der provider
. In traefik
kann man außerhalb der Hauptkonfiguration, weitere Konfigurationen hinzufügen. Alle middleware
Konfiguration müssen über provider
geregelt werden.
Wir sagen in traefik.toml
, dass wir im Ordner providers
Extrakonfiguration haben:
# Enable file configuration backend
[providers.file]
directory = "/providers"
Innerhalb dieses Ordners ist aktuell nur die file-provider.toml
, jedoch können da viele weitere Konfigurationsdateien sein und alles wird normal eingelesen.
[http.middlewares]
[http.middlewares.simpleRatelimiter.rateLimit]
average = 5
period = "1s"
burst = 50
[http.middlewares.nextcloudRatelimiter.rateLimit]
average = 5
period = "1s"
burst = 150
[http.middlewares.simpleInflightreq.inFlightReq]
amount = 50
[http.middlewares.auth.basicAuth]
usersFile = "/var/passwordfile"
removeheader = true
[http.middlewares.blogHeaders.headers]
frameDeny = true
sslRedirect = true
accessControlAllowMethods= ["GET"]
accessControlMaxAge = 100
addVaryHeader = true
stsSeconds = 31536000
stsIncludeSubdomains = true
stsPreload = true
contentTypeNosniff = true
browserXssFilter = true
referrerPolicy = "no-referrer"
featurePolicy = "geolocation 'none'; camera 'none'; microphone 'none'"
contentSecurityPolicy = """
default-src 'self' https;
script-src 'self' 'unsafe-inline' cdn.jsdelivr.net ajax.cloudflare.com;
style-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com fonts.googleapis.com;
font-src 'self' cdnjs.cloudflare.com fonts.googleapis.com fonts.gstatic.com;
img-src 'self' i.giphy.com;
"""
[http.middlewares.blogHeaders.headers.customResponseHeaders]
Expect-CT = "max-age=604800, enforce;"
X-Permitted-Cross-Domain-Policies = "none"
server = "Mein Server"
[http.middlewares.nextcloudHeaders.headers]
sslRedirect = true
accessControlMaxAge = 100
addVaryHeader = true
stsSeconds = 31536000
stsIncludeSubdomains = true
stsPreload = true
contentTypeNosniff = true
browserXssFilter = true
referrerPolicy = "no-referrer"
featurePolicy = "geolocation 'none'; camera 'none'; microphone 'none'"
contentSecurityPolicy = """
default-src 'self' https;
script-src 'self' 'unsafe-inline' cdn.jsdelivr.net ajax.cloudflare.com;
style-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com fonts.googleapis.com;
font-src 'self' cdnjs.cloudflare.com fonts.googleapis.com fonts.gstatic.com;
frame-ancestors 'self';
"""
[http.middlewares.nextcloudHeaders.headers.customResponseHeaders]
Expect-CT = "max-age=604800, enforce;"
X-Permitted-Cross-Domain-Policies = "none"
X-Frame-Options = "SAMEORIGIN"
server = "Mein Server"
[http.middlewares.simpleHeaders.headers]
frameDeny = true
sslRedirect = true
accessControlMaxAge = 100
addVaryHeader = true
stsSeconds = 31536000
stsIncludeSubdomains = true
stsPreload = true
contentTypeNosniff = true
browserXssFilter = true
referrerPolicy = "no-referrer"
featurePolicy = "geolocation 'none'; camera 'none'; microphone 'none'"
contentSecurityPolicy = """
default-src 'self' https;
"""
[http.middlewares.simpleHeaders.headers.customResponseHeaders]
Expect-CT = "max-age=604800, enforce;"
X-Permitted-Cross-Domain-Policies = "none"
server = "Mein Server"
Hier definiere ich einige globale wie wichtige HTTP Header
, rate limiting
und authentification
.
Mit diesen Headern sollte man eine A
-Bewertung bei Überprüfungen der Security Header haben. Je nachdem wie eure Services arbeiten und worauf sie zugreifen, sollte man Dinge wie contentSecruityPolicy
ändern. Die simpleHeaders
-Middleware sollte aber bei den meisten Services passen.
Ein Rätsel könnte das hier sein:
[http.middlewares.auth.basicAuth]
usersFile = "/var/passwordfile"
removeheader = true
Hier definieren wir die middleware
mit dem Namen auth
und ihr Typ ist basicAuth
. Die User, die dann auf die Domains zugreifen können, werden in usersFile
definiert.
Die Daten besteht aus Zeilen von user:hash(password)
- also alle Passwörter müssen mit MD5
, SHA1
oder Bcrypt
gehashed sein. Ich nutze Bcrypt
, da es vermutlich die beste hashing-methode von den angebotenen ist.
htpasswd -nbB USER "PASSWORD" | sed '/^$/d' >> passwordfile
Damit wird eine Datein passwordfile
erstellt oder erweitert mit dem USER
und dem gehashtem Password. Mit sed
entfernen wir einfach nur leere Zeilen aus dem Ergebnis von htpasswd
.
Starten der Services
docker stack deploy -c nexworking/docker-compose.yaml nextcloud-stack
docker stack deploy -c zrezai-dev/docker-compose.yaml blog-stack
docker stack deploy -c traefik/docker-compose.yaml traefik-stack
docker service rm blog-stack_blog_builder
Damit werden nacheinander alle Services gestartet. Es ist nur wichtig, dass traefik
zuletzt gestartet wird.
Misc (Automatisierung)
Starten und beenden der docker stacks
kann nervig sein, wenn man alles per Hand macht. Daher hier ein kleines Script, was das ganze automatisiert.
Taskfile
ist ein cooles kleines Tool, was dabei helfen kann solche Aufgaben schnell zu automatisieren.
Hier ist mein Taskfile.yml
- es liegt auf der selben Höhe wie die Ordner.
version: '2'
tasks:
default:
desc: Start all services
deps:
- task: nextcloud
vars:
CMD: "deploy -c nexworking/docker-compose.yaml"
- task: zrezai-dev
vars:
CMD: "deploy -c zrezai-dev/docker-compose.yaml"
- task: vault
vars:
CMD: "deploy -c vault/docker-compose.yaml"
cmds:
- task: traefik
vars:
CMD: "deploy -c traefik/docker-compose.yaml"
- task: zrezai-dev-remove-builder
up:
desc: Start all services
cmds:
- task: default
down:
desc: Stop all services
deps:
- task: traefik
vars:
CMD: "rm"
cmds:
- task: nextcloud
vars:
CMD: "rm"
- task: zrezai-dev
vars:
CMD: "rm"
- task: vault
vars:
CMD: "rm"
traefik:
desc: Start traefik
cmds:
- docker stack {{.CMD}} traefik-stack
silent: false
nextcloud:
desc: Start Nextcloud
cmds:
- docker stack {{.CMD}} nextcloud-stack
zrezai-dev:
desc: Start personal blog
cmds:
- docker stack {{.CMD}} blog-stack
zrezai-dev-remove-builder:
desc: Remove unneeded builder services
cmds:
- docker service rm blog-stack_blog_builder
Recht primitiv, aber jetzt kann man einfach mit task
oder task up
alle Stacks in der korrekten Reihenfolge starten & den blog_builder
korrekt löschen. Mit task down
werden alle stacks
wieder beendet & ihre Überreste werden entfernt.