diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d4ec9..2ad788e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # CHANGELOG — Set-OPS +## 2026-08-11 — Forgejo à 16.0.2, et une ancre de confiance qui existe vraiment + +Six versions majeures d'un coup — mais la découverte importante est ailleurs. + +### La « rotation de clé » n'en était pas une + +Quatre versions, trois signataires différents : + +``` +10.0.0 → B3B1F60AC577F2A2 14.0.0 → C4186DF66F4B6750 +12.0.0 → D0A820050E1609E5 16.0.2 → C4186DF66F4B6750 +``` + +Ce ne sont pas des clés distinctes : ce sont des **sous-clés de signature** sous une **clé +primaire stable depuis 2022** — `EB114F5E…C5923710`, `Forgejo `. La +sous-clé `0F527CF9…0E1609E5` est bien celle qui avait signé la 12.0.0. + +**D'où une correction du vérificateur** : il comparait l'empreinte du *signataire*, donc une +sous-clé. Il aurait échoué à chaque rotation légitime — et on aurait appris à lever la garde +pour avancer, ce qui est la pire chose qui puisse arriver à un contrôle. Il accepte désormais +la **clé primaire** (dernier champ de `VALIDSIG`), qui survit aux rotations tout en refusant +une clé étrangère. + +### 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 donc **indépendante du canal de livraison**, exactement ce qui +manquait pour Keycloak. Forgejo publie en outre une somme `sha256`, vérifiée conforme. + +Le projet annonce lui-même la rotation : *« the GPG key is updated on a regular basis »* — +ce qui confirme qu'épingler la primaire est le bon choix. + +### Vérifié, dans les deux sens + +Nominal `0`. Binaire altéré d'un octet `1`. Empreinte de Keycloak appliquée à Forgejo `1`. +Signature d'un *autre* artefact `1`. Et Keycloak ne régresse pas après la modification du +comparateur. + +`make versions-mesurer` : **0 en retard**. Rôle appliqué de bout en bout sur `forge-01`. + +Rappel du modèle : `forge-01` tourne toujours 10.0.0 — l'épinglage décrit ce qu'on +**installe**, pas ce qui **tourne**. + + ## 2026-08-10 — Keycloak : vérifier QUI a produit l'archive, pas seulement qu'elle est intacte L'exploitant : *« j'ai besoin d'une confiance réelle. Keycloak est probablement l'élément le diff --git a/roles/serveur_forgejo/defaults/main.yml b/roles/serveur_forgejo/defaults/main.yml index 1a000c6..3bd2ba4 100644 --- a/roles/serveur_forgejo/defaults/main.yml +++ b/roles/serveur_forgejo/defaults/main.yml @@ -1,6 +1,6 @@ --- # Version Forgejo : verifier/ajuster sur https://forgejo.org/download/ -serveur_forgejo_version: "10.0.0" +serveur_forgejo_version: "16.0.2" serveur_forgejo_url: >- https://codeberg.org/forgejo/forgejo/releases/download/v{{ serveur_forgejo_version }}/forgejo-{{ serveur_forgejo_version }}-linux-amd64 @@ -90,3 +90,18 @@ serveur_forgejo_marqueur_admin: "/etc/forgejo/.setops-admin-sha256" # puis pousse par SSH : aucun flux nouveau, et l'artefact devient deployable hors ligne. # Surchargeable par `setops_cache_artefacts` pour partager un cache commun. serveur_forgejo_cache_local: "{{ setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops') }}" + +# SIGNATURE DU BINAIRE — Forgejo signe ses releases (PGP detachee, `.asc`) ET publie une +# somme sha256. Les deux ont ete verifiees avant epinglage. +# +# ON EPINGLE LA CLE PRIMAIRE, PAS LA SOUS-CLE QUI SIGNE. Forgejo fait tourner ses +# sous-cles de signature — 0F527CF9... pour la 12.0.0, 3BF4E813... pour la 16.0.2 — sous +# une primaire stable depuis 2022, et le dit explicitement sur sa page de telechargement +# (« the GPG key is updated on a regular basis »). Epingler la sous-cle ferait echouer la +# verification a chaque rotation LEGITIME, et on apprendrait a lever la garde pour +# avancer. La primaire survit aux rotations et refuse quand meme une cle etrangere. +# +# Cette empreinte a une SOURCE INDEPENDANTE du telechargement : forgejo.org/download la +# publie, et le binaire vient de codeberg.org. C'est ce qui manque a Keycloak. +serveur_forgejo_signature_empreinte: "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710" +serveur_forgejo_signature_url: "{{ serveur_forgejo_url }}.asc" diff --git a/roles/serveur_forgejo/files/forgejo-release.asc b/roles/serveur_forgejo/files/forgejo-release.asc new file mode 100644 index 0000000..7c7685d --- /dev/null +++ b/roles/serveur_forgejo/files/forgejo-release.asc @@ -0,0 +1,68 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEY3T/yhYJKwYBBAHaRw8BAQdAVxqCQrSbpDNrx8CiTM8PUAVqdCyv2UmBDhpP +HZIpoIC0HUZvcmdlam8gPGNvbnRhY3RAZm9yZ2Vqby5vcmc+iQE+BBMWCgDmAhsD +BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAhkBFiEE6xFPXmwNwrzdGDVQpLYaLcWS +NxAFAmSc08VBFIAAAAAAEAAocHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vY29kZWJl +cmcub3JnL2Zvcmdlam8vZ2l0ZWFfcHJvb2Y2FIAAAAAAEAAdcHJvb2ZAYXJpYWRu +ZS5pZGh0dHBzOi8vZmxvc3Muc29jaWFsL0Bmb3JnZWpvMRSAAAAAABAAGHByb29m +QGFyaWFkbmUuaWRkbnM6Zm9yZ2Vqby5vcmc/dHlwZT1UWFQACgkQpLYaLcWSNxDM +2wEA6bOel3R25z3YUXL4hI2S8jRkJbOQawq0vgUnYNgS9hcBAK2zq4Zt4ctvSB+x +TqhR6Zi6aqSD3QrRnUVvV1xZhdkEiQFABBMWCgDoAhsDBQsJCAcDBRUKCQgLBRYC +AwEAAh4BAheAAhkBFiEE6xFPXmwNwrzdGDVQpLYaLcWSNxAFAmScz7JDFIAAAAAA +EAAqcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vY29kZWJlcmcub3JnL2Zvcmdlam8v +Zm9yZ2Vqb19wcm9vZjYUgAAAAAAQAB1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9m +bG9zcy5zb2NpYWwvQGZvcmdlam8xFIAAAAAAEAAYcHJvb2ZAYXJpYWRuZS5pZGRu +czpmb3JnZWpvLm9yZz90eXBlPVRYVAAKCRCkthotxZI3EBJ1AP9UeN1HFGz90r34 +PGrOj1225HfJzdWgamEUkEKEwShcIQD+K/o7sLJM+C/mJXaCixAZgvRd9/rYq27T +9Y2rTQybSwmJAT4EExYKAOYCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4ACGQEW +IQTrEU9ebA3CvN0YNVCkthotxZI3EAUCY3spkjYUgAAAAAAQAB1wcm9vZkBhcmlh +ZG5lLmlkaHR0cHM6Ly9mbG9zcy5zb2NpYWwvQGZvcmdlam8xFIAAAAAAEAAYcHJv +b2ZAYXJpYWRuZS5pZGRuczpmb3JnZWpvLm9yZz90eXBlPVRYVEEUgAAAAAAQAChw +cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvZm9yZ2Vqby9naXRl +YV9wcm9vZgAKCRCkthotxZI3EC+hAQCxsUupyqMChFMqk/74PqzSlmCd6RXtbezP +W66YostSbgD9G+N+c9or1cBkcSejREOSy7TFrDrvek+ZhdZVhCp1UAKJAQcEExYK +AK8CGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4ACGQEWIQTrEU9ebA3CvN0YNVCk +thotxZI3EAUCY3inA0EUgAAAAAAQAChwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9j +b2RlYmVyZy5vcmcvZm9yZ2Vqby9naXRlYV9wcm9vZjEUgAAAAAAQABhwcm9vZkBh +cmlhZG5lLmlkZG5zOmZvcmdlam8ub3JnP3R5cGU9VFhUAAoJEKS2Gi3FkjcQNV8B +AJfcHBzskW78lJVJeQwCI70qorOhC/QUp80jjNzs5lO7AQCujYgrae3vZb/476sc +wM33ufSpBguNwTLbCI6C5g8+B7QmRm9yZ2VqbyBSZWxlYXNlcyA8cmVsZWFzZUBm +b3JnZWpvLm9yZz6IkAQTFgoAOAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgBYh +BOsRT15sDcK83Rg1UKS2Gi3FkjcQBQJjeKH0AAoJEKS2Gi3FkjcQC5YBAKwCGFDD +SpX0JwBrzIP8W8ElwHvdBz2XDg8LwyQgr722AP9r01rbFwY4axDxpNj+BUFxwD5F +hza1cE3932eTsSOPDoiQBBMWCAA4FiEE6xFPXmwNwrzdGDVQpLYaLcWSNxAFAmN4 +k+kCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQpLYaLcWSNxBHrQD+PFo/ +ii8p72HP0KsJbHPGnS/Sk9pFGd16fs1Hd88JHj0A/10XNyGQgwbe+X+K/a97vWW8 +vAzA1EtFIMfCGhIO8EoIuDMEY3UANBYJKwYBBAHaRw8BAQdAKvAs2Ij2RamYUzz4 +sBgsc2J+4fEwvSMcTp6rPZizRheI9QQYFggAJhYhBOsRT15sDcK83Rg1UKS2Gi3F +kjcQBQJjdQA0AhsCBQkB4TOAAIEJEKS2Gi3FkjcQdiAEGRYIAB0WIQT3y/AglOdm +XhftbETjgb8+UNU3BwUCY3UANAAKCRDjgb8+UNU3B0iWAP4rXme4R5z5VMV0/thj +dLEzxGODYS1eTTTv4sztdB832wEA4uMFPMh1poo6mgCQZD1OH8imFJQ6c9hlU7lr +pw37AAzjnwD+K1/e6tB6EjlJsq1tGogOGpBlBUBwR4bZ5AW6e6h+MNYBAO3+ATle +BOJCCeRNyoBfsZB4Tvg79Dm8O1fXQ2NvNXsPuDMEZ45q/RYJKwYBBAHaRw8BAQdA +jYlceMjHbmD1Zkc3KOR1SJEE+XljUmOMkI+9czT/1GOI9QQYFggAJhYhBOsRT15s +DcK83Rg1UKS2Gi3FkjcQBQJnjmr9AhsCBQkDtTgAAIEJEKS2Gi3FkjcQdiAEGRYI +AB0WIQQPUnz5Oj0NCSXTxV7QqCAFDhYJ5QUCZ45q/QAKCRDQqCAFDhYJ5Y1MAPsE +o1P5pOMRiJcMyXT55zcyLIjHqrbmKJG0qziMVyIT9QD/d6rVa/pDrt6Q7PSQvvAm +BtAytUA9S5pRj+fVkRnr0glwoQEA8ukgn533K+T+H2Gs9OPk5dAg1JBThj/rFUv4 +rD07LGwA/R9oSiShJsLgHdjHhQNwaN8+a1FVpHwHJ7+zsu7vjlkHuDMEZXx54RYJ +KwYBBAHaRw8BAQdAxqCjzzjse656k99fHspFJDCW5PNzIXAaq3nDgAzt4tqI9QQY +FggAJhYhBOsRT15sDcK83Rg1UKS2Gi3FkjcQBQJlfHnhAhsCBQkCx+oAAIEJEKS2 +Gi3FkjcQdiAEGRYIAB0WIQTfMxnqNtWZwdSmg9SzsfYKxXfyogUCZXx54QAKCRCz +sfYKxXfyoocRAP48OQpiBTkwC7kLLyuqVlP1t0hBQddr4i4rIV8Ug5tHzwEApl8Q ++S4k/ROQS5FOhy7GBC337SncJFJYDD0pTcSecw+J3AEAn4qax72Oyfb0vaPYm+Wd +qsfVBd2Hd2vJIwCjglp55B0BALIJE6nvACcKzTRUj7AQSLGvELGfJfM320xXE0ZV +BZkKuDMEaUFtxRYJKwYBBAHaRw8BAQdA1AZR9j6PksmArHjaoqOHIOQ4H1iw0KZy +LKLgu1onR8eI9QQYFggAJhYhBOsRT15sDcK83Rg1UKS2Gi3FkjcQBQJpQW3FAhsC +BQkCx+oAAIEJEKS2Gi3FkjcQdiAEGRYIAB0WIQQ79OgT+EgSQR2gHlvEGG32b0tn +UAUCaUFtxQAKCRDEGG32b0tnUNAcAP0RQQzbe0klRp7QvUNsadYSncadk5Abdnfi +5a/XrBFXcQD6Am4dr1mvVNcARdcHBDNkisAVvL7sjmyZY9UzzhzvFgD+oQD/Z1Aw +xlxF6UULR8g+8ZAwZ9h4GvxuQcqUkFA5N26R5I8BAJx32uolPs01mU+0nVPA0B8B +rcvbdt2n90S0qET53T0IuDgEY3T/yhIKKwYBBAGXVQEFAQEHQJmdF8gfFNWE4XyR +2Ft7hgmx4/JUWLxRJ+4NfLgeAgdGAwEIB4h4BBgWCAAgFiEE6xFPXmwNwrzdGDVQ +pLYaLcWSNxAFAmN0/8oCGwwACgkQpLYaLcWSNxB2ugD+McWq3vL+v/nN0rhNhin7 +mOU6Azlkjuk2FaG0gVuqbH0BAIpIl/ZiA8Es0lIv/zq+kT1voi9MT/wQ1H/vFhog +DugC +=Dld6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/roles/serveur_forgejo/tasks/main.yml b/roles/serveur_forgejo/tasks/main.yml index 68eabfe..2fca1d3 100644 --- a/roles/serveur_forgejo/tasks/main.yml +++ b/roles/serveur_forgejo/tasks/main.yml @@ -97,6 +97,39 @@ - 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 }}" diff --git a/scripts/verifier_signature.py b/scripts/verifier_signature.py index c1a891b..5b0b0a9 100644 --- a/scripts/verifier_signature.py +++ b/scripts/verifier_signature.py @@ -80,14 +80,29 @@ def verifier(artefact: Path, signature: Path, cle: Path, empreinte: str) -> tupl # La signature est bonne — mais par QUELLE cle ? `VALIDSIG` porte l'empreinte # reelle du signataire ; s'en remettre a « bonne signature » sans la lire # laisserait passer une signature valide faite par une autre cle du trousseau. - vs = re.search(r"\[GNUPG:\] VALIDSIG ([0-9A-F]{40})", etat) - if not vs: + # `VALIDSIG` porte l'empreinte de la SOUS-CLE qui a signe, et — en dernier champ — + # celle de la cle PRIMAIRE. Les deux sont acceptees, et c'est essentiel : + # + # Forgejo fait tourner ses sous-cles de signature (0F527CF9… pour la 12.0.0, + # 3BF4E813… pour la 16.0.2) sous une cle primaire stable depuis 2022, et le dit + # explicitement — « the GPG key is updated on a regular basis ». Epingler la + # sous-cle ferait echouer la verification a chaque rotation legitime, et on + # apprendrait a lever la garde pour avancer. **Epingler la PRIMAIRE survit aux + # rotations tout en refusant une cle etrangere.** + vs = re.search(r"\[GNUPG:\] VALIDSIG ([0-9A-F]{40})(?:\s+\S+)*?\s+([0-9A-F]{40})\s*$", + etat, re.M) + simple = re.search(r"\[GNUPG:\] VALIDSIG ([0-9A-F]{40})", etat) + if not simple: return False, "signature acceptee mais sans empreinte lisible — refuse" - if vs.group(1) != empreinte: + signataire = simple.group(1) + primaire = vs.group(2) if vs else signataire + if empreinte not in (signataire, primaire): return False, (f"signee par une AUTRE cle que celle epinglee.\n" - f" epinglee : {empreinte}\n" - f" signataire : {vs.group(1)}") - return True, f"signature valide, faite par {empreinte}" + f" epinglee : {empreinte}\n" + f" signataire : {signataire}\n" + f" primaire : {primaire}") + via = "" if signataire == empreinte else f" (sous-cle {signataire})" + return True, f"signature valide, faite par {empreinte}{via}" def main(argv: list[str] | None = None) -> int: