3 min read

Integrating Nginx and Keycloak without OpenResty

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 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
Example playbook to integrate Nginx with Keycloak.

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
SSL error when not setting a global session_secret.

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/;
    }

}
Nginx configuration using Lua to authenticate sessions with Keycloak.

Hopefully this helps anyone else wanting to use Ansible to install Nginx and Keycloak!