Integrating Nginx and Keycloak without OpenResty
Extending on my previous post about creating a custom CA and using client certificates through Cloudflare, I wanted to write about how I integrated Keycloak with Nginx without OpenResty.
As we had a handful of different websites and applications running on the server, I wanted to simplify everything with the use of an identity platform. This would then be the place that usernames and passwords are stored, thus governing authentication to any existing or new property.
Installing Keycloak itself with a supplied Ansible role didn't quite go to plan, mostly due to some minor differences in Ubuntu 20 and Keycloak 12. Eventually I worked my way around the error messages that Ansible kept throwing and created a pull request so we could share the love back.
Despite the fact that every single blog post and technical article claimed the only way to complete a Keycloak/Nginx integration was to use OpenResty (as it combines Nginx with LuaJIT), I didn't want to because I was extremely happy with the Ansible role I use to manage Nginx and a comparable role wasn't available for OpenResty. Using this role was also the reason I didn't want to have to install Nginx from source.
As a result, I needed to find a way to install Lua and other dependencies for me to be able to use access_by_lua
in my Nginx configuation. Doing this by hand in the first isntance revealed that:
- The versions of Lua and LuaRocks in default Ubuntu apt repos were not recent enough
- Lua-resty-string did not have a recent enough version in the LuaRocks repos which led to
undefined symbol: EVP_CIPHER_CTX_init
errors
The correct combination of each for a successful implementation was therefore:
- Lua 5.4.2
- LuaRocks 3.4.0
- libnginx-mod-http-lua
- lua-cjson
- lua-resty-http
- lua-resty-session
- lua-resty-jwt
- lua-resty-openidc
- lua-resty-string
All of the Lua modules except for lua-resty-string can be installed directly with LuaRocks. Because of the error mentioned above, I installed lua-resty-string from source. Converting this to Ansible and using roles to install Lua and LuaRocks gave me the following example playbook:
- hosts: webservers
vars_files:
- vars/main.yml
roles:
- { role: andrewrothstein.lua }
- { role: andrewrothstein.luarocks }
- name: install libnginx-mod-http-lua
package:
name: libnginx-mod-http-lua
state: present
- name: luarocks install lua-resty-http
become: yes
become_user: root
command: luarocks install lua-resty-http
args:
creates: /usr/local/share/lua/5.1/resty/http.lua
- name: luarocks install lua-resty-session
become: yes
become_user: root
command: luarocks install lua-resty-session
args:
creates: /usr/local/share/lua/5.1/resty/session.lua
- name: luarocks install lua-resty-jwt
become: yes
become_user: root
command: luarocks install lua-resty-jwt
args:
creates: /usr/local/share/lua/5.1/resty/jwt.lua
- name: luarocks install lua-resty-openidc
become: yes
become_user: root
command: luarocks install lua-resty-openidc
args:
creates: /usr/local/share/lua/5.1/resty/openidc.lua
- name: luarocks install lua-cjson
become: yes
become_user: root
command: luarocks install lua-cjson
args:
creates: /usr/local/share/lua/5.1/cjson
- name: look for resty-string
become: yes
stat:
path: /usr/local/share/lua/5.1/resty/string.lua
changed_when: False
register: restystring
- when: not restystring.stat.exists
block:
- name: download tgz...
become: yes
become_user: root
get_url:
url: https://github.com/openresty/lua-resty-string/archive/v0.12.tar.gz
dest: /tmp/lua-resty-string-0.12
checksum: sha256:bfd8c4b6c90aa9dcbe047ac798593a41a3f21edcb71904d50d8ac0e8c77d1132
- name: unarchiving tgz
become: yes
become_user: root
unarchive:
remote_src: yes
src: /tmp/lua-resty-string-0.12
dest: /usr/local/src
- copy:
src: "{{ item }}"
dest: /usr/local/share/lua/5.1/resty/
owner: root
group: root
mode: '0644'
with_fileglob:
- /usr/local/src/lua-resty-string-0.12/lib/resty/*
always:
- name: cleaning up...
become: yes
become_user: root
with_items:
- /tmp/lua-resty-string-0.12
- /usr/local/src/lua-resty-string-0.12
file:
path: '{{ item }}'
state: absent
The final step from here was to extend the Nginx configuration from my previous blog post to use access_by_lua
. The set $session_secret
line was crucially important as without that I kept running into the following error:
SSL_do_handshake() failed (SSL: error:141CF06C:SSL routines:tls_parse_ctos_key_share:bad key share) while SSL handshaking
The final working Nginx configuration looked like the below (with replacement values for session_secret, client_id and client_secret of course).
server {
listen 443 ssl http2;
server_name www.oursite.com;
index index.html index.htm;
include /etc/nginx/cloudflare-allow.conf;
deny all;
ssl_certificate /etc/letsencrypt/live/oursite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/oursite.com/privkey.pem;
ssl_verify_client on;
ssl_client_certificate /etc/ssl/ca/certs/OurRoot_CA.crt;
set $session_secret gbbPlrI5jJrWTAHuZpQxAg961dwNSUFB;
access_by_lua '
local opts = {
redirect_uri = "https://www.oursite.com/redirect_uri",
accept_none_alg = true,
discovery = "https://keycloak.oursite.com/auth/realms/master/.well-known/openid-configuration",
client_id = "nginx-oursite",
client_secret = "client-secret-goes-here",
ssl_verify = "no",
redirect_uri_scheme = "https",
logout_path = "/logout",
redirect_after_logout_uri = "https://keycloak.oursite.com/auth/realms/master/protocol/openid-connect/logout",
redirect_after_logout_with_id_token_hint = false,
session_contents = {id_token=true}
}
local res, err = require("resty.openidc").authenticate(opts)
if err then
ngx.status = 403
ngx.say(err)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
';
location / {
proxy_pass http://localhost:3434/;
}
}
Hopefully this helps anyone else wanting to use Ansible to install Nginx and Keycloak!