DEV Community

Miklos Halasz
Miklos Halasz

Posted on

Basic Keycloak setup with Ansible

So in this post let me show how I solved the installation of Keycloak on a bare-metal (VM) server. I have a couple stuff in my homelab like Jenkins, Sonar, Nexus OSS, etc where I want to use and I have some projects that need SSO.

I picked Keycloak because it integrates well with Kubernetes / OpenShift and this fact is important to me, because I work a lot with these systems.

Install the Keycloak server

You can download the binary from the GitHub Release page of the project. I created an ansible playbook to perform the install. It was a good practice and I use Fedora servers in my homelab and if a new major version come out I can easily reinstall my services in the future.

site.yaml

So I created the following site.yaml:

- name: Install Keycloak
  hosts: keycloak
  vars:
    keycloak_version: 26.1.1
    keycloak_db_password: xxx

  become: true
  become_user: root
  become_method: sudo

  gather_facts: yes

  roles:
    - { role: keycloak }
Enter fullscreen mode Exit fullscreen mode

Of course the keycloak_db_password was hashed, I used the ansible-vault command to generate the sercure form of the password.

ansible-vault encrypt_string 'secret_password' --name 'dbPassword'
Enter fullscreen mode Exit fullscreen mode

This command gave me an output that I copied into the site.yaml file. Simple as that :).

Keycloak configuration

Database configuration

Because it is in my homelab environment I didn't overthink the database configuration and security settings for now. I SSH into the database server, started the psql and created the database for the Keycloak with the following statements:

-- Create user for db connection
CREATE USER keycloak_db WITH PASSWORD '1234';

-- Create database
CREATE DATABASE keycloak OWNER keycloak_db ENCODING 'UTF8';

-- stepped into the database with \c keycloak
CREATE SCHEMA prod;

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak_db;
GRANT ALL PRIVILEGES ON SCHEMA prod TO keycloak_db;
Enter fullscreen mode Exit fullscreen mode

In the following block you can see the configuration which is used by the Keycloak. There are some database configuration that points to one of my other local VM that runs... surprise: PostgreSQL.

# Database configuration
db=postgres
db-driver=org.postgresql.Driver
db-password={{ keycloak_db_password }}
db-schema=prod
db-url-database=keycloak
db-url-host=postgres.home
db-url-port=5432
db-username=keycloak_db

# Hostname
hostname=keycloak.home

# HTTP config
https-certificate-file=/etc/ssl/certs/keycloak/server.crt
https-certificate-key-file=/etc/ssl/certs/keycloak/server.key
https-port=443

# Health and metrics
health-enabled=true
metrics-enabled=true

# Logging
log=console,file

# Tracing
tracing-enabled=true
Enter fullscreen mode Exit fullscreen mode

As you can see above I configured the hostname property. It's quite important because of the redirections and other security stuffs that I don't know yet and the Keycloak does behind the scenes. If you are a Keycloak expert please let me know why got so big role this setting. After that comes the TLS certificates and the HTTPS port (which is 8443 by default). I set the standard 443 because I'm lazy, I did not want write always the port number. It's important to mention no service should be run in the name of the root user. I also did not run the Keycloak as root, I created a user and group for this purpose, but in this case you have to enable the use of privileged ports.

Systemd unit

Nothing interesting here. I want to run Keycloak as a service so I wrote the Systemd unit file below. I used the AmbientCapabilities=CAP_NET_BIND_SERVICE setting to enable the use of privileged ports.

[Unit]
Description=Keycloak Identity and Access Management
After=network.target
Wants=network-online.target

[Service]
User=keycloak
Group=keycloak
WorkingDirectory=/opt/keycloak
ExecStart=/opt/keycloak/bin/kc.sh start
AmbientCapabilities=CAP_NET_BIND_SERVICE
Restart=on-failure
TimeoutStopSec=10
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Install role

The essence of the ansible playbook is below:

