Set-OPS-Public/roles/serveur_forgejo/tasks/main.yml
Daniel Allaire e9b9e9b7ea forgejo : epingle 16.0.2, et le verificateur accepte la cle PRIMAIRE
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>
2026-08-11 01:04:16 -04:00

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