Six majeures d'un coup, mais la decouverte importante est ailleurs. LA « ROTATION DE CLE » N'EN ETAIT PAS UNE. Quatre versions, trois signataires differents — 10.0.0 par B3B1F60A, 12.0.0 par D0A82005, 14.0.0 et 16.0.2 par C4186DF6. Ce ne sont pas des cles distinctes : ce sont des SOUS-CLES de signature sous une primaire stable depuis 2022 (EB114F5E...C5923710, « Forgejo <contact@forgejo.org> »). La sous-cle 0F527CF9...0E1609E5 est bien celle qui avait signe la 12.0.0. D'ou une correction du verificateur : il comparait l'empreinte du SIGNATAIRE, donc une sous-cle, et aurait echoue a chaque rotation LEGITIME — on aurait appris a lever la garde pour avancer, ce qui est la pire chose qui puisse arriver a un controle. Il accepte desormais la cle primaire (dernier champ de VALIDSIG), qui survit aux rotations et refuse quand meme une cle etrangere. FORGEJO A L'ANCRE QUE KEYCLOAK N'A PAS. forgejo.org/download publie l'empreinte, et le binaire vient de codeberg.org : la source de confiance est INDEPENDANTE du canal de livraison. Le projet annonce lui-meme la rotation (« the GPG key is updated on a regular basis »), ce qui confirme qu'epingler la primaire est le bon choix. Somme sha256 egalement publiee et verifiee conforme. Eprouve dans les deux sens : nominal 0 ; binaire altere d'un octet 1 ; empreinte de Keycloak appliquee a Forgejo 1 ; signature d'un autre artefact 1 ; et Keycloak ne regresse pas apres modification du comparateur. Verifie : versions-mesurer 0 en retard, role applique sur forge-01, ansible-lint production sur 50 fichiers, prouver.py 35 OK (code lu sans tube). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
364 lines
14 KiB
YAML
364 lines
14 KiB
YAML
---
|
|
|
|
# L'URL PUBLIQUE de l'IdP se derive de l'exposition declaree au plan, elle ne se
|
|
# fabrique pas : `keycloak.<domaine>` n'est publie nulle part (voir resoudre_idp).
|
|
- name: Resoudre le fournisseur d'identite (role partage)
|
|
ansible.builtin.include_role:
|
|
name: resoudre_idp
|
|
vars:
|
|
resoudre_idp_realm: "{{ serveur_forgejo_oidc_realm }}"
|
|
- name: Refuser de fermer la connexion locale sur un Forgejo anterieur a la v10
|
|
# `ENABLE_INTERNAL_SIGNIN` a ete ajoute en v10 (suivi amont, issue 7476). Sur une
|
|
# version anterieure il est ignore EN SILENCE : le role croirait avoir ferme la porte
|
|
# alors qu'elle reste ouverte. On refuse plutot que de laisser croire.
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_forgejo_version is version('10.0.0', '>=')
|
|
fail_msg: >-
|
|
serveur_forgejo_connexion_locale=false exige Forgejo >= 10.0.0
|
|
(ici {{ serveur_forgejo_version }}) : le reglage serait ignore sans erreur.
|
|
Mettre a jour, ou assumer la connexion locale en la passant a true.
|
|
when: not (serveur_forgejo_connexion_locale | bool)
|
|
|
|
- name: Exiger les secrets Forgejo (Vault)
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_forgejo_secret_key | length > 0
|
|
- serveur_forgejo_internal_token | length > 0
|
|
- serveur_forgejo_admin_password | length > 0
|
|
fail_msg: "secret_key, internal_token et admin_password sont requis (Ansible Vault)."
|
|
|
|
- name: Resoudre la base de donnees depuis le registre (role partage)
|
|
ansible.builtin.include_role:
|
|
name: resoudre_base
|
|
vars:
|
|
resoudre_base_groupe: "{{ serveur_forgejo_groupe }}"
|
|
|
|
- name: Adopter les facts de base pour Forgejo
|
|
ansible.builtin.set_fact:
|
|
serveur_forgejo_entree: "{{ resoudre_base_entree }}"
|
|
serveur_forgejo_db_password: "{{ resoudre_base_db_password }}"
|
|
serveur_forgejo_db_host: "{{ resoudre_base_db_host }}"
|
|
serveur_forgejo_db_port: "{{ resoudre_base_db_port }}"
|
|
no_log: true
|
|
|
|
- name: Creer l'utilisateur systeme git
|
|
ansible.builtin.user:
|
|
name: "{{ serveur_forgejo_utilisateur }}"
|
|
system: true
|
|
shell: /bin/bash
|
|
home: "/home/{{ serveur_forgejo_utilisateur }}"
|
|
create_home: true
|
|
comment: "Git Version Control"
|
|
|
|
# Le chemin de destination PORTE la version : `forgejo-10.0.0` ne peut pas designer un
|
|
# autre contenu demain. Le binaire est donc immuable une fois pose, et le retelecharger
|
|
# n'a aucun sens — or `get_url` sans garde recontacte le serveur distant a CHAQUE
|
|
# deploiement, pour 106 Mo deja presents.
|
|
#
|
|
# Constate le 2026-08-09 pendant le passage d'idempotence : « Connection failure: The
|
|
# read operation timed out ». Un deploiement de flotte echouait parce qu'un serveur tiers
|
|
# etait lent — sur une plateforme qui se veut souveraine, c'est une dependance de trop
|
|
# sur le chemin critique.
|
|
- name: Le binaire de cette version est-il deja pose ?
|
|
ansible.builtin.stat:
|
|
path: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
|
|
register: serveur_forgejo_binaire_present
|
|
|
|
# Le controleur telecharge, puis pousse par SSH — la cible ne tire jamais d'Internet.
|
|
# Motif explique en detail dans `roles/serveur_nextcloud/tasks/installer.yml`. C'est
|
|
# precisement ce binaire qui a fait tomber un deploiement de flotte le 2026-08-09, sur un
|
|
# « read operation timed out » venu d'un serveur tiers.
|
|
- name: Cache d artefacts du contrôleur
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_forgejo_cache_local }}"
|
|
state: directory
|
|
mode: "0700"
|
|
delegate_to: localhost
|
|
become: false
|
|
when: not serveur_forgejo_binaire_present.stat.exists
|
|
|
|
- name: L artefact est-il déjà dans le cache du contrôleur ?
|
|
ansible.builtin.stat:
|
|
path: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
|
|
register: serveur_forgejo_cache_present
|
|
delegate_to: localhost
|
|
become: false
|
|
when: not serveur_forgejo_binaire_present.stat.exists
|
|
|
|
- name: Télécharger le binaire Forgejo dans le cache du contrôleur (une seule fois)
|
|
ansible.builtin.get_url:
|
|
url: "{{ serveur_forgejo_url }}"
|
|
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
|
|
mode: "0644"
|
|
delegate_to: localhost
|
|
become: false
|
|
when:
|
|
- not serveur_forgejo_binaire_present.stat.exists
|
|
- not (serveur_forgejo_cache_present.stat.exists | default(false))
|
|
|
|
# VERIFIER LA SIGNATURE AVANT DE POSER UN BINAIRE EXECUTABLE SUR UN HOTE.
|
|
#
|
|
# C'est un binaire, pas une archive de donnees : ce qu'on depose ici s'execute en tant que
|
|
# service. La verification tourne sur le CONTROLEUR, contre la cle versionnee du depot et
|
|
# l'empreinte PRIMAIRE epinglee — voir `defaults/main.yml` pour la raison de la primaire.
|
|
- name: Récupérer la signature détachée du binaire
|
|
ansible.builtin.get_url:
|
|
url: "{{ serveur_forgejo_signature_url }}"
|
|
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
|
|
mode: "0644"
|
|
force: true
|
|
delegate_to: localhost
|
|
become: false
|
|
when: not serveur_forgejo_binaire_present.stat.exists
|
|
|
|
- name: Vérifier la signature du binaire contre l empreinte épinglée
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "{{ role_path }}/../../scripts/verifier_signature.py"
|
|
- --artefact
|
|
- "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
|
|
- --signature
|
|
- "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
|
|
- --cle
|
|
- "{{ role_path }}/files/forgejo-release.asc"
|
|
- --empreinte
|
|
- "{{ serveur_forgejo_signature_empreinte }}"
|
|
delegate_to: localhost
|
|
become: false
|
|
changed_when: false
|
|
when: not serveur_forgejo_binaire_present.stat.exists
|
|
|
|
- name: Déposer le binaire Forgejo sur l hôte depuis le cache
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
|
|
dest: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
when: not serveur_forgejo_binaire_present.stat.exists
|
|
|
|
- name: Activer la version courante du binaire
|
|
ansible.builtin.file:
|
|
src: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
|
|
dest: "{{ serveur_forgejo_binaire }}"
|
|
state: link
|
|
notify: Redemarrer forgejo
|
|
|
|
- name: Creer les repertoires de donnees
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_forgejo_data }}/{{ item }}"
|
|
state: directory
|
|
owner: "{{ serveur_forgejo_utilisateur }}"
|
|
group: "{{ serveur_forgejo_utilisateur }}"
|
|
mode: "0750"
|
|
loop:
|
|
- ""
|
|
- custom
|
|
- data
|
|
- log
|
|
|
|
- name: Creer le repertoire de configuration
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_forgejo_config_dir }}"
|
|
state: directory
|
|
owner: root
|
|
group: "{{ serveur_forgejo_utilisateur }}"
|
|
mode: "0770"
|
|
|
|
# JWT_SECRET appartient au SERVICE, pas au depot : Forgejo le genere au premier
|
|
# demarrage et le persiste dans `app.ini`. Le gabarit ne le portait pas — donc chaque
|
|
# rendu l'EFFACAIT, Forgejo en generait un nouveau au redemarrage, et le passage suivant
|
|
# recommencait. Autrement dit : chaque deploiement faisait tourner le secret JWT de la
|
|
# forge, invalidant les jetons qu'elle avait emis. Trouve par le passage d'idempotence du
|
|
# 2026-08-09 — seule ligne differente entre deux rendus.
|
|
#
|
|
# On le RELIT donc avant de rendre, et on le repose tel quel. Le depot cesse de disputer
|
|
# au service une valeur qui lui appartient.
|
|
- name: Relire le secret JWT genere par Forgejo (lui appartient)
|
|
ansible.builtin.slurp:
|
|
src: "{{ serveur_forgejo_config }}"
|
|
register: serveur_forgejo_app_ini
|
|
failed_when: false
|
|
no_log: true
|
|
|
|
- name: Retenir le secret JWT (vide au premier deploiement)
|
|
ansible.builtin.set_fact:
|
|
# Sans groupe de capture : on selectionne la ligne, puis on retire le prefixe. La
|
|
# forme avec `regex_search(..., '\\1', multiline=True)` fonctionnait pourtant en
|
|
# isolation — mais elle echouait sur l'hote, et `no_log` (indispensable ici, c'est un
|
|
# secret) empechait de voir pourquoi. Une expression qu'on ne peut pas diagnostiquer
|
|
# en place doit ceder a une expression plus simple.
|
|
serveur_forgejo_jwt_secret: >-
|
|
{{ (serveur_forgejo_app_ini.content | default('') | b64decode).splitlines()
|
|
| select('match', '^JWT_SECRET *=')
|
|
| map('regex_replace', '^JWT_SECRET *= *', '')
|
|
| first | default('', true) | trim }}
|
|
no_log: true
|
|
|
|
- name: Deployer app.ini
|
|
ansible.builtin.template:
|
|
src: app.ini.j2
|
|
dest: "{{ serveur_forgejo_config }}"
|
|
owner: "{{ serveur_forgejo_utilisateur }}" # Forgejo persiste des secrets générés (oauth2 JWT) → doit pouvoir écrire
|
|
group: "{{ serveur_forgejo_utilisateur }}"
|
|
# 0600, et non 0640 : Forgejo REECRIT ce fichier lui-meme (il y persiste des secrets
|
|
# generes) et le repose systematiquement en 0600. Le role remettait 0640 a chaque
|
|
# passage, Forgejo le ramenait a 0600 — deux proprietaires pour un fichier, en
|
|
# desaccord, et un `changed` perpetuel qui redemarrait le service pour rien.
|
|
# Trouve par le passage d'idempotence du 2026-08-09 : contenu identique, diff VIDE,
|
|
# seul le mode differait — c'est ce qui rendait la cause invisible.
|
|
#
|
|
# Le desaccord n'avait aucun effet utile : le groupe est `git`, c'est-a-dire le
|
|
# service lui-meme. On s'aligne sur le plus strict, qui est aussi le sien.
|
|
mode: "0600"
|
|
no_log: true
|
|
notify: Redemarrer forgejo
|
|
|
|
- name: Deployer l'identite visuelle (logo, favicon, accent aurore, accueil)
|
|
ansible.builtin.copy:
|
|
src: custom/
|
|
dest: "{{ serveur_forgejo_data }}/custom/"
|
|
owner: "{{ serveur_forgejo_utilisateur }}"
|
|
group: "{{ serveur_forgejo_utilisateur }}"
|
|
mode: "0644"
|
|
directory_mode: "0755"
|
|
when: serveur_forgejo_branding | bool
|
|
notify: Redemarrer forgejo
|
|
|
|
- name: Deployer l'unite systemd forgejo
|
|
ansible.builtin.template:
|
|
src: forgejo.service.j2
|
|
dest: /etc/systemd/system/forgejo.service
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Redemarrer forgejo
|
|
|
|
- name: Activer et demarrer Forgejo
|
|
when: not ansible_check_mode
|
|
ansible.builtin.systemd:
|
|
name: "{{ serveur_forgejo_service }}"
|
|
enabled: true
|
|
state: started
|
|
daemon_reload: true
|
|
|
|
- name: Appliquer les redemarrages Forgejo (config a jour) avant admin/OAuth2
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
- name: Attendre que Forgejo reponde (migrations terminees)
|
|
ansible.builtin.wait_for:
|
|
host: 127.0.0.1
|
|
port: "{{ serveur_forgejo_http_port }}"
|
|
timeout: 120
|
|
when: not ansible_check_mode
|
|
|
|
- name: Creer le compte administrateur (une fois)
|
|
ansible.builtin.shell:
|
|
cmd: >-
|
|
{{ serveur_forgejo_binaire }} admin user create
|
|
--admin --username {{ serveur_forgejo_admin_user }}
|
|
--email {{ serveur_forgejo_admin_email }}
|
|
--password '{{ serveur_forgejo_admin_password }}'
|
|
--config {{ serveur_forgejo_config }}
|
|
&& touch {{ serveur_forgejo_data }}/.admin-created
|
|
creates: "{{ serveur_forgejo_data }}/.admin-created"
|
|
become: true
|
|
become_user: "{{ serveur_forgejo_utilisateur }}"
|
|
environment:
|
|
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
|
|
no_log: true
|
|
|
|
- name: Configurer la source OAuth2 SSO (Keycloak, idempotent)
|
|
ansible.builtin.shell:
|
|
executable: /bin/bash
|
|
cmd: |
|
|
set -euo pipefail
|
|
FJ="{{ serveur_forgejo_binaire }} --config {{ serveur_forgejo_config }}"
|
|
# Les reglages de GROUPE sont RECONCILIES, pas seulement poses a la creation :
|
|
# `add-oauth` une fois puis plus rien laissait un cablage perime vivre
|
|
# indefiniment — c'est ce qui est arrive a la federation LDAP de Keycloak, qui
|
|
# a pointe un hote inexistant pendant des semaines (2026-08-07).
|
|
OPTS_GROUPE=""
|
|
{% if serveur_forgejo_oidc_groupe_admin | length > 0 %}
|
|
OPTS_GROUPE="--group-claim-name {{ serveur_forgejo_oidc_groupe_claim }} --admin-group {{ serveur_forgejo_oidc_groupe_admin }}"
|
|
{% endif %}
|
|
ID=$($FJ admin auth list 2>/dev/null \
|
|
| awk -v n="{{ serveur_forgejo_oidc_nom }}" '$2 == n {print $1}' | head -1)
|
|
if [ -z "$ID" ]; then
|
|
$FJ admin auth add-oauth \
|
|
--name "{{ serveur_forgejo_oidc_nom }}" \
|
|
--provider openidConnect \
|
|
--key "{{ serveur_forgejo_oidc_client_id }}" \
|
|
--secret "$OIDC_SECRET" \
|
|
--auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \
|
|
$OPTS_GROUPE >/dev/null
|
|
echo SETOPS_CHANGED
|
|
else
|
|
# `update-oauth` est idempotent cote Forgejo : il reecrit les memes valeurs
|
|
# sans effet de bord. On ne peut pas comparer avant/apres (la CLI n'expose
|
|
# pas le detail d'une source), donc on ne signale PAS `changed`.
|
|
# `--secret` fait partie de la reconciliation : sans lui, faire tourner
|
|
# `vault_forgejo_oidc` laissait Forgejo sur l'ancienne valeur.
|
|
$FJ admin auth update-oauth --id "$ID" \
|
|
--auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \
|
|
--secret "$OIDC_SECRET" \
|
|
$OPTS_GROUPE >/dev/null
|
|
echo SETOPS_OK
|
|
fi
|
|
environment:
|
|
OIDC_SECRET: "{{ serveur_forgejo_oidc_client_secret }}"
|
|
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
|
|
become: true
|
|
become_user: "{{ serveur_forgejo_utilisateur }}"
|
|
register: serveur_forgejo_oauth
|
|
changed_when: "'SETOPS_CHANGED' in serveur_forgejo_oauth.stdout"
|
|
no_log: true
|
|
when:
|
|
- serveur_forgejo_oidc_actif | bool
|
|
- not ansible_check_mode
|
|
|
|
# --- ROTATION DU COMPTE DE SECOURS -------------------------------------------
|
|
# `admin user create` porte `creates: .admin-created` : il ne tourne qu'UNE fois.
|
|
# Changer la voute ne changeait donc rien, et la voute se mettait a mentir. Meme
|
|
# raisonnement que pour Grafana — idempotence par EMPREINTE du secret applique.
|
|
- name: Empreinte du mot de passe admin en place
|
|
ansible.builtin.slurp:
|
|
src: "{{ serveur_forgejo_marqueur_admin }}"
|
|
register: serveur_forgejo_marque
|
|
failed_when: false
|
|
changed_when: false
|
|
no_log: true
|
|
|
|
- name: Appliquer le mot de passe du compte de secours (rotation)
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "{{ serveur_forgejo_binaire }}"
|
|
- "--config"
|
|
- "{{ serveur_forgejo_config }}"
|
|
- admin
|
|
- user
|
|
- change-password
|
|
- "--username"
|
|
- "{{ serveur_forgejo_admin_user }}"
|
|
- "--password"
|
|
- "{{ serveur_forgejo_admin_password }}"
|
|
environment:
|
|
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
|
|
become: true
|
|
become_user: "{{ serveur_forgejo_utilisateur }}"
|
|
changed_when: true
|
|
no_log: true
|
|
when: >-
|
|
(serveur_forgejo_marque.content | default('') | b64decode | trim)
|
|
!= (serveur_forgejo_admin_password | hash('sha256'))
|
|
|
|
- name: Retenir l'empreinte appliquee
|
|
ansible.builtin.copy:
|
|
content: "{{ serveur_forgejo_admin_password | hash('sha256') }}\n"
|
|
dest: "{{ serveur_forgejo_marqueur_admin }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
no_log: true
|