Using httpd as a reverse proxy for OpenID Connect authentication
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.
- 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
- 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 allCOPY run-httpd.sh /usr/sbin/run-httpd.sh
COPY ca.crt /etc/pki/ca-trust/source/anchors/rh-sso.crtRUN 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.confRUN 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/bashif [ -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
fiecho "
<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.confmv /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 !!!