Categories
Development

Tomcat Redis

Nachdem ich mich vor ca. zwei Jahren mal mit Redis auseinander gesetzt hatte, hat es sich jetzt ergeben, ein Beispielsetup in der Praxis umzusetzen.

Eine Anwendung, die auf einem Tomcat Server läuft, soll vorbereitet werden skalierbar gemacht zu werden. Dazu wird im ersten Schritt Redis als Session Cache für den Tomcat eingebunden und ein zweiter Tomcat daneben gestellt, der ebenfalls auf den Redis Session Cache zugreift. Zur Lastverteilung wird ein Reverse Proxy vor die beiden Tomcats gestellt.

Die Server laufen alle in Docker Containern und werden über eine Docker-Compose Datei gesteuert.

Als Beispielanwendung für dieses Projekt kommt mal wieder Show Headers zum Einsatz.

Die Sourcen lege ich in ein GitHub-Repository.

Basis Setup

Basis Setup von einem Tomcat 9 und Show Headers, daneben ein Redis Server.

Das Show Headers ROOT.war liegt im tomcat Ordner.

Das docker-compose File:

version: "3.8"
services:
  tomcat:
    image: tomcat:9.0.83-jre21
    hostname: tomcat
    ports: 
      - "8888:8080"
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war

  redis:
    image: redis
    volumes:
      - redisvolume:/data

volumes:
  redisvolume: {}

networks:
  default:
    name: tomcatredis-network

Browser Output:

Tomcat einrichten

Für die Verbindung von Tomcat zu Redis wird Redisson verwendet.

Step 1

context.xml von einem Tomcat 9 in das tomcat Verzeichnis kopieren und den RedissonSessionManager einrichten:

<Context>
  
    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Redis Session Manager -->
    <!-- https://redisson.org/articles/redis-based-tomcat-session-management.html -->
    <Manager className="org.redisson.tomcat.RedissonSessionManager" 
            configPath="${catalina.base}/conf/redisson.yaml" 
            readMode="MEMORY" 
            updateMode="DEFAULT"/>

</Context>

Konfigurationsdatei anlegen:

singleServerConfig:
  address: "redis://${REDIS_HOST:-redis}:${REDIS_PORT:-6379}"

Step 2

Die beiden Redisson Dateien von Redisson herunterladen und ebenfalls in das tomcat Verzeichnis kopieren.

Step 3

Es muss ein neues Tomcat Image inklusive Redisson gebaut werden, dazu ein neues Dockerfile im tomcat Ordner anlegen:

# https://hub.docker.com/_/tomcat
FROM tomcat:9.0.83-jre21

# Add Redis session manager dependencies
COPY ./redisson-all-3.22.0.jar $CATALINA_HOME/lib/
COPY ./redisson-tomcat-9-3.22.0.jar $CATALINA_HOME/lib/

# Replace the default Tomcat context.xml with custom context.xml
COPY ./context.xml $CATALINA_HOME/conf/

# Add Redisson configuration
COPY ./redisson.yaml $CATALINA_HOME/conf/

# Expose the port Tomcat will run on
EXPOSE 8080

# Start Tomcat
CMD ["catalina.sh", "run"]

Anstelle des image Eintrags in der docker-compose den build Eintrag setzen: "build: ./tomcat"

Testen

Erneut starten:

docker-compose up --detach

Und es läuft immer noch im Browser:

Redis

So weit so gut, aber wird auch wirklich der Redis Cache verwendet?
Nein, denn bisher wurde noch gar keine Session erzeugt.
Holen wir das nach, indem wir ShowSession aufrufen:

Schauen wir in der Redis Datenbank nach, indem wir uns zuerst in den Container connecten:

docker exec -it tomcatredissample-redis-1 bash

Dort die redis-cli starten und die Keys aller Einträge zeigen lassen mittels "keys *":

Dort ist ein Eintrag mit der Session ID aus meinem Browser zu finden.
Es funktioniert!

Welche Daten stehen in der Session?
Um die Daten auslesen zu können, müssen wir erst den Datentyp mittels "TYPE" herausfinden, in diesem Fall ein "hash" und dann mit "HGETALL" anzeigen lassen:

Die seltsamen oder unlesbaren Informationen, die man sieht, wie z.B. "\t\xa6\xfa\xbd\xbe\x83c" für "session:thisAccessedTime", sind wahrscheinlich auf die Art und Weise zurückzuführen, wie Sitzungsdaten serialisiert werden, bevor sie in Redis gespeichert werden. Viele auf Java basierende Systeme, einschließlich solcher, die Tomcat für die Sitzungsverwaltung verwenden, serialisieren Objekte in ein binäres Format, bevor sie in einem Sitzungsspeicher wie Redis gespeichert werden. Diese binären Daten sind nicht direkt lesbar, wenn Sie sie mit Redis-Befehlen abrufen.

Um diese Daten zu interpretieren, müssen sie in ein lesbares Format deserialisiert werden.
Darauf gehe ich hier aber nicht weiter ein.

Reverse Proxy

Der Reverse Proxy basiert auf Apache HTTPD 2.4 und wird der docker-compose Datei hinzugefügt.

Die httpd.conf Datei aus dem Container wird in den reverseproxy Ordner kopiert und am Ende erweitert:

[...]

LoadModule proxy_module modules/mod_proxy.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


ServerName reverseproxy

<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"

    ## Tomcat
    ProxyPass        /          http://tomcat:8080/
    ProxyPassReverse /          http://tomcat:8080/

</VirtualHost>

Die Docker Compose Datei:

version: "3.8"
services:
  reverseproxy:
    image: httpd:2.4
    ports: 
      - "8888:80"
    volumes:
      - ./reverseproxy/httpd.conf:/usr/local/apache2/conf/httpd.conf

  tomcat:
    build: ./tomcat
    hostname: tomcat
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war

  redis:
    image: redis
    volumes:
      - redisvolume:/data

volumes:
  redisvolume: {}

networks:
  default:
    name: tomcatredis-network

Der anschließende Aufruf von http://localhost:8888/ShowSession funktioniert immer noch, Test bestanden.

Load Balancer

Im nächsten Schritt fügen wir einen Load Balancer hinzu, der erstmal auf genau den einen Tomcat "loadbalanced". Nach erfolgreichem Test wissen wir dann, dass der Load Balancer generell funktioniert und können dann weitere Server hinzufügen.
Die erweiterte Apache Konfiguration:

[...]
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.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 slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule watchdog_module modules/mod_watchdog.so


ServerName reverseproxy

<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"

    <Proxy "balancer://tomcat">
        BalancerMember http://tomcat:8080
    </Proxy>

    ProxyPass        /          balancer://tomcat/
    ProxyPassReverse /          balancer://tomcat/

</VirtualHost>

Mehr Tomcat Server

Die einfachste Möglichkeit, mehrere Tomcat Server zu erzeugen, ist im Docker Compose weitere Replicas zu starten.

Docker Compose managed dann auch das Load Balancing, so dass alle Tomcat Instanzen über den Service Namen "tomcat" ansprechbar sind.

Wir haben damit ein doppeltes Load Balancing: Zuerst der Apache HTTPD der immer auf den "tomcat" loadbalanced und dann das wirkliche Load Balancing durch Docker auf die Replikas.

services:
  [...]
  tomcat:
    build: ./tomcat
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
    deploy:
      mode: replicated
      replicas: 4
  [...]

Genau zwei Server

Jetzt die Variante ohne Replikas und mit zwei dedizierten Tomcat Servern.
Die Zuteilung zum Server erfolgt beim Sessionaufbau sticky, aber wir können über Manipulation des Session Cookies den Server wechseln und so gezielt ansteuern.

In Docker Compose legen wir zwei Tomcat Server an:

services:
  [...]
  tomcat-1:
    build: ./tomcat
    hostname: tomcat-1
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
      - ./tomcat/server-1.xml:/usr/local/tomcat/conf/server.xml

  tomcat-2:
    build: ./tomcat
    hostname: tomcat-2
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
      - ./tomcat/server-2.xml:/usr/local/tomcat/conf/server.xml
  [...]

Die server.xml ist eine Kopie der Tomcat 9 server.xml und lediglich an einer Stelle angepasst, für das Setzen der jeweiligen jvmRoute:

<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-1">

In der Apache Konfiguration werden die beiden Server im Load Balancer eingetragen:

    <Proxy "balancer://tomcat">
        BalancerMember http://tomcat-1:8080 route=tomcat-1
        BalancerMember http://tomcat-2:8080 route=tomcat-2
    </Proxy>

Ggf. Session Cookies im Browser löschen, dann http://localhost:8888/ShowServer bzw. http://localhost:8888/ShowHeaders aufrufen. Man kann erkennen, dass bei jedem Aufruf der Server gewechselt wird.

Beim erstmaligen Aufruf von http://localhost:8888/ShowSession wird die Session erzeugt und man wird einem Server zugewiesen.

Man kann sehen, dass die Session ID ein Postfix ".tomcat-1" bzw. ".tomcat-2" hat.

Man kann im Browser den Session Cookie editieren und den Postfix auf den anderen Server ändern, zb von "SESSIONID.tomcat-1" auf "SESSIONID.tomcat-2". Dadurch kann man dann den Server auswählen, auf den man gelangen möchte. Eigentlich zumindest, denn leider hat es nicht funktioniert.

Entweder muss noch irgendwo irgendwas konfiguriert werden, oder es könnte auch ein Bug in Redisson sein: Der Postfix wird als Teil der Session ID durch Redisson in Redis als Key gespeichert.
Wenn man nun also lediglich den Postfix verändert, hat man eine ungültige Session ID und es wird eine neue Session generiert. Und so kann es irgendwie passieren, dass man wieder auf dem ursprünglichen Server landet, mit einer neuen Session.
Es könnte auch am Reverse Proxy liegen, dass dort der Postfix abgeschnitten werden muss, bei der Kommunikation RP zu Tomcat und lediglich auf der Strecke RP zum Browser gesetzt werden muss.

Vielleicht werde ich die Ursache des Problems und deren Behebung ermitteln können, dann gibt es hier ein Update. Allerdings werde ich nicht allzuviel Energie hineinstecken können, da andere Sachen wichtiger sind, zumal die Lösung mit den Replikas und dem durch Docker bereitgestellten Load Balancing durchaus ausreichend sein sollten.

Categories
Development

Mutual SSL

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

Server_1

Die Verifizierung des Clients wird aktiviert.

SSLVerifyClient require
SSLCACertificateFile /usr/local/apache2/conf/server.crt

Server_2

Die Verifizierung des Clients wird nicht aktiviert.

Docker

Es wird Load Balancer, Server 1 und Server 2 definiert.

Für beide Server wird ein Port nach Außen zum Testen aufgemacht.

version: '3.8'
services:

  loadbalancer:
    build: ./loadbalancer
    hostname: loadbalancer
    volumes:
      - ${PWD}/loadbalancer/conf/loadbalancer_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/loadbalancer/conf/loadbalancer.conf:/usr/local/apache2/conf/loadbalancer.conf
      - ${PWD}/loadbalancer/conf/loadbalancer-ssl.conf:/usr/local/apache2/conf/loadbalancer-ssl.conf
      - ${PWD}/certs/loadbalancer.crt:/usr/local/apache2/conf/server.crt
      - ${PWD}/certs/loadbalancer-key.pem:/usr/local/apache2/conf/server.key
      - ${PWD}/certs/loadbalancer-combined.key:/usr/local/apache2/conf/combined.key
    ports:
      - 80:80
      - 443:443

  server_1: 
    build: ./server_1
    hostname: server_1
    volumes:
      - ./server_1/public_html:/usr/local/apache2/htdocs
      - ${PWD}/server_1/conf/server_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/server_1/conf/server.conf:/usr/local/apache2/conf/server.conf
      - ${PWD}/server_1/conf/server-ssl.conf:/usr/local/apache2/conf/server-ssl.conf
      - ${PWD}/certs/server_1.crt:/usr/local/apache2/conf/server.crt
      - ${PWD}/certs/server_1-key.pem:/usr/local/apache2/conf/server.key
    ports:
      - 8091:443

  server_2: 
    build: ./server_2
    hostname: server_2
    volumes:
      - ./server_2/public_html:/usr/local/apache2/htdocs
      - ${PWD}/server_2/conf/server_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/server_2/conf/server.conf:/usr/local/apache2/conf/server.conf
      - ${PWD}/server_2/conf/server-ssl.conf:/usr/local/apache2/conf/server-ssl.conf
      - ${PWD}/certs/server_2.crt:/usr/local/apache2/conf/server.crt
      - ${PWD}/certs/server_2-key.pem:/usr/local/apache2/conf/server.key
    ports:
      - 8092:443

Test

https://localhost

Man landet abwechselnd auf beiden Servern:

https://localhost:8091/

Ein direkter Zugriff auf Server_1 wird nicht gestattet, da der Client (unser Browser) nicht verifiziert werden konnte.

https://localhost:8092/

Ein direkter Zugriff auf Server_2 ist möglich, da wir auf diesem Server keine Verifizierung aktiviert haben.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version8 zu finden.

Categories
Development

SSL für Apache HTTP

Der letzte Post endete mit einer Fehlermeldung:

Analyse

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://<server-ip>/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"]

Gestartet wird mit Docker Compose:

version: '3.8'
services:

  reverseproxy:
    build: ./reverseproxy
    hostname: reverseproxy
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
      - ${PWD}/reverseproxy/conf/reverseproxy-ssl.conf:/usr/local/apache2/conf/reverseproxy-ssl.conf
    ports:
      - 80:80
      - 443:443

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:

<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"
    ServerName localhost
</VirtualHost>

Die Konfiguration für https wird später in reverseproxy-ssl.conf erfolgen, die Datei ist im ersten Schritt leer.

Starten mittels Docker Compose:

docker-compose up

Die Seite ist sowohl über localhost, als auch über IP erreichbar:

Schritt 2: Port 443

Der Server soll über den https-Port 443 die Seiten ausliefern.

Dazu wird die http/Port 80-Konfiguration angepasst kopiert.

Wichtig ist auch das "Listen 443", das am Anfang hinzugefügt werden muss.

Listen 443
<VirtualHost *:443>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"
    ServerName localhost
</VirtualHost>

Server neu durchstarten:

docker exec -it selfsignedtest_reverseproxy_1 bash
apachectl configtest && apachectl restart

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.

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

ServerName localhost:443
ServerAdmin deringo@github.com

SSLEngine on

SSLCertificateFile "/usr/local/apache2/conf/server.crt"
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"

Ein Neustart des Servers schlägt jetzt fehl, da die CertificateFiles noch nicht existieren.

Certificate Files erzeugen lassen:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /usr/local/apache2/conf/server.key -out /usr/local/apache2/conf/server.crt

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

<VirtualHost localhost:443>
    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 <INSERT-HERE a-random-secret>

    OIDCProviderMetadataURL <INSERT-HERE>

    OIDCClientID <INSERT-HERE>
    OIDCClientSecret <INSERT-HERE>
    # 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"


    <Location /private/>
        AuthType openid-connect
        Require valid-user
    </Location>

</VirtualHost>                                  

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
##

<VirtualHost _default_:443>

#   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"

<FilesMatch "\.(cgi|shtml|phtml|php)$">
    SSLOptions +StdEnvVars
</FilesMatch>
<Directory "/usr/local/apache2/cgi-bin">
    SSLOptions +StdEnvVars
</Directory>

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 <INSERT-HERE a-random-secret>

    OIDCProviderMetadataURL <INSERT-HERE>

    OIDCClientID <INSERT-HERE>
    OIDCClientSecret <INSERT-HERE>
    # 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"


    <Location /private/>
        AuthType openid-connect
        Require valid-user
    </Location>

</VirtualHost>

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
<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"
    ServerName localhost
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %%{HTTPS} off
        RewriteRule (.*) https://%%{HTTP_HOST}%%{REQUEST_URI} [R=301,L]
    </IfModule>
</VirtualHost>

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:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /usr/local/apache2/conf/server.key -out /usr/local/apache2/conf/server.crt -subj "/"

Der Befehl wird in das Dockerfile hinzugefügt.

Schritt 7: Trusted Certs

Aus den Erkenntnissen des letzten Posts folgt, dass die Möglichkeit vorgesehen werden muss, eine vertrauenswürdige Zertifikatskette einzubinden.

Dazu müssen die *.crt-Dateien in das conf-Verzeichnis kopiert 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
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /usr/local/apache2/conf/server.key -out /usr/local/apache2/conf/server.crt -subj "/"
COPY ./conf/*.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
CMD ["httpd-foreground"]

Schritt 8: ShowHeaders

Ich hatte ShowHeaders zwischendurch vermisst, so kommt es am Ende auch wieder mit rein.

Auch das ShowHeaders Image wird für Trusted Certs erweitert.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version7 zu finden.

Categories
Development Java

OneLogin AccessFilter

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 Header sieht das dann beispielsweise so aus:

oidc_claim_params
	{"post_logout_redirect_uri": "http://localhost", "roles": "user;admin"}

ACHTUNG

Das die Rollen als Parameter übertragen werden, funktioniert nur, wenn man explizit angibt, dass diese als Parameter übergeben werden sollen!

Dazu muss die Application in OneLogin entsprechend konfiguriert werden:

Applications -> Applications -> select Application -> Parameters -> "+"-Button:

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.

package deringo.oneloginjavaappsample;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;


@WebFilter("/*")
public class AccessFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		System.out.println("AccessFilter init");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("AccessFilter doFilter");
		
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		
		String oidc_claim_email = httpRequest.getHeader("oidc_claim_email");
		String oidc_claim_preferred_username = httpRequest.getHeader("oidc_claim_preferred_username");
		String userid = oidc_claim_email;
		
		String oidc_claim_params = httpRequest.getHeader("oidc_claim_params");
		List<String> roles = getRoles(oidc_claim_params);
		boolean hasRequiredRole = roles.contains("user");
		
		if (!StringUtils.isBlank(userid) && hasRequiredRole) {
			// Grant access
			chain.doFilter(request, response);
		} else {
			response.getWriter().println("Access denied");
			response.getWriter().close();
		}
	}

	@Override
	public void destroy() {
		System.out.println("AccessFilter destroy");
	}

	private static List<String> getRoles(String oidc_claim_params) {
		List<String> rolesList = new ArrayList<>();
		try {
			JSONObject o = new JSONObject(oidc_claim_params);
			String rolesS = o.get("roles").toString();
			String[] roles = StringUtils.split(rolesS, ";");
			rolesList = Arrays.asList(roles);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return rolesList;
	}
}

Docker

Über Docker Compose werden beide Server gestartet.

Über Port 80 ist der Reverse Proxy erreichbar, über Port 8080 direkt der Anwendungsserver.

version: '3.8'
services:

   reverseproxy:
      build: ./reverseproxy
      hostname: reverseproxy
      volumes:
        - ./reverseproxy/public_html:/usr/local/apache2/htdocs
        - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
        - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
      ports:
        - 80:80
      
   sampleapp:
      build: ./oneloginjavaappsample
      hostname: sampleapp
      ports:
        - 8080:8080

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:

http://localhost:8080/index.html

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version6 zu finden.

Categories
Development Linux

Reverse Proxy mit OneLogin hinter LoadBalancer

Die Version des Reverse Proxy mit OneLogin wird um einen vorgelagertem Load Balancer und einem zweiten Reverse Proxy erweitert:

Docker Compose

version: '3.8'
services:

  loadbalancer:
    build: ./loadbalancer
    hostname: loadbalancer
    volumes:
      - ${PWD}/loadbalancer/conf/loadbalancer_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/loadbalancer/conf/loadbalancer.conf:/usr/local/apache2/conf/loadbalancer.conf
    ports:
      - 80:80

  reverseproxy_1:
    build: ./reverseproxy
    hostname: reverseproxy_1
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf

  reverseproxy_2:
    build: ./reverseproxy
    hostname: reverseproxy_2
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf


  showheaders:
    build: ./showheaders
    hostname: showheaders     

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.

[...]
    <Proxy "balancer://myreverseproxy">
        BalancerMember http://reverseproxy_1:80
        BalancerMember http://reverseproxy_2:80
    </Proxy>
    
    ProxyPass /          balancer://myreverseproxy/
    ProxyPassReverse /   balancer://myreverseproxy/
[...]

Die geschützten Seiten ließen sich nicht öffnen.

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.

[...]
    ## Header for LoadBalancer
    Header add Set-Cookie "ROUTEID=.%%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED


    #define loadbalancer for Applicationserver
    <Proxy "balancer://myreverseproxy">
        BalancerMember http://reverseproxy_1:80 route=reverseproxy_1
        BalancerMember http://reverseproxy_2:80 route=reverseproxy_2
        ProxySet stickysession=ROUTEID
    </Proxy>

    ProxyPass /          balancer://myreverseproxy/
    ProxyPassReverse /   balancer://myreverseproxy/

[...]

Test

Über die Logausgaben in dem Terminalfenster, in dem Docker Compose gestartet wurde, kann man gut nachvollziehen, welche Server aufgerufen werden:

In der ShowHeaders Anwendung können wir sehen, welche ROUTEID gesetzt wurde und über den x-forwarded-host können wir die Route nachvollziehen.

In obigem Beispiel sind wir über RP2 gekommen. Wenn wir den ROUTID Cookie auf RP1 ändern, wird die nächste Anfrage über RP1 gerouted.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version3 zu finden.

Getrennte Netze

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:

version: '3.8'
services:

  loadbalancer:
    build: ./loadbalancer
    hostname: loadbalancer
    volumes:
      - ${PWD}/loadbalancer/conf/loadbalancer_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/loadbalancer/conf/loadbalancer.conf:/usr/local/apache2/conf/loadbalancer.conf
    networks:
      public_network:
      dmz_network:
    ports:
      - 80:80

  reverseproxy_1:
    build: ./reverseproxy
    hostname: reverseproxy_1
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    networks:
      dmz_network:
      private_network:

[...]

  admin: 
    image: httpd:2.4
    hostname: admin
    volumes:
      - ./admin/public_html:/usr/local/apache2/htdocs
    networks:
      private_network:

networks:
  public_network:
    external: true
  dmz_network:
    external: false
  private_network:
    external: false

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.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version4 zu finden.

Authorisierung

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.

Categories
Development Linux

Reverse Proxy mit OneLogin

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.

GitHub

Es wird ein Projekt auf GitHub für diesen Post angelegt: DockerOneLoginApacheSample

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.

Apache Module OpenID

Für den Apache HTTP wird mod_auth_openidc verwendet.

Docker

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.

version: '3.8'
services:

  reverseproxy:
    build: ./reverseproxy
    hostname: reverseproxy
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    ports:
      - 80:80

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

<VirtualHost *:80>
    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

	<Location /private>
        AuthType openid-connect
        Require valid-user
    </Location>

</VirtualHost>

Befehle

Docker Container starten:

docker-compose up

In den laufenden Docker Container einloggen:

docker exec -it dockeroneloginapachesample_reverseproxy_1 bash

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:

version: '3.8'
services:

  reverseproxy:
    build: ./reverseproxy
    hostname: reverseproxy
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    ports:
      - 80:80

  showheaders:
    build: ./showheaders
    hostname: showheaders
    ports:
      - 8080:8080

Die Reverse Proxy Konfiguration erweitern:

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

<VirtualHost *:80>
    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

	<Location /private>
        AuthType openid-connect
        Require valid-user
    </Location>


    # showheaders block
    ProxyPass        /showheaders   http://showheaders:8080/
    ProxyPassReverse /showheaders   http://showheaders:8080/

    <Location /showheaders>
        AuthType openid-connect
        Require valid-user
    </Location>

</VirtualHost>

Die Startseite wurde um einen Link zu ShowHeaders erweitert:

Zuerst der OneLogin Login:

Es werden alle übertragenen Header angezeigt:

Headers:

host
	showheaders:8080
upgrade-insecure-requests
	1
user-agent
	Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
accept
	text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site
	cross-site
sec-fetch-mode
	navigate
sec-fetch-user
	?1
sec-fetch-dest
	document
sec-ch-ua
	" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"
sec-ch-ua-mobile
	?0
sec-ch-ua-platform
	"Windows"
accept-encoding
	gzip, deflate, br
accept-language
	de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
cookie
	mod_auth_openidc_session=13dc8a76-10b9-479f-94b1-53d7ddb760e2
oidc_claim_sub
	175995661
oidc_claim_email
	Max@Mustermann.de
oidc_claim_preferred_username
	Max@Mustermann.de
oidc_claim_name
	Max Mustermann
oidc_claim_nonce
	mXShr6JxWX49umdsGUDe2l1zmkB0eYs9Vx7Jm1We38Q
oidc_claim_at_hash
	RBPJPDQuzxFJUembbLFdLg
oidc_claim_sid
	24f09d55-0a19-4a15-9446-010c84ff4461
oidc_claim_aud
	22a153c0-a91c-013a-175a-02471d082b0b208817
oidc_claim_exp
	1652179601
oidc_claim_iat
	1652172401
oidc_claim_iss
	https://deringo-dev.onelogin.com/oidc/2
oidc_access_token
	eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVcIsIMtpZCI6IkpSY080bnhzNWpnYzhZZE43STJoTE80Vl9xbDFiZG9pTVhtY1lnSG00SHMifQ.eyJqdGkiOiJHWlJac0YyVVhrd0xrZEl3WGplNEoiLCJzdWIiOiIxNzU5OTU2NjEiLCJpc3MiOiJodHRwczovL2RlcmluZ28tZGV2Lm9uZWxvZ2luLmNvbS9vaWRjLzIiLCJpYXQiOjE2NTIxNzI0MDEsImV4cCI6MTY1MjE3NjAwMSwic2NvcGUiOiJvcGVuaWQiLCJhdWQiOiIyMmExNTNjMC1hOTFjLTAxM2EtMTc1YS0wMjQ3MWQwODJiMGIyMDg4MTcifQ.barxBngs7jirZS3nAsYMpsqBdwybrmuqzCtP1dTiwJxEkbQsRe77Z0xqdGRmXWG8sr6s65omAcyF8ZaacY51OMQiVmEriL9MUxBTG3Z4-noO9C0nq6wmMrwHBls5CG9BPhnoc-rWMB_fvsFELiP0WZk5FM4iV-POqUkJgCNTKLbyBjpkm4y6Q4IUBgCpqmuRCtgfq0jMDrXpxLkiKkya7UymtGref2pL6UurD5r0VLwvU75lV927SUyxCKnUIDOY7Mdv05BzBfrCg6KBibWunGuY9wh6xs9WWreBlu21JcZxdgYL8Vx_MPfXrTjfU-0sjhACaqn5h0nnCrwcLoK8Hw
oidc_access_token_expires
	1652176004
x-forwarded-for
	172.24.0.1
x-forwarded-host
	localhost
x-forwarded-server
	localhost
connection
	Keep-Alive

Auffälligkeit

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.

Version1 ist ohne, Version2 ist mit ShowHeaders.