diff --git a/playbooks/keycloak.yml b/playbooks/keycloak.yml index 9926ff0..ef142b3 100644 --- a/playbooks/keycloak.yml +++ b/playbooks/keycloak.yml @@ -1,25 +1,17 @@ ---- -# Infrastructure -# Ansible instructions to deploy the infrastructure -# Copyright (C) 2019-2020 Christoph (Sheogorath) Kern -# Copyright (C) 2020 Saibotk -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +- name: Install Keycloak. -- name: Install & configure Keycloak hosts: keycloak + roles: - - docker - - docker_cleanup - - traefik - - keycloak + - role: podman + become: true + tags: + - always + - podman + - role: caddy + become: true + tags: + - always + - caddy + - role: keycloak + become: true diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md deleted file mode 100644 index 1c83094..0000000 --- a/roles/keycloak/README.md +++ /dev/null @@ -1,35 +0,0 @@ -Keycloak -========= - -This will setup a [Keycloak](https://www.keycloak.org) instance using their official docker container and traefik as a reverse proxy. - -Requirements ------------- - -You will need to have docker, docker-compose and traefik installed or declared as dependencies with their respective roles. - -**This role assumes that you have setup traefik with an endpoint called `websecure`.** - -Role Variables --------------- - -**Please look at the [defaults/main.yml](defaults/main.yml) for all available variables and their description.** - -**Note: Lines that are commented out via `#` are usually still valid/used variables, but they are not defined by default, so they might enable a feature, when uncommenting/defining them!** - -### Global variables, that are used: - -- `proxy_network`: Defined by the local traefik installation, this is the shared proxy network used by traefik to reach the containers. (optional) -- `proxy_hiddenservice`: Defined by the local traefik installation, this is used to generate the alt-svc header for the alternative Tor domain. (optional) - -Dependencies ------------- - -- docker -- docker-compose -- traefik - -License -------- - -GPL-3.0-only diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml index a059370..c7f297e 100644 --- a/roles/keycloak/defaults/main.yml +++ b/roles/keycloak/defaults/main.yml @@ -1,58 +1,41 @@ ---- -# Default variables for the keycloak role +keycloak_install_dir: "/opt/keycloak" -# Infrastructure -# Ansible instructions to deploy the infrastructure -# Copyright (C) 2019-2020 Christoph (Sheogorath) Kern -# Copyright (C) 2020 Alexander Wellbrock -# Copyright (C) 2020 Saibotk -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +keycloak_domains: + - keycloak.example.com + - keycloak2.example.com -# Install locations -keycloak_install_location: /srv/keycloak -keycloak_database_location: "{{ keycloak_install_location }}/database" - -# The keycloak image to use -keycloak_baseimage: quay.io/keycloak/keycloak - -# The keycloak & postgres version -# !!! Don't just update the postgres version, instead backup first and check if you need to migrate !!! +keycloak_containerimage: quay.io/keycloak/keycloak # renovate: depName=quay.io/keycloak/keycloak -keycloak_version: 26.0.5 +keycloak_image_tag: "26.1.0" + +keycloak_postgres_containerimage: docker.io/library/postgres # renovate: depName=docker.io/library/postgres -keycloak_postgres_version: 17.0 +keycloak_postgres_image_tag: "17.2-alpine" -# The image tags -keycloak_image_version: "{{ keycloak_version }}" -keycloak_postgres_image_version: "{{ keycloak_postgres_version }}-alpine" +keycloak_selinux_level: "{{ omit }}" -# The log level settings for keycloak -keycloak_log_level: "ERROR" -keycloak_root_log_level: "ERROR" +# Default JVM params of Keycloak need at least 512mb of ram +keycloak_memory_low: 512m +keycloak_memory_high: 0 +keycloak_swap_max: -1 -# The certresolver traefik should use for this domain -keycloak_traefik_certresolver: letsencrypt_http +keycloak_postgres_memory_low: 256m +keycloak_postgres_memory_high: 0 +keycloak_postgres_swap_max: -1 -# The domain which traefik will serve keycloak under -keycloak_domain: keycloak.example.com +keycloak_database_password: "{{ lookup('passwordstore', 'keycloak.example.com/database create=true') }}" -# The database credentials. -keycloak_database_user: "keycloak" -keycloak_database_password: "{{ lookup('passwordstore', keycloak_domain + '/database' + ' create=true') }}" +keycloak_environment_vars: + KC_HOSTNAME_STRICT: "true" + # Unless you include 'https://' HTTPS won't be enforced + KC_HOSTNAME: "https://{{ keycloak_domains | first }}" + KC_LOG_LEVEL: ERROR + KC_DB_URL_HOST: keycloak-postgres + KC_DB_URL_DATABASE: keycloak + KC_DB_USERNAME: "keycloak" + KC_DB_PASSWORD: "{{ keycloak_database_password }}" -# The default welcome theme keycloak will use -keycloak_theme_welcome: "{{ omit }}" - -# The SELinux level that should be used for the postgres data folder/container -keycloak_postgres_selinux_level: "{{ omit }}" +keycloak_postgres_environment_vars: + POSTGRES_DB: "keycloak" + POSTGRES_USER: "keycloak" + POSTGRES_PASSWORD: "{{ keycloak_database_password }}" diff --git a/roles/keycloak/handlers/main.yml b/roles/keycloak/handlers/main.yml new file mode 100644 index 0000000..8a3f1a2 --- /dev/null +++ b/roles/keycloak/handlers/main.yml @@ -0,0 +1,25 @@ +- name: Apply new SELinux file context to filesystem. + ansible.builtin.command: "restorecon -irF {{ keycloak_install_dir }}" + become: true + listen: "keycloak selinux context changed" + +- name: Restart keycloak service. + ansible.builtin.systemd: + state: restarted + name: keycloak.service + daemon_reload: true + become: true + listen: + - "keycloak service changed" + - "keycloak network changed" + +- name: Restart keycloak postgres service. + ansible.builtin.systemd: + state: restarted + name: keycloak-postgres.service + daemon_reload: true + become: true + listen: + - "keycloak postgres service changed" + - "keycloak selinux context changed" + - "keycloak network changed" diff --git a/roles/keycloak/meta/main.yml b/roles/keycloak/meta/main.yml index 7839933..43520e0 100644 --- a/roles/keycloak/meta/main.yml +++ b/roles/keycloak/meta/main.yml @@ -1,44 +1,21 @@ galaxy_info: - author: saibotk - description: "Installs Keycloak via the official container and traefik." + author: histalek + description: Deploy keycloak with podman and systemd. + + issue_tracker_url: https://git.histalek.de/histalek-de/infrastructure/-/issues + license: GPL-3.0-only - min_ansible_version: "2.9" - standalone: true + + min_ansible_version: "2.10" platforms: - - name: EL - versions: - - all - - name: GenericUNIX - versions: - - all - name: Fedora versions: - - all - - name: opensuse - versions: - - all - - name: GenericBSD - versions: - - all - - name: FreeBSD - versions: - - all - - name: Ubuntu - versions: - - all - - name: SLES - versions: - - all - - name: GenericLinux - versions: - - all - - name: Debian - versions: - - all + - "39" + - "40" + + standalone: true galaxy_tags: [] -dependencies: - - role: docker - - role: traefik +dependencies: [] diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index fc921d9..e489536 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,78 +1,132 @@ ---- -# Tasks file for the keycloak role - -# Infrastructure -# Ansible instructions to deploy the infrastructure -# Copyright (C) 2019-2020 Christoph (Sheogorath) Kern -# Copyright (C) 2020 Alexander Wellbrock -# Copyright (C) 2020 Saibotk -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - name: Update default SELinux contexts community.general.sefcontext: - target: "{{ item }}(/.*)?" + target: "{{ keycloak_install_dir }}/postgres(/.*)?" setype: "container_file_t" - selevel: "{{ keycloak_postgres_selinux_level }}" + selevel: "{{ keycloak_selinux_level }}" state: present - with_items: - - "{{ keycloak_database_location }}" become: true + notify: "keycloak selinux context changed" -- name: Create install directory +- name: Create keycloak directories. ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: "0700" + path: "{{ keycloak_install_dir }}" owner: "root" group: "root" - with_items: - - "{{ keycloak_install_location }}" - tags: - - keycloak - become: true - -- name: Create data directory - ansible.builtin.file: # noqa risky-file-permissions # Container manages permissions on its own - path: "{{ item }}" state: directory - setype: "container_file_t" - selevel: "{{ keycloak_postgres_selinux_level }}" - with_items: - - "{{ keycloak_database_location }}" - tags: - - keycloak + mode: "0700" become: true -- name: Deploy docker-compose.yml +- name: Ensure keycloak postgres directory exists. + block: + - name: Stat keycloak postgres dir. + ansible.builtin.stat: + path: "{{ keycloak_install_dir }}/postgres" + become: true + register: keycloak_stat_postgres_dir + + - name: Create keycloak postgres directory. + ansible.builtin.file: + path: "{{ keycloak_install_dir }}/postgres" + owner: "{{ keycloak_stat_postgres_dir.stat.uid | default('root') }}" + group: "{{ keycloak_stat_postgres_dir.stat.gid | default('root') }}" + state: directory + mode: "0700" + setype: "container_file_t" + selevel: "{{ keycloak_selinux_level }}" + become: true + +- name: Deploy keycloak environment file. ansible.builtin.template: - src: docker-compose.yml - dest: "{{ keycloak_install_location }}/docker-compose.yml" + src: keycloak.env.j2 + dest: "{{ keycloak_install_dir }}/keycloak.env" mode: "0600" owner: "root" group: "root" - validate: docker compose -f %s config -q - tags: - - docker - - keycloak + become: true + notify: "keycloak service changed" + +- name: Deploy keycloak postgres environment file. + ansible.builtin.template: + src: keycloak-postgres.env.j2 + dest: "{{ keycloak_install_dir }}/keycloak-postgres.env" + mode: "0600" + owner: "root" + group: "root" + become: true + notify: "keycloak postgres service changed" + +- name: Ensure container image is present on the host. + containers.podman.podman_image: + name: "{{ item.name }}" + state: present + tag: "{{ item.tag }}" + loop: + - name: "{{ keycloak_containerimage }}" + tag: "{{ keycloak_image_tag }}" + - name: "{{ keycloak_postgres_containerimage }}" + tag: "{{ keycloak_postgres_image_tag }}" become: true -- name: Compose keycloak - community.docker.docker_compose_v2: - state: present - project_src: "{{ keycloak_install_location }}" - pull: always - remove_orphans: true - tags: - - keycloak +- name: Add caddy config file. + block: + - name: Check caddy config dir. + ansible.builtin.stat: + path: "{{ caddy_install_dir }}/config" + become: true + register: caddy_stat_config_dir + + - name: Template caddy config for keycloak. + ansible.builtin.template: + src: keycloak.caddy.j2 + dest: "{{ caddy_install_dir }}/config/keycloak.caddy" + mode: "0600" + setype: "container_file_t" + selevel: "{{ caddy_selinux_level }}" + owner: "{{ caddy_stat_config_dir.stat.uid }}" + group: "{{ caddy_stat_config_dir.stat.gid }}" + notify: "caddy config changed" + become: true + +- name: Create keycloak container file. + ansible.builtin.template: + src: keycloak.container.j2 + dest: "/etc/containers/systemd/keycloak.container" + owner: "root" + group: "root" + mode: "0644" + become: true + notify: "keycloak service changed" + +- name: Create keycloak postgres container file. + ansible.builtin.template: + src: keycloak-postgres.container.j2 + dest: "/etc/containers/systemd/keycloak-postgres.container" + owner: "root" + group: "root" + mode: "0644" + become: true + notify: "keycloak postgres service changed" + +- name: Create keycloak network file. + ansible.builtin.template: + src: keycloak.network.j2 + dest: /etc/containers/systemd/keycloak.network + owner: root + group: root + mode: "0644" + become: true + notify: "keycloak network changed" + +- name: Flush handlers + ansible.builtin.meta: flush_handlers + +- name: Ensure keycloak services are started. + ansible.builtin.systemd: + state: started + enabled: true + name: "{{ item }}" + daemon_reload: true + loop: + - keycloak.service + - keycloak-postgres.service become: true diff --git a/roles/keycloak/templates/docker-compose.yml b/roles/keycloak/templates/docker-compose.yml deleted file mode 100644 index fbd9e03..0000000 --- a/roles/keycloak/templates/docker-compose.yml +++ /dev/null @@ -1,90 +0,0 @@ -{{ ansible_managed | comment }} - -# Infrastructure -# Ansible instructions to deploy the infrastructure -# Copyright (C) 2019-2020 Christoph (Sheogorath) Kern -# Copyright (C) 2020 Saibotk -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -version: '2' -services: - keycloak: - image: {{ keycloak_baseimage }}:{{ keycloak_image_version }} - mem_limit: 1gb - memswap_limit: 1280mb - security_opt: - - no-new-privileges - labels: - - "traefik.http.routers.keycloak.rule=Host(`{{ keycloak_domain }}`) && PathPrefix(`/`)" - - "traefik.http.routers.keycloak.entrypoints=websecure" - - "traefik.http.routers.keycloak.tls=true" - - "traefik.http.routers.keycloak.tls.certresolver={{ keycloak_traefik_certresolver }}" - - "traefik.http.routers.keycloak.middlewares=keycloak,compress" - - "traefik.http.middlewares.keycloak.headers.sslredirect=true" - - "traefik.http.middlewares.keycloak.headers.stsSeconds=63072000" - - - "traefik.enable=true" -{% if proxy_network is defined %} - - "traefik.docker.network={{ proxy_network }}" -{% endif %} - - environment: - - "KC_PROXY_HEADERs=xforwarded" - - "KC_HTTP_ENABLED=true" - - "KC_FEATURES=scripts,persistent-user-sessions" - - "KC_HOSTNAME_STRICT=true" - - "KC_HOSTNAME=https://{{ keycloak_domain }}" - - "KC_LOGLEVEL={{ keycloak_log_level }}" - - "KC_DB_URL_HOST=database" - - "KC_DB_USERNAME={{ keycloak_database_user }}" - - "KC_DB_PASSWORD={{ keycloak_database_password }}" - networks: - backend: -{% if proxy_network is defined %} - {{ proxy_network }}: -{% endif %} - - command: ["start", "--db=postgres"{% if keycloak_theme_welcome != omit %}, "--spi-theme-welcome-theme={{ keycloak_theme_welcome }}"{% endif %}] - restart: always - - database: - image: docker.io/library/postgres:{{ keycloak_postgres_image_version }} - mem_limit: 256mb - memswap_limit: 512mb - read_only: true -{% if keycloak_postgres_selinux_level != omit %} - security_opt: - - label=level:{{ keycloak_postgres_selinux_level }} -{% endif %} - tmpfs: - - /run/postgresql:size=512K - - /tmp:size=128K - stop_grace_period: 2m - stop_signal: SIGINT - environment: - - POSTGRES_USER={{ keycloak_database_user }} - - POSTGRES_PASSWORD={{ keycloak_database_password }} - volumes: - - ./database:/var/lib/postgresql/data - networks: - backend: - restart: always - -networks: - backend: - internal: true -{% if proxy_network is defined %} - {{ proxy_network }}: - external: true -{% endif %} diff --git a/roles/keycloak/templates/keycloak-postgres.container.j2 b/roles/keycloak/templates/keycloak-postgres.container.j2 new file mode 100644 index 0000000..670920a --- /dev/null +++ b/roles/keycloak/templates/keycloak-postgres.container.j2 @@ -0,0 +1,42 @@ +{{ ansible_managed | comment }} + +[Unit] +Description = Postgres for keycloak + +[Service] +Restart = always +RestartSec = 5s + +[Container] +Image = {{ keycloak_postgres_containerimage }}:{{ keycloak_postgres_image_tag }} +ContainerName = keycloak-postgres + +AutoUpdate = registry +LogDriver = journald + +NoNewPrivileges = true +ReadOnly = true +DropCapability = all +AddCapability = CHOWN DAC_OVERRIDE SETUID SETGID +UserNS = auto:size=65535 +{% if keycloak_selinux_level != omit %} +SecurityLabelLevel = {{ keycloak_selinux_level }} +{% endif %} + +Network = keycloak.network +ExposeHostPort = 5432 + +EnvironmentFile = {{ keycloak_install_dir }}/keycloak-postgres.env + +Volume = {{ keycloak_install_dir }}/postgres:/var/lib/postgresql/data:U +Tmpfs = /run/postgresql:rw,noexec,nosuid,nodev,size=1m +Tmpfs = /tmp:rw,noexec,nosuid,nodev,size=50m + +PodmanArgs = --memory={{ keycloak_postgres_memory_high }} +PodmanArgs = --memory-swap={{ keycloak_postgres_swap_max }} +PodmanArgs = --memory-reservation={{ keycloak_postgres_memory_low }} + +PodmanArgs = --stop-signal=SIGINT + +[Install] +WantedBy = default.target diff --git a/roles/keycloak/templates/keycloak-postgres.env.j2 b/roles/keycloak/templates/keycloak-postgres.env.j2 new file mode 100644 index 0000000..613028b --- /dev/null +++ b/roles/keycloak/templates/keycloak-postgres.env.j2 @@ -0,0 +1,7 @@ +{{ ansible_managed | comment}} + +{% if keycloak_postgres_environment_vars is mapping %} +{% for key, value in keycloak_postgres_environment_vars.items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} diff --git a/roles/keycloak/templates/keycloak.caddy.j2 b/roles/keycloak/templates/keycloak.caddy.j2 new file mode 100644 index 0000000..9306817 --- /dev/null +++ b/roles/keycloak/templates/keycloak.caddy.j2 @@ -0,0 +1,21 @@ +{{ ansible_managed | comment }} + +{{ keycloak_domains | join(', ') }} { + encode gzip + + header { + # enable HSTS + Strict-Transport-Security "max-age=31536000; preload;" + + # disable clients from sniffing the media type + X-Content-Type-Options nosniff + + # keep referrer data off of HTTP connections + Referrer-Policy no-referrer-when-downgrade + + # Server name removing + -Server + } + + reverse_proxy keycloak:8080 +} diff --git a/roles/keycloak/templates/keycloak.container.j2 b/roles/keycloak/templates/keycloak.container.j2 new file mode 100644 index 0000000..7c0b37f --- /dev/null +++ b/roles/keycloak/templates/keycloak.container.j2 @@ -0,0 +1,40 @@ +{{ ansible_managed | comment }} + +[Unit] +Description = Keycloak Server +Requires = keycloak-postgres.service +After = keycloak-postgres.service + +[Service] +Restart = always +RestartSec = 5s + +[Container] +Image = {{ keycloak_containerimage }}:{{ keycloak_image_tag }} +ContainerName = keycloak + +Exec = start \ + --db=postgres \ + --proxy-headers=xforwarded \ + --http-enabled=true \ + --features=persistent-user-sessions + +AutoUpdate = registry +LogDriver = journald + +NoNewPrivileges = true +DropCapability = all +UserNS = auto:size=65535 + +Network = keycloak.network +Network = caddy.network +ExposeHostPort = 8080 + +EnvironmentFile = {{ keycloak_install_dir }}/keycloak.env + +PodmanArgs = --memory={{ keycloak_memory_high }} +PodmanArgs = --memory-swap={{ keycloak_swap_max }} +PodmanArgs = --memory-reservation={{ keycloak_memory_low }} + +[Install] +WantedBy = default.target diff --git a/roles/keycloak/templates/keycloak.env.j2 b/roles/keycloak/templates/keycloak.env.j2 new file mode 100644 index 0000000..82d388f --- /dev/null +++ b/roles/keycloak/templates/keycloak.env.j2 @@ -0,0 +1,7 @@ +{{ ansible_managed | comment}} + +{% if keycloak_environment_vars is mapping %} +{% for key, value in keycloak_environment_vars.items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} diff --git a/roles/keycloak/templates/keycloak.network.j2 b/roles/keycloak/templates/keycloak.network.j2 new file mode 100644 index 0000000..e96ca7e --- /dev/null +++ b/roles/keycloak/templates/keycloak.network.j2 @@ -0,0 +1,7 @@ +{{ ansible_managed | comment }} + +[Network] +NetworkName = keycloak_internal +Driver = bridge +Internal = true +IPv6 = true