Using httpd as a reverse proxy for OpenID Connect authentication

Oren Oichman
ITNEXT
Published in
8 min readMar 12, 2021

--

Why this Article ?

Well, for Many Reasons… While going through the transition from Modular Application to Micro Service Application the authentication methods had changed as well..
while in the old days we would connect our application to Ldap or even a Kerberos Server (and more Active directory a like) in today’s world we are using HTTP based protocols for authentication such as SAML2 and OpenID Connect.

In some cases the overhead of migrating the application to the new way of authentication is a lot of work. in some cases, the application doesn’t have any authentication at all, and you want to make sure users are authenticated before they begin their work.

In the end, we need to take a reverse proxy that will send the authentication request to the OpenID Connect Server and redirect us to the application after we have authenticated.

What do we need ?

To achieve our setup, we need an OpenID Connect Server, and for that, we are going to use the Keycloak Open Source along with httpd with OpenID Connect module for the reverse proxy part.
This Article Assume that you have cluster-admin privileges to the Kubernetes cluster we are working with.

  1. Keycloak — the Red Hat Signed Sign-On open source project (for the production use case, I highly recommend using rh-sso and not keycloak for all the relevant aspect of a production environment
  2. A custom container of httpd, which I am going to elaborate on in this tutorial

Getting Started

KeyCloak Setup

To deploy keyCloak to your Kubernetes Cluster you can run the following commands :

First thing first , create a namespace for KeyCloak

$ oc create ns keycloak$ kubectl config set-context --current --namespace=keycloak

Step 1 — Database
KeyCloak can use PostgreSQL as a backend server, for that we will deploy it first with persistent storage.

Let’s create the PVC and

$ cat > postgresql-pvc.yaml << EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 1Gi
EOF

and apply it :

$ oc apply -f postgresql-pvc.yaml

now we need to generate the PostgreSQL deployment :

$ cat > postgresql-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: docker.io/library/postgres:11
imagePullPolicy: "Always"
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
value: keycloak_passwd
- name: POSTGRES_USER
value: keycloak
- name: POSTGRES_DB
value: keycloak
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgredb
volumes:
- name: postgredb
persistentVolumeClaim:
claimName: postgresql-pvc
EOF

And Apply it :

$ oc apply -f postgresql-deployment.yaml

For keycloak to work with the DB, we need to generate service for it :

$ cat > postgresql-service.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
ports:
- name: postgres
port: 5432
targetPort: 5432
selector:
app: postgres
EOF

And apply it :

$ oc apply -f postgresql-service.yaml

Step 2— Application
The First thing we need to do it to deploy application and point it to our PostgreSQL server :

$ cat > keycloak.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:12.0.4
env:
- name: KEYCLOAK_USER
value: "admin"
- name: KEYCLOAK_PASSWORD
value: "admin"
- name: PROXY_ADDRESS_FORWARDING
value: "true"
- name: DB_ADDR
value: postgres.keycloak.svc
- name: DB_USER
value: keycloak
- name: DB_PASSWORD
value: keycloak_passwd
- name: DB_DATABASE
value: keycloak
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
readinessProbe:
httpGet:
path: /auth/realms/master
port: 8080
EOF

And Apply it :

$ oc apply -f keycloak.yaml

make sure it is up and running:

$ oc get pods
NAME READY STATUS RESTARTS AGE
postgres-66cb8c965f-b96lr 1/1 Running 1 16h
keycloak-758d65676d-qwc7j 1/1 Running 2 16h

Now we need to add the Service and the ingress for our keycloak server so for this example we will call it sso.example.com

$ cat > keycloak-service.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: keycloak
labels:
app: keycloak
spec:
ports:
- name: keycloak
port: 8080
targetPort: 8080
- name: keycloak-ssl
port: 8443
targetPort: 8443
selector:
app: keycloak
EOF

and for the ingress :

$ cat > keycloak-ingress.yaml << EOF
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: keycloak-ingress
spec:
tls:
- hosts:
- sso.example.net
secretName: tls-sso
rules:
- host: "sso.example.net"
http:
paths:
- backend:
serviceName: keycloak
servicePort: 8080
path: /
EOF

NOTE
As you can see we are using a here a TLS secret in order to enable TLS , to generate the secret we create the certificates using the tutorial of OpenSSL Alter DNS name. once all files are in place we can go ahead and generate it :

$ oc create secret tls tls-sso --cert=./sso.crt --key=./sso.key

Once the secret and the ingress route have been created it is time to switch to the web so go into https://sso.example.com

For a quick fix we can extract the FQDN with the following command :

$ echo -n 'https://' &&  oc get ingress keycloak-ingress -o jsonpath='{.spec.rules[0].host}' ; echo

Go to the Web Page :

Kick on the Admin Console :

Login to the portal ( the default username and password are admin/admin)

Once login let’s create a new Domain :

For this example we will call it “example” :

Once we created the Realm Let’s add a Client :

Go to the Clients “TAB” and click on create :

write the name of the client in this case “reverse-sso”

Click back on the client and make sure of the following :

  • login theme : keycloak
  • Client Protocol : OpenID Connect
  • Access Type: confidential

in the redirect URL put an asterisks “*” and click on save.

A new tab of “credentials” will appear at the top of the page , click on it and then click on “Regenerate Registration access token”

Copy both Secret and Access Token aside and we are good to go (moving on to the reverse proxy setting)

Step 3 — Reverse-proxy

Everything we did until now is straightforward in regards deploying application on Kubernetes and now we can move forward and create a small application with parts which are already available … we just need to put them all together.

Let’s take the CENTOS base image, install Apache’s httpd with the proxy and the openidc.

Create a Dockerfile which the following content :

$ cat > Dockerfile << EOF
FROM centos
MAINTAINER Oren Oichman <Back to Root>
RUN dnf install -y httpd && dnf module \
enable mod_auth_openidc -y && \
dnf install -y mod_auth_openidc && dnf clean all
COPY run-httpd.sh /usr/sbin/run-httpd.sh
COPY ca.crt /etc/pki/ca-trust/source/anchors/rh-sso.crt
RUN update-ca-trust extract
RUN echo "PidFile /tmp/http.pid" >> /etc/httpd/conf/httpd.conf
RUN sed -i "s/Listen\ 80/Listen\ 8080/g" /etc/httpd/conf/httpd.conf
RUN sed -i "s/\"logs\/error_log\"/\/dev\/stderr/g" /etc/httpd/conf/httpd.conf
RUN sed -i "s/CustomLog \"logs\/access_log\"/CustomLog \/dev\/stdout/g" /etc/httpd/conf/httpd.conf
RUN echo 'IncludeOptional /opt/app-root/*.conf' >> /etc/httpd/conf/httpd.conf
RUN mkdir /opt/app-root/ && chown apache:apache /opt/app-root/ && chmod 777 /opt/app-root/
USER apacheEXPOSE 8080 8081
ENTRYPOINT ["/usr/sbin/run-httpd.sh"]
EOF

Now we need to create a configuration file which enables the modules we just installed but we need to it when the Service starts.
The best way to do that is to create a startup scripts which generate the configuration file.

Let’s generate the the “run-httpd.sh” script :

$ cat > run-httpd.sh << EOF
#!/bin/bash
if [ -z ${RH_SSO_FQDN} ]; then
echo "Environment variable RH_SSO_FQDN undefined"
exit 1
elif [[ -z $CLIENT_ID ]]; then
echo "Environment variable CLIENT_ID undefined"
exit 1
elif [[ -z $CLIENT_SECRET ]]; then
echo "Environment variable CLIENT_SECRET undefined"
exit 1
elif [[ -z $REVERSE_SSO_ROUTE ]]; then
echo "Environment variable REVERSE_SSO_ROUTE undefined"
exit 1
elif [[ -z ${DST_SERVICE_NAME} ]]; then
echo "Environment variable DST_SERVICE_NAME undefined"
exit 1
elif [[ -z $RH_SSO_REALM ]]; then
echo "Environment variable RH_SSO_REALM undefined"
exit 1
elif [[ -z ${DST_SERVICE_PORT} ]]; then
echo "Environment variable DST_SERVICE_PORT undefined"
exit 1
fi
echo "
<VirtualHost *:8080>
OIDCProviderMetadataURL https://${RH_SSO_FQDN}/auth/realms/${RH_SSO_REALM}/.well-known/openid-configuration
OIDCClientID $CLIENT_ID
OIDCClientSecret $CLIENT_SECRET
OIDCRedirectURI https://${REVERSE_SSO_ROUTE}/oauth2callback
OIDCCryptoPassphrase openshift
OIDCPassClaimsAs both
#Header set Authorization "Bearer %{OIDC_access_token}e" env=OIDC_access_token
<Directory "/opt/app-root/">
AllowOverride All
</Directory>
<Location />
AuthType openid-connect
Require valid-user
ProxyPreserveHost on
ProxyPass http://${DST_SERVICE_NAME}:${DST_SERVICE_PORT}/
ProxyPassReverse http://${DST_SERVICE_NAME}:${DST_SERVICE_PORT}/
</Location>
</VirtualHost>
" > /tmp/reverse.conf
mv /tmp/reverse.conf /opt/app-root/reverse.conf/usr/sbin/httpd $OPTIONS -DFOREGROUND
EOF

Copy your keyclock CA to a file named ca.crt and place it in our working directory

#cp <path to your CA> ./ca.crt

Now we can build the image

$ buildah bud -f Dockerfile -t < registry >/reverse-sso

And now just push the image to your registry :

$ buildah push < registry >/reverse-sso

Now that we have an image we can now continue to the deployment.

I know it would look odd at first but we need to deloy the service and the route/ingress and then the pod.

Let’s begin with the service :

$ cat > service.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: reverse-sso
spec:
selector:
app: reverse-sso
ports:
- protocol: TCP
port: 8080
targetPort: 8080
EOF

And now for the ingress :

$ cat > ingress.yaml << EOF
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: reverse-ingress
spec:
rules:
- host: "my-app.example.com"
http:
paths:
- backend:
serviceName: reverse-sso
servicePort: 8080
path: /
EOF

for the last step let’s deploy our application :

$ cat > pod-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: reverse-sso
spec:
selector:
matchLabels:
app: reverse-sso
replicas: 1
template:
metadata:
labels:
app: reverse-sso
spec:
containers:
- name: reverse-sso
image: < registry >/reverse-sso:latest
imagePullPolicy: Always
env:
- name: RH_SSO_FQDN
value:
- name: CLIENT_ID
value:
- name: CLIENT_SECRET
value:
- name: REVERSE_SSO_ROUTE
value:
- name: DST_SERVICE_NAME
value:
- name: RH_SSO_REALM
value:
- name: DST_SERVICE_PORT
value:
ports:
- containerPort: 8080
EOF

the rest if fairly simple , add the follow values to the deployment file and apply it :

  • RH_SSO_FQDN — the FQDN of the red hat SSO (in our example case it is keycloak
  • CLIENT_ID — the ID of the client we are using in our case it is “reverse-sso”
  • CLIENT_SECRET — the secret of the client from the credential tab in keycloak
  • REVERSE_SSO_ROUTE — the route/ingress FQDN of the reverse-sso service (which we created earlier)
  • DST_SERVICE_NAME — the destination service we would want to send the traffic to
  • RH_SSO_REALM — in our case it is “example”
  • DST_SERVICE_PORT — the port of our service application

and apply the deployment :

$ oc apply -f pod-deployment.yaml

All that is left is to go to the route/ingress of the reverse single sign on :

$ echo -n 'http://' &&  oc get ingress reverse-ingress -o jsonpath='{.spec.rules[0].host}' ; echo
http://my-app.example.com

If you have any question feel free to responed/ leave a comment.
You can find on linkedin at : https://www.linkedin.com/in/orenoichman
Or twitter at : https://twitter.com/ooichman

HAVE FUN !!!

--

--