Bei der Mutual SSL Authentication authentifizieren sich der Server und der Client gegenseitig.
PoC Setup
In diesem PoC authentifizieren sich der Load Balancer und der Server 1, bzw. Load Balancer und der Server 2, gegenseitig.
Der User kann durch den Load Balancer die Seite des Server 1, bzw. Server 2, aufrufen.
Ein direkter Aufruf der Seite auf den Servern, also ohne Load Balancer, ist nicht möglich, da nur der Load Balancer sich an den Servern 1&2 authentifizieren kann.
Zu Testzwecken wird allerdings Server 2 offen gehalten, eine Authentifizierung ist nicht erforderlich.
Keys und Zertifikate
Es wird eine eigene CA zum signieren der Zertifikate benötigt.
Für jeden Server wird ein Private Key und signierter Public Key benötigt.
Der LoadBalancer benötigt noch einen kombinierten Key aus seinem Private und Public Key.
Alle Dateien werden im Ordner certs abgelegt.
## Create CA
# Generate CA Key:
openssl genrsa -out ca-key.pem 2048
# Generate CA Certificate:
openssl req -x509 -new -nodes -extensions v3_ca -key ca-key.pem -days 1024 -out ca-root.pem -sha512 -subj "/C=DE/ST=NRW/L=Leverkusen/O=DerIngo/OU=IT Department/CN=deringo.de"
## Generate LoadBalancer
# Generate LoadBalancer Key:
openssl genrsa -out loadbalancer-key.pem 2048
## Generate CSR LoadBalancer
vim loadbalancer_csr_details.txt
openssl req -new -key loadbalancer-key.pem -out loadbalancer.csr -config loadbalancer_csr_details.txt
## Sign CSR
vim loadbalancer_ssl-extensions-x509.conf
openssl x509 -req -in loadbalancer.csr -CA ca-root.pem -CAkey ca-key.pem -CAcreateserial -out loadbalancer.crt -days 365 -sha512 -extensions v3_ca -extfile loadbalancer_ssl-extensions-x509.conf
## Mutual SSL - Reverse Proxy
## Create a file with the private and the signed public key:
# copy private key
cp loadbalancer-key.pem loadbalancer-combined.key
# copy public key (without public key of CA)
cat loadbalancer.crt >> loadbalancer-combined.key
## Add Public Key of CA to Public Key of RP
cat ca-root.pem >> loadbalancer.crt
## Generate Server_1
# Generate Server_1 Key:
openssl genrsa -out server_1-key.pem 2048
## Generate CSR Server_1
vim server_1_csr_details.txt
openssl req -new -key server_1-key.pem -out server_1.csr -config server_1_csr_details.txt
## Sign CSR
vim server_1_ssl-extensions-x509.conf
openssl x509 -req -in server_1.csr -CA ca-root.pem -CAkey ca-key.pem -CAcreateserial -out server_1.crt -days 365 -sha512 -extensions v3_ca -extfile server_1_ssl-extensions-x509.conf
## Add Public Key of CA to Public Key of RP
cat ca-root.pem >> server_1.crt
## Generate Server_2
# Generate Server_2 Key:
openssl genrsa -out server_2-key.pem 2048
## Generate CSR Server_2
vim server_2_csr_details.txt
openssl req -new -key server_2-key.pem -out server_2.csr -config server_2_csr_details.txt
## Sign CSR
vim server_2_ssl-extensions-x509.conf
openssl x509 -req -in server_2.csr -CA ca-root.pem -CAkey ca-key.pem -CAcreateserial -out server_2.crt -days 365 -sha512 -extensions v3_ca -extfile server_2_ssl-extensions-x509.conf
## Add Public Key of CA to Public Key of RP
cat ca-root.pem >> server_2.crt
Mutual SSL
LoadBalancer
Create a file with the private and the signed public key (bereits im Schritt zuvor erfolgt):
## Mutual SSL - Reverse Proxy
## Create a file with the private and the signed public key:
# copy private key
cp loadbalancer-key.pem loadbalancer-combined.key
# copy public key (without public key of CA)
cat loadbalancer.crt >> loadbalancer-combined.key
To communicate to the application server with SSL we need this lines in our HTTP configuration:
SSLProxyEngine On
SSLProxyMachineCertificateFile /usr/local/apache2/conf/combined.key
Der Fehler trat auf, nachdem ich meinen PoC von meinem Entwicklungsrechner in die Cloud transferierte und dort laufen ließ und dort testen wollte.
Auf der Console des Servers sieht der Aufruf mittels "curl http://localhost/private/index.html" auch erstmal gut aus.
Der Zugriff aus dem Browser mittels IP "http:///private/index.html" schlägt mit obiger Fehlermeldung komplett fehl.
Lokal nachstellen
Um das Problem lokal auf dem Entwicklerlaptop nachstellen zu können, muss erstmal die lokale (Docker-)IP ermittelt werden:
ping host.docker.internal
Ping wird ausgeführt für host.docker.internal [192.168.2.149] mit 32 Bytes Daten:
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Der Aufruf der PRIVATE Page, nach OneLogin Login versteht sich, funktioniert über localhost:
Der Aufruf der PRIVATE Page funktioniert nicht über die IP:
Auszug aus dem Logfile des Apache HTTP Servers:
reverseproxy_1 | [Thu Jun 09 13:00:54.048377 2022] [auth_openidc:error] [pid 256:tid 140563693147904] [client 172.24.0.1:33902] oidc_authenticate_user:
the URL hostname (localhost) of the configured OIDCRedirectURI does not match the URL hostname of the URL being accessed (192.168.2.149): the "state" a
nd "session" cookies will not be shared between the two!, referer: http://192.168.2.149/
reverseproxy_1 | 172.24.0.1 - - [09/Jun/2022:13:00:54 +0000] "GET /private/index.html HTTP/1.1" 500 531
Der Fehler besagt also, "the URL hostname (localhost) of the configured OIDCRedirectURI does not match the URL hostname of the URL being accessed (192.168.2.149)".
OneLogin Konfigurationsanpassung
Die Redirect URIs Liste muss um die IP des Servers erweitert werden:
Allerdings ist http als Protokoll lediglich für localhost zugelassen, nicht aber für alle anderen URIs, wie zB unsere Server IP oder später der Domain Name des Servers.
Die Redirect URI muss also mit https eingetragen werden.
Daraus ergibt sich allerdings auch, dass unser Server über https erreichbar sein muss!
Apache mit HTTPS
Ein selbstsigniertes Zertifikat ist für unseren PoC vollkommen ausreichend. Eine Signierung durch zB Let's Encrypt ist nicht notwendig.
Für diesen PoC brauchen wir lediglich den Reverse Proxy, der im ersten Schritt eine Seite über http und https ausliefern kann. Später kommt dann die Authentifizierung mit OneLogin hinzu.
Schritt 1: http
Das Dockerfile des Apache aus den vorherigen PoCs uA schon mit OpenID Module:
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Die Einbindung der Konfigurationsdateien erfolgt über reverseproxy_httpd.conf, die in den Container als httpd.conf hineinkopiert wird:
# load original configuration first
Include conf/container_httpd.conf
# customized configuration
ServerName reverseproxy
Include conf/reverseproxy.conf
Include conf/reverseproxy-ssl.conf
Die Konfiguration für http erfolgt in reverseproxy.conf:
Die Seite ist über http://localhost:443 erreichbar. Richtig: http, nicht https! Verschlüsselt wird hier noch nix.
Schritt 3: https
Als ersten Teilschritt wird die reverseproxy-ssl.conf mit der Default-SSL-Konfiguration überschrieben und alle Kommentare gelöscht.
Folgende Änderungen werden gemacht:
ServerName localhost:443
ServerAdmin deringo@github.com
SSLEngine off
#SSLCertificateFile "/usr/local/apache2/conf/server.crt"
#SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
Server neu starten, anschließend ist die Seite weiterhin unverschlüsselt über http://localhost:443 erreichbar.
Als nächster Teilschritt wird SSLEngine on gestellt und die beiden CertificateFile Einträge wieder ent-kommentiert. Außerdem müssen die Module mod_socache_shmcb.so und mod_ssl.so geladen werden.
Server neu durchstarten und der unverschlüsselte Aufruf wird abgewiesen:
Der verschlüsselte Aufruf über https://localhost funktioniert:
Allerdings wird die Seite als "Nicht sicher" angezeigt und es musste einmalig ein entsprechender Hinweis weggeklickt werden, da das Zertifikat nicht von einer vertrauenswürdigen Stelle signiert wurde.
Da am Ende praktisch nichts von der Standard-Konfiguration für SSL geändert wurde, wird die Konfiguration wieder vereinfacht:
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf
Schritt 4: OneLogin
Die OneLogin Konfiguration wird hinzugefügt:
## Default SSL
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf
##
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost:443
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase
OIDCProviderMetadataURL
OIDCClientID
OIDCClientSecret
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI https://localhost/private/redirect_uri
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
AuthType openid-connect
Require valid-user
Alle Seiten sind über https erreichbar.
Aber leider auch die Private Page OHNE dass vorher ein Login über OneLogin erfolgen musste.
Irgendwie klappt das nicht mit dem überschreiben/erweitern der default SSL Konfiguration. Also doch alles in eine Datei:
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Listen 443
SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLHonorCipherOrder on
SSLProtocol all -SSLv3
SSLProxyProtocol all -SSLv3
SSLPassPhraseDialog builtin
SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
SSLSessionCacheTimeout 300
##
## SSL Virtual Host Context
##
# General setup for the virtual host
ServerName localhost:443
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ErrorLog /proc/self/fd/2
TransferLog /proc/self/fd/1
# SSL Engine Switch:
# Enable/Disable SSL for this virtual host.
SSLEngine on
SSLCertificateFile "/usr/local/apache2/conf/server.crt"
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
SSLOptions +StdEnvVars
SSLOptions +StdEnvVars
BrowserMatch "MSIE [2-5]" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
CustomLog /proc/self/fd/1 \
"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase
OIDCProviderMetadataURL
OIDCClientID
OIDCClientSecret
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI https://localhost/private/redirect_uri
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
AuthType openid-connect
Require valid-user
So funktioniert es wie gewünscht und die Private Page wird erst nach Login angezeigt.
Allerdings nur über localhost. Nicht über die IP.
Die OIDCRedirectURI wird von https://localhost/private/redirect_uri auf https://192.168.2.149/private/redirect_uri geändert und schon funktioniert es genau anders herum: Nur über die IP, nicht über localhost.
Das ist nicht ganz so, wie ich mir das vorgestellt habe, aber für den PoC sollte es ausreichend sein.
Schritt 5: Nur https
Alles was über http rein kommt, soll über https weitergeleitet werden:
LoadModule rewrite_module modules/mod_rewrite.so
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Schritt 6: Docker
Die Generierung der Zertifikate erfolgte im laufenden Container. Dieser Schritt soll schon beim Erzeugen des Images erfolgen.
Ein Problem war, dass dies interaktiv erfolgen musste. Mit dem Parameter -subj "/" können Key & Zertifikat ohne Interaktion generiert werden. Im Subject könnten Daten eingetragen werden, dies ist aber für den PoC gar nicht erforderlich.
Einmal der Befehl vollständig, wie er im Container ausgeführt werden könnte:
Version 2 aus dem Beispiel Reverse Proxy mit OneLogin soll zu Testzwecken in der Cloud zum Laufen gebracht werden.
Bei dem PoC handelt es sich um einen Apache HTTP-Server mit simpler Public Page und OneLogin Authentifizierung für den Zugriff auf die Private Page: der ShowHeaders Anwendung in einem Apache Tomcat-Server. Beide Server laufen in Docker Containern, die über Docker Compose gestartet werden.
Cloud Server einrichten
Auf dem Server muss Docker etc. eingerichtet werden:
sudo su
yum install docker -y
systemctl umask docker
systemctl start docker
docker version
curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose
docker-compose --version
# # Output:
# Docker Compose version v2.2.3
yum install git -y
mkdir /app
cd /app
git clone https://github.com/DerIngo/DockerOneLoginApacheSample.git
vim /app/DockerOneLoginApacheSample/version2/reverseproxy/conf/reverseproxy.conf
Fehler
Auf dem lokalen Entwicklerlaptop läuft der PoC und entsprechend soll er auch in der Cloud gestartet werden:
sudo su
cd /app/DockerOneLoginApacheSample/version2
docker-compose up
Zuerst sieht alles wie gewohnt aus, bis Maven ShowHeaders bauen soll:
root@showheaders:/usr/local/tomcat/ShowHeaders# mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< deringo:ShowHeaders >-------------------------
[INFO] Building ShowHeaders 0.0.1-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-catalina/8.5.53/tomcat-catalina-8.5.53.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.956 s
[INFO] Finished at: 2022-06-07T12:28:46Z
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project ShowHeaders: Could not resolve dependencies for project deringo:ShowHeaders:war:0.0.1-SNAPSHOT: Failed to collect dependencies at org.apache.tomcat:tomcat-catalina:jar:8.5.53: Failed to read artifact descriptor for org.apache.tomcat:tomcat-catalina:jar:8.5.53: Could not transfer artifact org.apache.tomcat:tomcat-catalina:pom:8.5.53 from/to central (https://repo.maven.apache.org/maven2): Transfer failed for https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-catalina/8.5.53/tomcat-catalina-8.5.53.pom: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException
Es kann eine Dependency nicht aufgelöst werden und im Text findet sich dieser Satz: "PKIX path building failed". Also irgendwas mit Zertifikaten, wie mir die Erfahrung sagt.
Nach einer Recherche ist klar: In dem Cloud Setup wird der ausgehende Traffic durch einen Proxy geleitet und dessen Zertifikate sind nicht in den Standard-Zertifikaten enthalten.
Simple Lösung: Validierung aushebeln
Die schnellste Lösung: Einfach auf jedwede Validierung verzichten.
In dem Dockerfile von ShowHeaders wird der Maven Aufruf um obige Parameter ergänzt:
WORKDIR ShowHeaders
# aus
RUN mvn package
# wird
RUN mvn package -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true
und schon kann das Image wieder erfolgreich gebaut werden.
Der gleiche Fehler
Die Images werden gebaut, die Container starten und die Public Page wird korrekt angezeigt auf http://localhost:80/public
Aber: die Private Page auf http://localhost:80/private wird nicht angezeigt, bzw. wird nicht mal das OneLogin Login angezeigt.
Das Problem findet sich in der OpenID Konfiguration für OneLogin:
Das funktioniert prinzipiell, dauert aber sehr lange. Außerdem wird in einem ausführlichen Kommentar darauf hingewiesen, dass die Intermediate Certificates so nicht gezogen würden.
Eine ausführliche Anleitung findet sich auf Baeldung, deren Ansatz ist wesentlich schneller (von mir nicht auf Intermediate C. getestet):
Anschließend wieder gestoppt, Reverse Proxy Image neu bauen lassen, Container wieder gestartet: Beim ersten curl kommt der "SSL certificate problem: unable to get local issuer certificate"-Fehler, bei weiteren curls der "The document has moved"-Hinweis.
Schauen wir mal, wie das im Container aussieht:
Vorbereitung:
# frisch starten:
docker-compose down -v
docker-compose build reverseproxy
docker-compose up
# vor dem ersten Aufruf: Ab in den Container:
docker exec -it version2-reverseproxy-1 bash
Im Container:
# Im Container:
apt install curl
# 1. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
# 2. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
# 3. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
{"acr_values_supported":["onelogin:nist:level:1:re-auth"],"authorization_endpoint":"https://deringo-dev.onelogin.com/oidc/2/auth","claims_parameter_suppor[...]
Ich habe es noch ein paar mal ausprobiert, ungefähr jedes zweite bis dritte Mal hat es funktioniert.
Offensichtlich ist da noch ein LoadBalancer zwischen. 🤦♂️🤦♂️🤦♂️
Mehrmaliges pingen gab zwei verschiedene IPs zurück.
apt install nmap
nmap deringo-dev.onelogin.com
Starting Nmap 7.80 ( https://nmap.org ) at 2022-06-07 14:18 UTC
Nmap scan report for deringo-dev.onelogin.com (52.29.255.229)
Host is up (0.0052s latency).
Other addresses for deringo-dev.onelogin.com (not scanned): 52.29.255.230
rDNS record for 52.29.255.229: ec2-52-29-255-229.eu-central-1.compute.amazonaws.com
Not shown: 994 filtered ports
PORT STATE SERVICE
21/tcp open ftp
53/tcp open domain
80/tcp open http
443/tcp open https
554/tcp open rtsp
1723/tcp open pptp
Aha! Es gibt zwei IPs zu dem Domainnamen und anscheinend ist OneLogin in den AWS.
Nochmal ein anderer Test; Wie ist das denn, wenn ich nicht im Container, sondern direkt vom Host curle:
Jeder Aufruf funktioniert, diesmal sieht es wirklich gut aus.
Umsetzung
Das Dockerfile des Reverse Proxies:
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
COPY /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
RUN update-ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Anschließend das Image neu bauen:
docker-compose build reverseproxy
Ich fasse es nicht:
Step 3/6 : COPY /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
1 error occurred:
* Status: COPY failed: file not found in build context or excluded by .dockerignore: stat etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt: filedoes not exist, Code: 1
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
COPY ./ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
RUN update-ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
cp /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /app/DockerOneLoginApacheSample/version2/reverseproxy/ZscalerCloudChain.crt
docker-compose build reverseproxy
docker-compose up
Test
curl localhost/private/
Keine Fehler im Log! 🎉
Test im Browser mit Login
Steht noch aus, aktuell ist der Server nur über Console zu erreichen.
UPDATE: Inzwischen wurde der Zugriff per Browser auf den Server freigeschaltet (bzw. eine fehlerhafte Netzwerkkonfiguration gefixt) und die Public Seiten sind zugänglich.
Bei Aufruf der Private Seiten kommt allerdings eine Fehlermeldung:
Wir wollen unsere Anwendung durch einen Filter schützen, der nur Anfragen von eingeloggten Benutzern mit der richtigen Rolle hindurch lässt.
Für diesen PoC bauen wir einen Reverse Proxy für den Login und einen Anwendungsserver für den Filter. Der Anwendungsserver bekommt einen Zugang über den RP und einen Zugang ohne RP, um so zu zeigen, dass nur über den RP auf die Anwendung zugegriffen werden kann.
In produktiven Umgebungen darf es den Zugang ohne RP so ungeschützt nicht geben, da es sehr einfach ist, zB über ein Browser-Plugin, den RP mittels selbstgesetzter Header zu faken.
Reverse Proxy
Der Reverse Proxy bekommt grundsätzlich die gleiche Konfiguration wie in den Beispielen zuvor.
Hier müssen wir aber noch den Scope params hinzufügen, um so die OneLogin-Rollen des Benutzers zu übertragen:
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
Im nächsten Fenster des Dialoges die Value ("User Roles") setzen:
Access Filter
Der Access Filter prüft, ob eine UserID vorhanden ist. Das ist in unserem Beispiel die Email, es könnte aber auch der Preferred Username genommen werden.
Des weiteren prüft der Access Filter, ob der Benutzer die erforderliche Rolle user hat.
Sind beide Bedingungen erfüllt, kann auf die Anwendung zugegriffen werden, ansonsten wird lediglich "Access denied" angezeigt.
Im Docker File des Anwendungsservers kopieren wir erst nur das Maven POM, lassen dann Maven bauen, um so die Abhängigkeiten herunterzuladen. Erst danach kopieren wir das gesamte Projekt in den Container und lassen die Anwendung bauen. So wird nach Code Änderungen, ohne Anpassungen in Maven, der Bau des Images beschleunigt, da nicht jedes Mal die Bibliotheken heruntergeladen werden müssen.
FROM tomcat:8.5-jdk8-openjdk-slim
RUN apt update && apt install -y \
maven
COPY pom.xml /app/pom.xml
WORKDIR /app
RUN mvn package
COPY . /app
RUN mvn package
WORKDIR $CATALINA_HOME
RUN mv /app/target/ROOT.war webapps
EXPOSE 8080
CMD ["catalina.sh", "run"]
Test
Zugriff auf den Anwendungsserver über den Reverse Proxy nach Anmeldung:
http://localhost/private/test.html
Direkter Zugriff auf den Anwendungsserver ohne Anmeldung:
Ich hatte zuerst versucht, den Reverse Proxy über scale zu vervielfältigen, aber das funktionierte nicht, da jeder RP den selben Hostnamen zugewiesen bekommt. Laut Forenkommentaren soll man das Problem wohl mittels Scripte oder Docker Swarm lösen können, für dieses kleine Projekt war es hingegen völlig ausreichend, den Block für den RP zu duplizieren.
Load Balancer
Neu hinzugekommen ist der den beiden RPs vorgeschaltete Load Balancer.
Die Regel, nach der das Loadbalancing erfolgt, ist hier nicht relevant und wird nicht explizit gesetzt.
Der erste Versuch, bei dem die Anfragen abwechselnd auf den RPs verteilt werden funktioniert für die public Pages.
Der Login bei OneLogin funktioniert, aber nicht das öffnen der Seite. Anscheinend harmoniert der OneLogin Flow nicht mit diesem Setup, es scheint so, als ob die Antwort der Anfrage von RP1 an OneLogin von RP2 erhalten wird, dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP2 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird, [...]
Es ist also notwendig, das wir immer auf dem selben RP landen. Das Load Balancing darf nur einmal am Anfang statt finden.
Um das zu erreichen, setzten wir einen Header, der die Route zum RP enthält und setzten die Session sticky.
Im nächsten Schritt möchte ich verschiedene Netzwerke und Server verwenden.
Der User kommt aus dem Internet und geht über den Load Balancer in die DMZ, in der er über die RPs Zugang zu den Public Servern hat und nach Authentifizierung über OneLogin (Internet) gelangt er in das Interne Netz wo er Zugang auf den ShowHeaders und die Privaten Server hat.
Vorbereitet wird auch schon die Authorisierung über die RPs und OneLogin: Falls der Benutzer die Rolle user hat, bekommt er Zugang auf den User Server, falls er die Rolle admin hat, bekommt er Zugang auf den Admin Server.
In der Docker Compose Datei werden all die Server in der Services Sektion angelegt und den jeweiligen Netzwerken zugewiesen, in der darauf folgenden Networks Sektion definiert werden:
Das Public Netzwerk muss angelegt werden, anschließend kann Docker Compose gestartet werden:
docker network create public_network
docker-compose up
Test
Wie zuvor: Über die Logausgaben in dem Terminalfenster, in dem Docker Compose gestartet wurde, kann man gut nachvollziehen, welche Server aufgerufen werden.
Vor dem Login werden über Load Balancer und RPs die Seiten der beiden Public Server angezeigt.
Nach dem Login werden auch die Private und ShowHeaders Seiten angezeigt.
Außerdem werden auch die Seiten der User und Admin Server angezeigt. Das sollte nur erfolgen, wenn der eingeloggte Benutzer auch die entsprechenden Rollen hat, wird aber momentan noch nicht abgefragt. Die Umsetzung wird weiter unten beschrieben, sobald ich herausgefunden habe, wie sie zu implementieren ist.
Der Zugang zu den Seiten der User und Admin Server soll nur mit entsprechenden Rollen erfolgen.
Die Implementierung ist noch offen.
UPDATE: Inzwischen konnte ich mit einem Experten für OneLogin sprechen und wurde aufgeklärt, dass es seitens OneLogin gar nicht vorgesehen ist, dass die Anwendungs-Rollen in OneLogin gepflegt werden.
Folglich kann keine Authorisierung durch den RP mit OneLogin erfolgen.
Im letzten Post habe ich mir OneLogin angeschaut und zwei Javascript Beispiele zum laufen gebracht.
In diesem Post möchte ich einen Reverse Proxy aufbauen, der eine öffentlich zugängliche Seite bereit stellt und eine private Seite nur für eingeloggte Mitglieder.
OneLogin
Auf der OneLogin Applications Seite sammle ich folgende Informationen ein, die später in der ReverseProxy Konfiguration benötigt werden:
Client ID
Client Secret
Issuer URL
Außerdem wird der Token Endpoint auf Basic gesetzt.
In der Configuration muss eine Redirect URI eingetragen werden, in diesem Fall: http://localhost/private/redirect_uri
Reverse Proxy
Den Reverse Proxy wird mit Docker aufgebaut.
Der Reverse Proxy wird eine Startseite bereit stellen und von dort auf eine öffentlich zugängliche Unterseite und einen geschützten Bereich verlinken.
In der ersten Version wird der geschützte Bereich lediglich eine weitere Unterseite sein.
In der zweiten Version wird ein weiterer geschützter Bereich mit der ShowHeaders App hinzugefügt.
In Eclipse habe ich zuerst das neue GitHub-Repository hinzugefügt und ausgechecked, dann händisch .project angelegt und konnte dann in Eclipse über Import das Project hinzufügen. Fühlt sich viel zu umständlich an, aber ich muss das zum Glück nicht so oft machen, als dass ich dem jetzt weiter auf dem Grund gehen müsste, wie das besser geht.
Es wird ein Docker Image angelegt, das wiederum über Docker-Compose gestartet wird, um Dateien des Filesystems einzubinden. Das ist für die Entwicklung leichter, am Ende könnte man natürlich alles in ein Image packen und starten.
Das Docker Image basiert auf dem offiziellen Apache HTTPD Image. Es wird mod_auth_openidc hinzugefügt, sowie ca-certificates um eine verschlüsselte Verbindung per HTTPS zum OneLogin-Server aufbauen zu können. Die Datei für den mod_auth_openidc muss noch an die richtige Stelle verschoben werden und ein Backup der originalen httpd.conf angelegt werden.
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Im Docker-Compose wird das Image gebaut und die HTML Seiten sowie Konfigurationsdateien eingebunden.
Dabei wird die Datei reverseproxy_httpd.conf als httpd.conf eingebunden und über diese Datei wird die zuvor gesicherte, originale httpd.conf und anschließend die reverseproxy.conf geladen. Das ist eine einfache Möglichkeit, die ursprüngliche Konfiguration zu erhalten. Für ein produktives Setup ist das vermutlich nicht die beste Wahl.
Die Variable ${PWD} ist unter Linux verfügbar, daher starte ich den Container unter Windows WSL.
Die Datei reverseproxy_httpd.conf (bzw. httpd.conf im Container) ist simpel aufgebaut und enthält nur die Includes zur ursprünglichen httpd.conf und zu unserer reverseproxy.conf:
# load original configuration first
Include conf/container_httpd.conf
# customized configuration
ServerName reverseproxy
Include conf/reverseproxy.conf
Apache HTTPD Konfiguration
Die in der OneLogin Seite eingesammelten Werte müssen entsprechend in die Konfiguration eingetragen werden.
Geschützt wird der Bereich, der unter /private liegt.
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer
OIDCProviderMetadataURL https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
OIDCClientID geheim-a91c-013a-175a-02471d082b0b208817
OIDCClientSecret wirklich-ganz-geheim
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI http://localhost/private/redirect_uri
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
AuthType openid-connect
Require valid-user
Im laufenden Docker Container den Apache neu durchstarten:
apachectl -t && apachectl restart
Testen
In einem neuen Browserfenster, im Inkognito Modus die Seite öffnen: http://localhost.
Der Link Index Page führt auf diese Index-Seite, der Public Page Link auf die öffentlich zugängliche Seite und Private Page auf die Seite, die nur für OneLogin User zugänglich ist.
Die Public Page:
Die Private Page führt im ersten Schritt zum OneLogin Login:
Erst nach erfolgreichem Login sehen wir die private Seite:
Reverse Proxy - mit ShowHeaders
Der Reverse Proxy schreibt einige Informationen in den Header, diese werden aber nur dem Server gesendet, der Client (zB unser Webbrowser) sieht davon nichts. Um sehen zu können, welche Informationen übermittelt werden, verwende ich eine kleine App, die nichts anderes macht, als die Header anzuzeigen, daher auch der Name ShowHeaders.
Bisher existierte noch kein Dockerfile für ShowHeaders, daher habe ich das für diesen Test entwickelt und hinzugefügt:
FROM tomcat:8.5-jdk8-openjdk-slim
RUN apt update && apt install -y \
maven
RUN git clone https://github.com/DerIngo/ShowHeaders.git
WORKDIR ShowHeaders
RUN mvn package
WORKDIR $CATALINA_HOME
RUN mv ShowHeaders/target/ROOT.war webapps
EXPOSE 8080
CMD ["catalina.sh", "run"]
ShowHeaders wird in die Docker Konfiguration mit aufgenommen:
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer
OIDCProviderMetadataURL https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
OIDCClientID geheim-a91c-013a-175a-02471d082b0b208817
OIDCClientSecret wirklich-ganz-geheim
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI http://localhost/private/redirect_uri
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
AuthType openid-connect
Require valid-user
# showheaders block
ProxyPass /showheaders http://showheaders:8080/
ProxyPassReverse /showheaders http://showheaders:8080/
AuthType openid-connect
Require valid-user
Die Startseite wurde um einen Link zu ShowHeaders erweitert:
Die OIDCRemoteUserClaim-Konfiguration scheint keinen Einfluss zu haben:
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
Auf der ShowHeaders-Seite werden oidc_claim_email und oidc_claim_preferred_username angezeigt. Hingegen wird keine Header REMOTE_USER angezeigt.
Das Entfernen der OIDCRemoteUserClaim-Konfiguration hat auch keinen Einfluss auf die angezeigten Header.
Anscheinend macht diese Konfiguration nicht das, was ich erwartet hatte, daher entferne ich sie wieder. Weitere Recherchen dazu sind für diesen Test nicht notwendig, daher belasse ich es dabei.
Weitere Informationen zur Konfiguration des Mod Auth OpenIDC finden sich in der kommentierten Beispielkonfiguration auf GitHub.
GitHub
Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version1 und version2 zu finden.
In meinem letzten Blogeintrag habe ich eine geDockerte Anwendung auf einem Server mit Ubuntu 18 zum laufen gebracht. Aus verschiedenen Gründen war das aber nur ein Zwischenschritt, um zu testen, ob die Anwendung grundsätzlich in solch einer Umgebung lauffähig ist. Neben den beschriebenen Problemen gab es noch viele weitere, die gelöst werden mussten.
Als nächsten Schritt möchte ich die Anwendung in die AWS umziehen, immerhin bin ich ja inzwischen ein zertifizierter Cloud Practitioner.
AWS User
Mit dem Stammbenutzer einen neuen IAM Nutzer für die Anwendung anlegen. Dieser bekommt erstmal umfangreiche Rechte, was nicht best Practice ist und später sollte ich diese Rechte auf das unbedingt benötigte zurücksetzen.
EC2 Server
Die Anwendung soll erstmal mit dem Docker Setup auf einem EC2 Server laufen.
Mit dem neuen IAM Nutzer wechsele ich zuerst auf die Europa Zone ec-central-1.
Ich lege eine neue EC2 Server Instanz an, wobei ich als Sparfuchs nach "nur kostenloses Kontingent" filtere und ein AMI für Ubuntu Server 20.04 LTS (x64) und Typ t2.micro auswähle. Es wird ein neues Schlüsselpaar erzeugt und ich speichere den privaten Schlüssel.
Über EC2 > Instances > Server-Instanz auswählen.
Über Verbinden lässt sich im Browser ein Terminal öffnen. Hier lässt sich aber auch am einfachsten die öffentliche IP und vor allem der Benutzername finden:
Ich habe allerdings nicht die Web Shell verwendet, sondern die Daten, sowie den privaten Schlüssel genommen, um eine Verbindung in WinSCP einzurichten. So kann ich später leicht die Daten auf den Server kopieren und per Klick eine PuTTY-Shell öffnen.
Port Freigabe
Standardmäßig ist für den Server nur Port 22 für SSH frei gegeben.
Weitere Ports, wie zB der benötigte HTTP Port 80 oder HTTPS 443, lassen sich über die AWS Management Console frei geben.
Die EC2-Server-Instanz auswählen und unter Sicherheit findet sich die Sicherheitsgruppe:
In der Sicherheitsgruppe können die Regeln für den eingehenden Datenverkehr erweitert werden. Dabei ist zu beachten, dass man weitere Regeln hinzufügen muss und nicht den bestehenden Typ SSH auf zB HTTP ändert und speichert, weil das diesen nur ändert und nicht als neue, weitere Regel hinzufügt. Dann kann man zwar die Seiten des Webservers bewundern, aber sich nicht mehr per SSH einloggen.
Server einrichten
Auf der Linux Konsole des EC2-Servers wird dieser eingerichtet, dazu wird Docker Compose installiert, was als Abhängigkeit Docker mitbringt.
apt list --upgradable
sudo apt update
sudo apt upgrade -y
sudo apt install docker-compose -y
sudo docker version # -> 20.10.7
sudo docker-compose version # -> 1.25.0
sudo service docker status # -> running
sudo docker run hello-world
Docker läuft und es werden die Daten der Anwendung auf den Server kopiert und anschließend über Docker Compose gestartet.
sudo docker-compose up
Leider führte das zu einem Fehler, wie er schon bei der Ubuntu 18 Installation aufgetreten ist. Das zuvor gewonnene Wissen kann ich jetzt zur schnellen Fehlerbehebung anwenden:
sudo apt-get remove docker-compose -y
sudo curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
# Output:
-bash: /usr/bin/docker-compose: No such file or directory
# Lösung: neue Shell, zb per tmux, starten
# und dann nochmals testen
docker-compose --version
# Output:
Docker Compose version v2.2.3
Anschließend ließ sich die Anwendung per Docker Compose starten und per cURL, bzw. HTTPie, über localhost:80 und <öffentlicheIP>:80 aufrufen. Der Aufruf <öffentlicheIP>:80 vom Entwickler Laptop funktioniert auch.
Der Start dauerte etwas länger, die Webanwendung selbst ließ sich anschließend aber angenehm schnell bedienen. Zumindest als Test-Server scheint der "Gratis"-EC2-Server völlig auszureichen.
Ausblick
Auf dem kostenfreien Server laufen ein Tomcat Webserver, eine PostgreSQL Datenbank und PGAdmin und das, zumindest den ersten Tests nach, mit völlig ausreichender Performance.
Als nächstes möchte ich dem Docker Compose Konstrukt noch um einen Reverse Proxy erweitern, der eine (vermutlich nur selbstsignierte) verschlüsselte Verbindung per HTTPS anbietet und über Port 80 und 443 die Anwendung und den PGAdmin erreichbar macht. Außerdem soll es einen einfachen Authentifizierungs- und ggf. Authorisierungsmechanismus geben. Das wird mit einem Apache HTTP Server realisiert werden und sollte keinen besonderen Ressourcenbedarf haben.
Falls sich die Zeit findet, möchte ich das um Keycloak erweitern und den Zugriff auf Anwendung und PGAdmin erst nach erfolgreicher Authentifizierung und Authorisierung erlauben. Vielleicht ist das noch mit dem Apache HTTP Server realisierbar, ggf. werde ich aber auf zB Traefik umstellen. Bei dem Setup kann ich mir schon vorstellen, dass die Ressourcen des kleinen Server nicht mehr ausreichen und es zu spürbaren Performanceeinbrüchen kommen wird.
Eine ansprechendere URL, anstelle der generierten AWS URL, wäre wünschenswert.
Docker Compose Datei vom Entwicklungsrechner auf den Server kopieren, kleinere Anpassungen vornehmen und ausführen. So einfach habe ich es mir vorgestellt, aber es gab dann leider doch noch Herausforderungen zu bewältigen:
Docker Compose Updaten
Ich habe einen Server mit dem nicht mehr ganz taufrischen Ubuntu 18 am laufen und wollte dort ein Docker Compose Skript ausführen.
Das Skript läuft auf meinem Entwicklungsrechner, aber auf dem Server wurde lediglich eine Fehlermeldung ausgegeben:
dockeruser@myServer:~/myproject$ docker-compose up
ERROR: Version in "./docker-compose.yml" is unsupported. You might be seeing this error because you're using the wrong Compose file version. Either specify a supported version (e.g "2.2" or "3.3") and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1.
For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/
Wie sich herausstellte, war für Ubuntu 18 bei Docker Compose 1.17.1 Schluss und ich muss händisch upgraden:
Docker Compose entfernen:
sudo apt-get remove docker-compose
Die aktuelle Docker Compose Version ermitteln (heute: 2.2.3): https://github.com/docker/compose/releases
Auf dieser Seite kann man auch den direkten Link zum Download finden, falls es beim ausführen des nächsten Befehls zu Problemen kommt.
Beispielsweise ist die Versionsnummer v2.2.3, also mit einem kleinen "v" am Anfang und wenn das fehlt, schlägt der Download fehl.
So lautet der Link für mein Ubuntu: https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64
Looks like this is because it defaults to use the secretservice executable which seems to have some sort of X11 dependency for some reason. If you install and configure pass docker will use that instead which seems to solve the problem.
# substitute with the latest version
url=https://github.com/docker/docker-credential-helpers/releases/download/v0.6.4/docker-credential-pass-v0.6.4-amd64.tar.gz
# download and untar the binary
wget $url
tar -xzvf $(basename $url)
# move the binary to a dir in your $PATH
sudo mv docker-credential-pass /usr/local/bin
# verify it works
docker-credential-pass list
# cleanup
rm docker-credential-pass-v0.6.4-amd64.tar.gz
gpg: agent_genkey failed: Keine Berechtigung Schlüsselerzeugung fehlgeschlagen: Keine Berechtigung
Und eine Erklärung mit Lösungsvorschlag findet sich hier:
Expected behavior. Here's why. At the point of failure, gen-key is about to ask the user for a passphrase. For security purposes, rather than using stdin/stdout, it wants to directly open the controlling terminal for the session and use that handle to write the prompt and receive the passphrase. When you use su to switch to some other user, the owner of the controlling terminal device file does not change; it remains associated with the user who actually logged in (i.e. received a real terminal from getty or got a pty from telnet or ssh or whatever). That device file is protected mode 600, so it can't be opened by anyone else.
The solution is to sudo-chown the device file to the user-who-needs-to-gen-the-key before su'ing to that user. Create the key within the su'd environment, then exit back to the original environment. Then, finally, sudo-chown the terminal back to yourself.
Glücklicherweise geht es auch einfacher, indem man einfach das Programm tmux verwendet. 🙂
tmux
# create a gpg2 key
gpg2 --gen-key
# list key information
gpg2 -k
# Copy the key id (from the line labelled [uid]) and do
pass init "whatever key id you have"
Jetzt sollte der Docker Login funktionieren, aber:
Auch wieder kein neues Problem, dass zB bereits hier und hier diskutiert wurde.
mkdir ~/.docker
touch ~/.docker/config.json
# brachte jeweils keine Änderung
/usr/local/bin/docker-credential-pass
# Output:
-bash: /usr/local/bin/docker-credential-pass: Keine Berechtigung
# Erfolg kam mit diesem Befehl:
sudo chmod +x /usr/local/bin/docker-credential-pass
#Zumindest funktioniert dieser Aufruf:
docker-credential-pass list
# Ein weiterer Fehler ließ sich beheben durch:
export GPG_TTY=$(tty)
Ich musste die einzelnen Images per docker pull imagename ziehen, erst danach konnte ich docker-compose ausführen.
Im vorletzten Post: PostgreSQL hatte ich beschrieben, wie ich zwei Bestandsdatenbanken analysiert und in eine PostgreSQL-DB in einem Docker Container gebracht habe, samt PGAdmin.
Jetzt möchte ich einen Schritt weiter gehen und das komplette Setup über ein Script starten können: DB, PGAdmin & SQL-Scripte. Dazu verwende ich Docker-Compose.
Ausgangslage
In PostgreSQL hatte ich bereits die zugrundeliegenden Docker-Images ermittelt: postgres:13.4-buster für die DB und dpage/pgadmin4 für PGAdmin. Inzwischen gibt es aber ein aktuelleres Image für die DB, das ich verwenden werde: postgres:13.5-bullseye
Für die SQL-Daten werde ich auf den Artikel PostgreSQL IDs zurückgreifen und daraus zwei Scripte machen, eines für das Schema der DB und eines mit den "Masterdaten" mit denen das Schema initial befüllt werden soll.
CREATE SEQUENCE object_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE person (
object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
vorname varchar(255),
nachname varchar(255),
CONSTRAINT person_pkey PRIMARY KEY (object_id)
);
CREATE TABLE adresse (
object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
strasse varchar(255),
ort varchar(255),
CONSTRAINT adresse_pkey PRIMARY KEY (object_id)
);
INSERT INTO person (vorname, nachname) VALUES ('Max', 'Mustermann');
INSERT INTO person (vorname, nachname) VALUES ('Peter', 'Person');
INSERT INTO person (vorname, nachname) VALUES ('Donald', 'Demo');
INSERT INTO adresse (strasse, ort) VALUES ('Beispielstrasse', 'Beispielstadt');
INSERT INTO adresse (strasse, ort) VALUES ('Erpelweg', 'Entenhausen');
INSERT INTO adresse (strasse, ort) VALUES ('Bruchstrasse', 'Berlin');
Auf der Seite von Docker Compose bringe ich in Erfahrung, dass die aktuelle Version von Docker Compose 3.9 ist und erstelle schon mal die Datei:
version: "3.9" # optional since v1.27.0
Ordernstrucktur:
Images starten
Bisher habe ich PostgreSQL und PGAdmin über folgende Kommandos gestartet:
docker run --name myapp-db -p 5432:5432 -e POSTGRES_PASSWORD=PASSWORD -d postgres:13.4-buster
Jetzt können beide Images mit einem einfachen Befehl gestartet werden:
\myapp> docker-compose up
Creating network "myapp_default" with the default driver
Creating myapp_myapp-db_1 ... done
Creating myapp_myapp-pgadmin_1 ... done
Attaching to myapp_myapp-db_1, myapp_myapp-pgadmin_1
PGAdmin im Browser starten: http://localhost:80 Login wie bisher mit admin@admin.com / admin
Im nächsten Schritt gibt es bereits eine entscheidende Änderung: Während bisher beide Container isoliert nebeneinander liefen und nur über den Host-Rechner kommunizieren konnten, wurde durch Docker-Compose automatisch beim Start ein Netzwerk ("myapp_default") angelegt, in dem beide Container laufen. Außerdem sind beide Container über ihren Servicenamen ("myapp-db" & "myapp-pgadmin") erreichbar.
Dadurch muss nicht mehr die IP des Host-Rechners ermittelt werden (die sich manchmal ändert), sondern es kann der Name genommen werden:
Datenbank erstellen
In der PostgreSQL Instanz muss jetzt eine Datenbank erzeugt werden, in der die Anwendungsdaten gespeichert werden.
Hierzu gehen wir in den DB Container. Allerdings ist der Name anders als bisher: Es wurde der Verzeichnisname als Präfix davor und eine 1 (für die 1. und in unserem Fall einzige Instanz) als Postfix dahinter gehangen und so lautet der Name : myapp_myapp-db_1
docker exec -it myapp_myapp-db_1 bash
Im Container erzeugen wir die DB:
su postgres
createdb myappdb
exit
So war es zumindest bisher, einfacher geht es mit Docker-Compose und dem Setzten der Environment-Variablen POSTGRES_DB wodurch die DB automatisch angelegt und verwendet wird. Sicherlich hätte ich das auch bisher im Docker Kommando so nehmen können, aber im letzten Post hatte ich es zum einen mit zwei DBs zu tun und zum anderen musste ich eh auf die Kommandozeile um die DBs einzuspielen.
Einfacher geht es über Docker-Compose und den Mechanismus, dass PostgreSQL automatisch die Dateien importiert, die im Verzeichnis /docker-entrypoint-initdb.d/ liegen. Und zwar in alphabetischer Reihenfolge.
Während der Entwicklung öfters mal die DB platt macht und komplett neu aufsetzt: Mit Docker ist das schnell gemacht. Mit Docker-Compose sind es jetzt nur noch zwei Befehle:
# stop and remove stopped containers
docker-compose down
# start containers
docker-compose up
Einen Nachteil gibt es allerdings: Die Einstellungen im PGAdmin gehen ebenfalls flöten und müssen neu eingegeben werden. Die Lösung: Der PGAdmin Container bekommt ein persistentes Volume, das ein docker-compose down übersteht. Und wenn es doch mal neu aufgesetzt werden muss, ist das einfach über das -v Flag umsetzbar: docker-compose down -v
An Schema und Stammdaten wird sich erstmal nichts ändern. Daher wäre es gut, wenn beim Neubau der Container die DB bereits mit Schema und Stammdaten gestartet wird und nicht diese erst aufbauen muss.
Das wird dadurch erreicht, dass ein Image gebaut wird, dass die PostgreSQL DB sowie Schema und Stammdaten enthält.
Im Verzeichnis database wird eine Datei Dockerfile angelegt. Dieses Dockerfile enthält die Informationen zum Bau des DB Images.
Sollte es erforderlich sein, das Image neu zu bauen, zB wenn ich das Schema verändert hat:
docker-compose up --build
# or
docker-compose build
UPDATE: Das war leider nix mit dem vorgefüllten Image
Das Dockerfile enthält zwar den Schritt die SQL Dateien in das Image zu kopieren. Es fehlt aber der Schritt, bei dem diese Dateien in die Datenbank hineinmigriert werden. Das geschieht wie zuvor auch erst beim Starten des Containers. Mein Ziel, einen Image zu haben, dass diese Daten bereits enthält, habe ich damit also leider nicht erreicht. Das Dockerfile enthält keine Informationen, die nicht zuvor auch schon im Docker Compose enthalten waren.
Da mich das herumkaspern mit diesem Problem heute den ganzen Tag gekostet und abgesehen vom Erkenntnisgewinn leider nichts gebracht hat, kehre ich zur Docker Compose Variante zurück, bei der die DB beim Starten des Containers gebaut wird. Für das Beispiel auf dieser Seite macht das praktisch gesehen keinen Unterschied, da die Scripte winzig sind. Für mein Projekt leider schon, da benötigt der Aufbau der DB ein paar Minuten. Für die Anwendungsentwicklung, bei der ich die DB alle paar Tage mal neu aufsetze, ist das durchaus OK. Für Tests, die mit einer frischen DB starten sollen, die am Ende weggeschmissen wird, ist das schon ein Problem.
Also Rückkehr zu Docker Compose, diesmal mit einem extra Volume für die DB. Das kann ich dann gezielt löschen.
# Create and start containers
docker-compose up
# Image neu bauen
docker-compose up --build
# or
docker-compose build
# Stop containers
docker-compose stop
# Start containers
docker-compose start
# Stop containers and remove them
docker-compose down
# Stop containers, remove them and remove volumes
docker-compose down -v
Für die Neu- und Weiterentwicklung einer Anwendung habe ich zur Analyse die Bestandsanwendung samt Datenbanken bekommen. Für die Analyse musste ich zunächst die Datenbanken zum laufen bekommen und uA mit einem DB-Client einsehen.
Ich habe zum einen eine Datei dump.backup bekommen. Zunächst musste ich herausfinden, um was für eine Datei es sich dabei handelt, dazu nutzte ich das Linux Tool file:
Es handelt sich also um einen Dump einer PostgreSQL Datenbank. Und im Dump konnte ich eine Versionsnummer 13.0 finden.
Die andere Datei mydb.sql.gz beinhaltet eine gezippte Version eines SQL Exports einer PostgreSQL DB Version 13.2 von einem Debian 13.2 Server.
Im Laufe der weiteren Analyse der DB Exporte stellte sich heraus, dass der dump.backup die PostGIS Erweiterung der PostgreSQL DB benötigt, welche mit installiert werden muss.
PostgreSQL Datenbank Docker Image
Ich werde beide Datenbanken in einer Docker Version installieren, dazu werde ich eine DB Instanz starten, in der beide DBs installiert werden.
Da es sich um die Versionsnummer 13.0 und 13.2 handelt, werde ich ein aktuelles Image von Version 13 verwenden, was zum Projektzeitpunkt Version 13.4 war.
Da zumindest eine der beiden DBs auf einem Debian System gehostet ist, werde ich ein Debian Image wählen.
Die Wahl des Images fällt also auf: postgres:13.4-buster
Datenbank installieren
Zuerst das Docker Image ziehen:
docker pull postgres:13.4-buster
Datenbank starten:
docker run --name myapp-db -p 5432:5432 -e POSTGRES_PASSWORD=PASSWORD -d postgres:13.4-buster
DB-Dateien in den laufenden Docker Container kopieren:
su postgres
createdb mydb_dump
pg_restore -d mydb_dump -v /tmp/dump.backup
exit
DB von SQL erstellen:
su postgres
cd /tmp
gunzip mydb.sql.gz
createdb mydb_sql
pg_restore -d mydb_sql -v /tmp/mydb.sql
exit
Die Datenbanken sind installiert und es kann mittels eines letzten exit der Container verlassen werden.
DB Client PGAdmin installieren
Um in die Datenbanken hinein sehen zu können wird ein Client Programm benötigt. Sicherlich es gibt da bereits etwas auf der CommandLine Ebene:
su postgres
psql mydb_sql
Übersichtlich oder komfortabel ist das aber nicht. Daher möchte ich ein Tool mit einer grafischen Oberfläche verwenden. Die Wahl fiel auf pgAdmin, welches sich leicht von einem Docker Image installieren und anschließend über den Browser bedienen lässt.
Für die Konfiguration für die Verbindung zur zuvor gestarteten PostgreSQL Datenbank benötige ich die IP meines Rechners, die ich mittels ipconfig herausfinden kann.