- name: Install keycloak
  block:
    - name: Check if there's any Keycloak installation
      ansible.builtin.shell: "test -L /opt/keycloak && echo 'installed' || echo 'not_installed'"
      register: is_installed

    - ansible.builtin.debug: 
        msg: "Keycloak is not installed"
      when: is_installed.stdout == 'not_installed'

    - name: Get installed version
      ansible.builtin.shell: readlink -f /opt/keycloak | sed -E 's/.*keycloak-([0-9]+\.[0-9]+\.[0-9]+\-[0-9]+)/\1/'
      args:
        chdir: /opt
      ignore_errors: true
      register: current_version
      when: is_installed.stdout == 'installed'

    - ansible.builtin.debug: 
        msg: "Keycloak is installed. Version: {{ current_version.stdout }}"
      when: is_installed.stdout == 'installed'

    - name: Perform installation
      when: is_installed.stdout == 'not_installed' or current_version.stdout != keycloak_version
      block: 
        # Create non-root group and user to run the service
        - name: Create 'keycloak' group
          ansible.builtin.group:
            name: keycloak
            state: present

        - name: Create 'keycloak' user
          ansible.builtin.user:
            name: keycloak
            group: keycloak
            state: present

        # Install prerequisites to run Keycloak and connect to DB
        - name: Install OpenJDK 21
          ansible.builtin.dnf:
            name: 
              - java-21-openjdk
              - postgresql-jdbc
            state: latest

        # Stop the service if it's already installed
        - name: Stop Keycloak service
          ansible.builtin.systemd_service:
            name: keycloak
            state: stopped
          ignore_errors: true

        - name: Create temporary download directory
          ansible.builtin.file:
            path: /tmp/keycloak/
            state: directory

        - name: Download Keycloak binary
          ansible.builtin.get_url:
            url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_version }}/keycloak-{{ keycloak_version }}.tar.gz"
            dest: /tmp/keycloak/
            checksum: sha1:a6faa5d97eb349a1898aa7ab81b2f9fc2778bde5

        - name: Get the tarball path
          ansible.builtin.find:
            paths: /tmp/keycloak/
            file_type: file
            recurse: no
          register: tmp_files

        - name: Unarchive Keycloak tarball
          ansible.builtin.unarchive:
            src: "{{ tmp_files.files[0].path }}"
            dest: /tmp/keycloak/
            remote_src: true

        - name: Install Keycloak
          ansible.builtin.copy:
            src: "/tmp/keycloak/keycloak-{{ keycloak_version }}"
            dest: /opt
            remote_src: true

        # I did not turn the SELinux into permissive or disabled mode, so I have to deal with this also.
        - name: Set SELinux type
          ansible.builtin.file:
            path: "/opt/keycloak-{{ keycloak_version }}"
            owner: keycloak
            group: keycloak
            setype: usr_t
            recurse: true

        - name: Create symlink to Keycloak directory
          ansible.builtin.file:
            src: "/opt/keycloak-{{ keycloak_version }}"
            dest: /opt/keycloak
            state: link

        - name: Add configuration file
          ansible.builtin.template:
            src: keycloak.conf.j2
            dest: /opt/keycloak/conf/keycloak.conf
            group: keycloak
            owner: keycloak
            backup: true

        - name: Add Keycloak systemd unit file
          ansible.builtin.copy:
            src: keycloak.service
            dest: /etc/systemd/system/

        # Open 443 port on the firewall
        - name: Enable network connection, HTTP and HTTPS services
          ansible.builtin.shell: |
            setsebool httpd_can_network_connect 1
            firewall-cmd --add-service=https --permanent
            firewall-cmd --reload

  # Do some revert work if needed
  rescue:
    - name: "Remove {{ keycloak_version }} form /opt"
      ansible.builtin.file:
        path: "/opt/keycloak-{{ keycloak_version }}/"
        state: absent

    - name: Create symlink to original Keycloak directory
      ansible.builtin.file:
        src: "/opt/keycloak-{{ current_version.stdout }}/"
        dest: /opt/keycloak
        state: link
      when: is_installed.stdout == 'installed'

  # Alwazs try to restart the service and delete temporary files
  always:
    - name: Start Keycloak service
      ansible.builtin.systemd_service:
        name: keycloak
        daemon_reload: true
        state: started
        enabled: true

    - name: Delete temporary files
      ansible.builtin.file:
        path: /tmp/keycloak/
        state: absent
Enter fullscreen mode Exit fullscreen mode

I tried to put some error handling logic into this role so if some error happens it rolls back for the previous Keycloak version. After the playbook had finished successfully I was able to open the web UI.

First login

When you open first time the Keycloak UI it tells you that you should go in some localhost page. Okay, but I installed it on a headless VM. Fortunately we have a tool called SSH. With the following command I opened a tunnel and mapped the Keycloak server port onto my dev machine:

ssh -L 8443:127.0.0.1:443 myUser@keycloak
Enter fullscreen mode Exit fullscreen mode

With this trick I was able to create the first temporary user with admin rights and use the Keycloak UI through the supposed URL.

Create administrator user

After you first logged in with the temporary admin user Keycloak warns you to create a permanent admin user and delete the temporary one.

I'm sure I have skill issues, but I was not able to create this permanent admin user from the UI, so went back to CLI. With the following command I created a session with the temp. admin.

./kcadm.sh config credentials --server https://keycloak.home:443 --realm master --user tmpadmin
Enter fullscreen mode Exit fullscreen mode

Then created a permanent user who called superadmin:

./kcadm.sh create users -r master -s username=superadmin -s enabled=true -o --fields id,username
Enter fullscreen mode Exit fullscreen mode

Granted administrative permissions to the newly created user:

./kcadm.sh add-roles --rolename=admin --uusername=superadmin -r master
Enter fullscreen mode Exit fullscreen mode

And after all one more strep. Set the password for this user:

./kcadm.sh set-password --username=superadmin --new-password='Almafa1234'
Enter fullscreen mode Exit fullscreen mode

Tadaaa. We can log in the admin console and create many interesting things.

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs