diff --git a/CHANGELOG.md b/CHANGELOG.md index ba3fa63..d58ad62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ ## 2026-07-03 ### Ajouté +- **Rôle `serveur_oauth2_proxy` — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO.** + oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak **devant** n'importe quelle app sans OIDC + natif : elle reçoit l'utilisateur authentifié via en-tête, en auth `external`. Rôle paramétrable + (client, secret voûte, redirect, upstream, cookie voûte) — **réutilisable** pour toute app + OIDC-less. Éprouvé sur `sup-01` **devant Icinga Web 2** : client Keycloak `icingaweb2`, + oauth2-proxy `:4180` (exposé par l'edge) → upstream nginx local `:8080` → icingaweb2 + `backend = external` (REMOTE_USER depuis `X-Forwarded-Preferred-Username`). **Prouvé** (flux + authorization code headless) : `testmail` → oauth2-proxy → Keycloak → **icingaweb2 `/dashboard`, + connecté** (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct + d'icingaweb2. **Réglages appris** : `insecure_oidc_allow_unverified_email` (les users LDAP n'ont + pas `email_verified` ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe + `X-Forwarded-*` (pas `X-Auth-Request-*`) ; **handler nginx en `restart` (pas `reload`)** car un + changement d'adresse d'écoute n'est pas pris par un reload gracieux. - **Rôle `serveur_icingaweb2` — Icinga Web 2 (UI native) + module IcingaDB : éprouvé.** App PHP (php8.4-fpm) servie par un **nginx local**, exposée par l'edge (`icinga.lab.chezlepro.internal`, auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config **par fichiers `.ini`** diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index 9e0f397..0a758f7 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -44,9 +44,11 @@ sur `forge-01`, PostgreSQL via registre, **branché au SSO OIDC** — 2e app : ` compte auto-créé). Et **`serveur_redis`** (sur `data-sql-01`, cache réseau, auth obligatoire — SET/GET prouvé, accès sans mot de passe refusé). Et le **cœur `serveur_icinga`** (sur `sup-01` : icinga2 + icingadb + redis dédié, base PostgreSQL via registre — supervise, IcingaDB peuplée) + -**`serveur_icingaweb2`** (UI native, PHP/nginx local, module IcingaDB + **module BPM**, **auth LDAP** — -`testmail` se connecte, voit la supervision, et un processus métier BPM rend un état). -**Pile Icinga complète : moteur + Web 2 + BPM.** Restent « code non éprouvé » : `serveur_sendmail`. +**`serveur_icingaweb2`** (UI native, PHP/nginx local, module IcingaDB + **module BPM**, **SSO Keycloak** +via `serveur_oauth2_proxy` — `testmail` se connecte par le SSO, voit la supervision, et un processus +métier BPM rend un état). **Pile Icinga complète : moteur + Web 2 + BPM, au SSO.** Plus la passerelle +**`serveur_oauth2_proxy`** (oauth2-proxy) : SSO OIDC générique réutilisable pour toute app sans OIDC +natif. Restent « code non éprouvé » : `serveur_sendmail`. > **Lacunes connues sur Keycloak** (déployé mais pas complet) : la **fédération LDAP** (Keycloak > → OpenLDAP, modèle d'identité A) n'est **pas encore automatisée** dans le rôle ; l'**edge nginx** diff --git a/playbooks/groupes/serveur_oauth2_proxy.yml b/playbooks/groupes/serveur_oauth2_proxy.yml new file mode 100644 index 0000000..0bc1f3d --- /dev/null +++ b/playbooks/groupes/serveur_oauth2_proxy.yml @@ -0,0 +1,13 @@ +--- +- name: Appliquer le groupe serveur_oauth2_proxy + hosts: serveur_oauth2_proxy + become: true + gather_facts: true + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + roles: + - serveur_oauth2_proxy diff --git a/roles/serveur_icingaweb2/defaults/main.yml b/roles/serveur_icingaweb2/defaults/main.yml index 26293c5..d0b267c 100644 --- a/roles/serveur_icingaweb2/defaults/main.yml +++ b/roles/serveur_icingaweb2/defaults/main.yml @@ -45,6 +45,14 @@ serveur_icingaweb2_ldap_user_attr: "uid" # Qui reçoit le rôle Administrateur (liste d'uid séparés par des virgules). serveur_icingaweb2_admins: "testmail" +# Backend d'authentification : "ldap" (direct) ou "external" (utilisateur fourni par une +# passerelle SSO devant, ex. oauth2-proxy → Keycloak). En "external", nginx doit poser REMOTE_USER. +serveur_icingaweb2_auth: "ldap" +# Variable nginx d'où vient REMOTE_USER. En SSO : l'en-tête d'oauth2-proxy. +serveur_icingaweb2_remote_user: "$remote_user" +# Préfixe d'écoute nginx local ("" = toutes interfaces ; "127.0.0.1:" = local, derrière la passerelle). +serveur_icingaweb2_nginx_bind: "" + # Modules icingaweb2 à activer. serveur_icingaweb2_modules: - icingadb diff --git a/roles/serveur_icingaweb2/handlers/main.yml b/roles/serveur_icingaweb2/handlers/main.yml index f6329f3..42837f2 100644 --- a/roles/serveur_icingaweb2/handlers/main.yml +++ b/roles/serveur_icingaweb2/handlers/main.yml @@ -5,8 +5,10 @@ state: restarted when: not ansible_check_mode +# restart (pas reload) : un changement d'adresse d'écoute (ex. 0.0.0.0 -> 127.0.0.1) +# n'est pas pris par un reload gracieux (il conserve les sockets d'écoute existants). - name: Recharger nginx ansible.builtin.systemd: name: "{{ serveur_icingaweb2_nginx_service }}" - state: reloaded + state: restarted when: not ansible_check_mode diff --git a/roles/serveur_icingaweb2/templates/authentication.ini.j2 b/roles/serveur_icingaweb2/templates/authentication.ini.j2 index 39f9798..2684af9 100644 --- a/roles/serveur_icingaweb2/templates/authentication.ini.j2 +++ b/roles/serveur_icingaweb2/templates/authentication.ini.j2 @@ -1,7 +1,13 @@ ; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main. +{% if serveur_icingaweb2_auth == 'external' %} +; SSO : l'utilisateur est fourni par la passerelle (oauth2-proxy → Keycloak) via REMOTE_USER. +[icingaweb2] +backend = "external" +{% else %} [icingaweb2] backend = "ldap" resource = "icingaweb_ldap" user_class = "{{ serveur_icingaweb2_ldap_user_class }}" user_name_attribute = "{{ serveur_icingaweb2_ldap_user_attr }}" base_dn = "{{ serveur_icingaweb2_ldap_users_dn }}" +{% endif %} diff --git a/roles/serveur_icingaweb2/templates/nginx.conf.j2 b/roles/serveur_icingaweb2/templates/nginx.conf.j2 index a3526e6..020a0de 100644 --- a/roles/serveur_icingaweb2/templates/nginx.conf.j2 +++ b/roles/serveur_icingaweb2/templates/nginx.conf.j2 @@ -1,8 +1,10 @@ # Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main. # nginx LOCAL : sert Icinga Web 2 (PHP-FPM). L'edge nginx proxifie par nom vers ce port. server { - listen {{ serveur_icingaweb2_http_port }}; + listen {{ serveur_icingaweb2_nginx_bind }}{{ serveur_icingaweb2_http_port }}; +{% if not serveur_icingaweb2_nginx_bind %} listen [::]:{{ serveur_icingaweb2_http_port }}; +{% endif %} server_name {{ serveur_icingaweb2_hostname }}; root {{ serveur_icingaweb2_docroot }}; @@ -18,7 +20,7 @@ server { include fastcgi_params; fastcgi_param SCRIPT_FILENAME {{ serveur_icingaweb2_docroot }}/index.php; fastcgi_param ICINGAWEB_CONFIGDIR {{ serveur_icingaweb2_config_dir }}; - fastcgi_param REMOTE_USER $remote_user; + fastcgi_param REMOTE_USER {{ serveur_icingaweb2_remote_user }}; } location ~ /\.ht { diff --git a/roles/serveur_oauth2_proxy/defaults/main.yml b/roles/serveur_oauth2_proxy/defaults/main.yml new file mode 100644 index 0000000..b2d7a64 --- /dev/null +++ b/roles/serveur_oauth2_proxy/defaults/main.yml @@ -0,0 +1,36 @@ +--- +# Passerelle SSO OIDC générique (oauth2-proxy) : place Keycloak DEVANT n'importe quelle +# application sans OIDC natif. L'app reçoit l'utilisateur via en-têtes (auth « external »). +# Binaire Go statique depuis les releases GitHub (pas d'apt). Requiert client_pki (confiance +# step_ca pour joindre Keycloak) + résolution de l'hôte Keycloak. +serveur_oauth2_proxy_version: "v7.15.3" +serveur_oauth2_proxy_archive: "oauth2-proxy-{{ serveur_oauth2_proxy_version }}.linux-amd64.tar.gz" +serveur_oauth2_proxy_url: >- + https://github.com/oauth2-proxy/oauth2-proxy/releases/download/{{ + serveur_oauth2_proxy_version }}/{{ serveur_oauth2_proxy_archive }} +serveur_oauth2_proxy_binaire: "/usr/local/bin/oauth2-proxy" +serveur_oauth2_proxy_utilisateur: "oauth2-proxy" +serveur_oauth2_proxy_config: "/etc/oauth2-proxy/oauth2-proxy.cfg" +serveur_oauth2_proxy_service: "oauth2-proxy" + +# Écoute (l'edge nginx proxifie ici). +serveur_oauth2_proxy_ecoute: "0.0.0.0:4180" + +# OIDC (Keycloak) — realm applicatif. +serveur_oauth2_proxy_realm: "chezlepro" +serveur_oauth2_proxy_issuer: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_oauth2_proxy_realm }}" +serveur_oauth2_proxy_client_id: "" # OBLIGATOIRE (par instance) +serveur_oauth2_proxy_client_secret: "" # OBLIGATOIRE (voûte) +serveur_oauth2_proxy_redirect_url: "" # OBLIGATOIRE (https:///oauth2/callback) + +# Application protégée (upstream local). +serveur_oauth2_proxy_upstream: "" # OBLIGATOIRE (ex. http://127.0.0.1:8080) + +# Secret de cookie (voûte) — 32 octets base64. +serveur_oauth2_proxy_cookie_secret: "{{ vault_oauth2_cookie | default('') }}" +# * = tout utilisateur authentifié par Keycloak. +serveur_oauth2_proxy_email_domains: "*" +# IdP interne de confiance : les utilisateurs LDAP n'ont pas le drapeau email_verified. +serveur_oauth2_proxy_allow_unverified_email: true +# Bundle d'AC système (racine step_ca via client_pki) pour joindre Keycloak en HTTPS. +serveur_oauth2_proxy_ca_bundle: "/etc/ssl/certs/ca-certificates.crt" diff --git a/roles/serveur_oauth2_proxy/handlers/main.yml b/roles/serveur_oauth2_proxy/handlers/main.yml new file mode 100644 index 0000000..31de872 --- /dev/null +++ b/roles/serveur_oauth2_proxy/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Redémarrer oauth2-proxy + ansible.builtin.systemd: + name: "{{ serveur_oauth2_proxy_service }}" + state: restarted + daemon_reload: true + when: not ansible_check_mode diff --git a/roles/serveur_oauth2_proxy/meta/empreinte.yml b/roles/serveur_oauth2_proxy/meta/empreinte.yml new file mode 100644 index 0000000..0600b2e --- /dev/null +++ b/roles/serveur_oauth2_proxy/meta/empreinte.yml @@ -0,0 +1,6 @@ +--- +# Empreinte ressources — oauth2-proxy (binaire Go léger). +setops_empreinte: + coeurs: 1 + memoire_mo: 128 + disque_go: 1 diff --git a/roles/serveur_oauth2_proxy/tasks/main.yml b/roles/serveur_oauth2_proxy/tasks/main.yml new file mode 100644 index 0000000..8690e65 --- /dev/null +++ b/roles/serveur_oauth2_proxy/tasks/main.yml @@ -0,0 +1,70 @@ +--- +- name: Exiger la configuration OIDC (client, secret, redirect, upstream, cookie) + ansible.builtin.assert: + that: + - serveur_oauth2_proxy_client_id | length > 0 + - serveur_oauth2_proxy_client_secret | length > 0 + - serveur_oauth2_proxy_redirect_url | length > 0 + - serveur_oauth2_proxy_upstream | length > 0 + - serveur_oauth2_proxy_cookie_secret | length > 0 + fail_msg: "client_id, client_secret (voûte), redirect_url, upstream et cookie_secret (voûte) requis." + +- name: Créer l'utilisateur système oauth2-proxy + ansible.builtin.user: + name: "{{ serveur_oauth2_proxy_utilisateur }}" + system: true + shell: /usr/sbin/nologin + create_home: false + +- name: Télécharger oauth2-proxy + ansible.builtin.get_url: + url: "{{ serveur_oauth2_proxy_url }}" + dest: "/tmp/oauth2-proxy-{{ serveur_oauth2_proxy_version }}.tar.gz" + mode: "0644" + +- name: Extraire le binaire oauth2-proxy + ansible.builtin.unarchive: + src: "/tmp/oauth2-proxy-{{ serveur_oauth2_proxy_version }}.tar.gz" + dest: /usr/local/bin + remote_src: true + extra_opts: ["--strip-components=1", "--wildcards", "*/oauth2-proxy"] + owner: root + group: root + mode: "0755" + creates: "{{ serveur_oauth2_proxy_binaire }}" + notify: Redémarrer oauth2-proxy + +- name: Créer le répertoire de configuration + ansible.builtin.file: + path: "{{ serveur_oauth2_proxy_config | dirname }}" + state: directory + owner: root + group: "{{ serveur_oauth2_proxy_utilisateur }}" + mode: "0750" + +- name: Déployer la configuration oauth2-proxy + ansible.builtin.template: + src: oauth2-proxy.cfg.j2 + dest: "{{ serveur_oauth2_proxy_config }}" + owner: root + group: "{{ serveur_oauth2_proxy_utilisateur }}" + mode: "0640" + no_log: true + notify: Redémarrer oauth2-proxy + +- name: Déployer l'unité systemd oauth2-proxy + ansible.builtin.template: + src: oauth2-proxy.service.j2 + dest: /etc/systemd/system/oauth2-proxy.service + owner: root + group: root + mode: "0644" + notify: Redémarrer oauth2-proxy + +- name: Activer et démarrer oauth2-proxy + when: not ansible_check_mode + ansible.builtin.systemd: + name: "{{ serveur_oauth2_proxy_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/serveur_oauth2_proxy/templates/oauth2-proxy.cfg.j2 b/roles/serveur_oauth2_proxy/templates/oauth2-proxy.cfg.j2 new file mode 100644 index 0000000..a234e2d --- /dev/null +++ b/roles/serveur_oauth2_proxy/templates/oauth2-proxy.cfg.j2 @@ -0,0 +1,22 @@ +# Géré par Set-OPS (rôle serveur_oauth2_proxy). Ne pas éditer à la main. +http_address = "{{ serveur_oauth2_proxy_ecoute }}" +reverse_proxy = true + +provider = "oidc" +oidc_issuer_url = "{{ serveur_oauth2_proxy_issuer }}" +client_id = "{{ serveur_oauth2_proxy_client_id }}" +client_secret = "{{ serveur_oauth2_proxy_client_secret }}" +redirect_url = "{{ serveur_oauth2_proxy_redirect_url }}" +provider_ca_files = ["{{ serveur_oauth2_proxy_ca_bundle }}"] + +upstreams = ["{{ serveur_oauth2_proxy_upstream }}"] + +email_domains = ["{{ serveur_oauth2_proxy_email_domains }}"] +insecure_oidc_allow_unverified_email = {{ serveur_oauth2_proxy_allow_unverified_email | lower }} +cookie_secret = "{{ serveur_oauth2_proxy_cookie_secret }}" +cookie_secure = true + +# TLS terminé à l'edge ; oauth2-proxy voit HTTP mais reverse_proxy=true honore X-Forwarded-Proto. +skip_provider_button = true +set_xauthrequest = true +pass_user_headers = true diff --git a/roles/serveur_oauth2_proxy/templates/oauth2-proxy.service.j2 b/roles/serveur_oauth2_proxy/templates/oauth2-proxy.service.j2 new file mode 100644 index 0000000..7341558 --- /dev/null +++ b/roles/serveur_oauth2_proxy/templates/oauth2-proxy.service.j2 @@ -0,0 +1,19 @@ +# Géré par Set-OPS (rôle serveur_oauth2_proxy). Ne pas éditer à la main. +[Unit] +Description=OAuth2 Proxy (passerelle SSO OIDC Keycloak) +After=network-online.target +Wants=network-online.target + +[Service] +User={{ serveur_oauth2_proxy_utilisateur }} +Group={{ serveur_oauth2_proxy_utilisateur }} +ExecStart={{ serveur_oauth2_proxy_binaire }} --config={{ serveur_oauth2_proxy_config }} +Restart=on-failure +RestartSec=5 +NoNewPrivileges=yes +ProtectSystem=full +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target