Adding authentication to Kubernetes app using Keycloak and the new oauth2-proxy

Carlos Eduardo
4 min readNov 11, 2020

As a follow-up post to my previous post about adding authentication transparently to an application

Configuring Keycloak

Go to the left-side menu item “Client Scopes” and click “Create”:

Create a new client scope called “api” with default settings, then click the “Mappers” tab to add the field mappings to this scope.

In this tab, create a new mapper called “groups” with the following settings:

Save this mapper and then click the “Add Builtin”, adding the existing mappers “username”, “email” and “profile”.

Finally, add this scope to the client at “Clients” > [your created client] > “Client Scopes”. Select the newly created scope “api” at the left box and click “Add Selected” on “Default Client Scopes”.

Now Keycloak is ready to send the correct tokens to Oauth2-proxy sidecar container.

Deploying the application

Blablabla…

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
- name: oauth2-proxy
image: carlosedp/oauth2-proxy:v7.0.0
imagePullPolicy: Always
args:
- --config=/etc/oauth2-proxy.cfg
ports:
- containerPort: 3000
name: web
volumeMounts:
- name: oauth2-proxy-config
mountPath: /etc/oauth2-proxy.cfg
subPath: oauth2-proxy.cfg
- name: oauth2-templates
mountPath: /templates
volumes:
- name: oauth2-proxy-config
configMap:
name: oauth2-proxy-config
- name: oauth2-templates
configMap:
name: oauth2-templates
---
apiVersion: v1
kind: ConfigMap
metadata:
name: oauth2-proxy-config
namespace: default
data:
oauth2-proxy.cfg: |+
# Provider config
provider="keycloak"
provider_display_name="Keycloak"
login_url="https://keycloak.192.168.1.170.nip.io:8443/auth/realms/Kubeapps/protocol/openid-connect/auth"
redeem_url="https://keycloak.192.168.1.170.nip.io:8443/auth/realms/Kubeapps/protocol/openid-connect/token"
validate_url="https://keycloak.192.168.1.170.nip.io:8443/auth/realms/Kubeapps/protocol/openid-connect/userinfo"
ssl_insecure_skip_verify=true
# Client config
client_id="kubernetes"
client_secret="f6c21649-9972-4da9-8f47-d66b64d207d1"
cookie_secret="OQINaROshtE9TcZkNAm5Zs2Pv3xaWytBmc5W7sPX7ws="
cookie_secure="false"
# Upstream config
http_address="0.0.0.0:3000"
upstreams="http://127.0.0.1:80/"
# Proxy Config
keycloak_group="/app1grp"
#user_id_claim="preferred_username"
skip_auth_routes=["/health.*"]
skip_provider_button="true"
reverse_proxy="true"
email_domains=["*"]
cookie_domains=[".nip.io"]
whitelist_domains=[".nip.io:*"]
custom_templates_dir="/templates"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: oauth2-templates
namespace: default
data:
sign_in.html: |+
<!DOCTYPE html> <html lang="en" charset="utf-8"> <head> <title>Sign In</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <style>body{font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 14px; line-height: 1.42857143; color: #333; background: #f0f0f0;}.signin{display:block; margin:20px auto; max-width:400px; background: #fff; border:1px solid #ccc; border-radius: 10px; padding: 20px;}.center{text-align:center;}.btn{color: #fff; background-color: #428bca; border: 1px solid #357ebd; -webkit-border-radius: 4; -moz-border-radius: 4; border-radius: 4px; font-size: 14px; padding: 6px 12px; text-decoration: none; cursor: pointer;}.btn:hover{background-color: #3071a9; border-color: #285e8e; text-decoration: none;}label{display: inline-block; max-width: 100%; margin-bottom: 5px; font-weight: 700;}input{display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; margin:0; box-sizing: border-box;}footer{display:block; font-size:10px; color:#aaa; text-align:center; margin-bottom:10px;}footer a{display:inline-block; height:25px; line-height:25px; color:#aaa; text-decoration:underline;}footer a:hover{color:#aaa;}</style> </head> <body> <div class="signin center"> <form method="GET" action="{{.ProxyPrefix}}/start"> <input type="hidden" name="rd" value="{{.Redirect}}">{{if .SignInMessage}}<p>{{.SignInMessage}}</p>{{end}}<button type="submit" class="btn">Sign in with{{.ProviderName}}</button><br/> </form> </div>{{if .CustomLogin}}<div class="signin"> <form method="POST" action="{{.ProxyPrefix}}/sign_in"> <input type="hidden" name="rd" value="{{.Redirect}}"> <label for="username">Username:</label><input type="text" name="username" id="username" size="10"><br/> <label for="password">Password:</label><input type="password" name="password" id="password" size="10"><br/> <button type="submit" class="btn">Sign In</button> </form> </div>{{end}}<script>if (window.location.hash){(function(){var inputs=document.getElementsByName('rd'); for (var i=0; i < inputs.length; i++){// Add hash, but make sure it is only added once var idx=inputs[i].value.indexOf('#'); if (idx >=0){// Remove existing hash from URL inputs[i].value=inputs[i].value.substr(0, idx);}inputs[i].value +=window.location.hash;}})();}</script> <footer>{{if eq .Footer "-"}}{{else if eq .Footer ""}}Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy">OAuth2 Proxy</a> version{{.Version}}{{else}}{{.Footer}}{{end}}</footer> </body> </html>
error.html: |+
<html lang="en"><head> <title>Access Forbidden</title><style>*{font-family: "Courier", "Courier New", "sans-serif"; margin:0; padding: 0;}body{background: #233142;}.whistle{width: 20%; fill: #f95959; margin: 100px 40%; text-align: left; transform: translate(-50%, -50%); transform: rotate(0); transform-origin: 80% 30%; animation: wiggle .2s infinite;}@keyframes wiggle{0%{transform: rotate(3deg);}50%{transform: rotate(0deg);}100%{transform: rotate(3deg);}}h1{margin-top: -100px; margin-bottom: 20px; color: #facf5a; text-align: center; font-size: 90px; font-weight: 800;}h2, a{color: #455d7a; text-align: center; font-size: 30px; text-transform: uppercase;}</style> </head><body> <use> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve" class="whistle"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path d="M4295.8,3963.2c-113-57.4-122.5-107.2-116.8-622.3l5.7-461.4l63.2-55.5c72.8-65.1,178.1-74.7,250.8-24.9c86.2,61.3,97.6,128.3,97.6,584c0,474.8-11.5,526.5-124.5,580.1C4393.4,4001.5,4372.4,4001.5,4295.8,3963.2z"/><path d="M3053.1,3134.2c-68.9-42.1-111-143.6-93.8-216.4c7.7-26.8,216.4-250.8,476.8-509.3c417.4-417.4,469.1-463.4,526.5-463.4c128.3,0,212.5,88.1,212.5,224c0,67-26.8,97.6-434.6,509.3c-241.2,241.2-459.5,449.9-488.2,465.3C3181.4,3180.1,3124,3178.2,3053.1,3134.2z"/><path d="M2653,1529.7C1644,1445.4,765.1,850,345.8-32.7C62.4-628.2,22.2-1317.4,234.8-1960.8C451.1-2621.3,947-3186.2,1584.6-3500.2c1018.6-501.6,2228.7-296.8,3040.5,515.1c317.8,317.8,561,723.7,670.1,1120.1c101.5,369.5,158.9,455.7,360,553.3c114.9,57.4,170.4,65.1,1487.7,229.8c752.5,93.8,1392,181.9,1420.7,193.4C8628.7-857.9,9900,1250.1,9900,1328.6c0,84.3-67,172.3-147.4,195.3c-51.7,15.3-790.8,19.1-2558,15.3l-2487.2-5.7l-55.5-63.2l-55.5-61.3v-344.6V719.8h-411.7h-411.7v325.5c0,509.3,11.5,499.7-616.5,494C2921,1537.3,2695.1,1533.5,2653,1529.7z"/></g></g></svg></use><h1>403</h1><h2>Not this time, access forbidden!</h2><h2><a href="{{.ProxyPrefix}}/sign_out?rd=https://keycloak.192.168.1.170.nip.io:8443/auth/realms/Kubeapps/protocol/openid-connect/logout?redirect_uri=https://google.com">Logout</h2></body></html>
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx
name: nginx
namespace: default
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: web
selector:
app: nginx
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: test.192.168.1.170.nip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80

It’s currently not possible to have multiple groups attached to the application. I've sent a PR adding this functionality that might be available soon.

Also since it’s not yet possible to configure the user claim, users in Keycloak should have an email (cannot be blank).

--

--

Carlos Eduardo

Writing everything cloud and all the tech behind it. If you like my projects and would like to support me, check my Patreon on https://www.patreon.com/carlosedp