Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Forgejo : sécuriser webhooks, force brute et commits

Par mis à jour le

Améliorer cette page

Pour sécuriser une instance Forgejo 15 LTS, on resserre la liste des hôtes que les webhooks peuvent joindre, on bannit la force brute avec fail2ban, on garde un compte administrateur de secours, on limite la portée des jetons et on fait signer les commits. Aux essais, Forgejo 15.0.10 n'a montré ni limite de tentatives de connexion ni journal d'audit dédié : la leçon montre ce qui existe, puis ce que l'hôte doit apporter. Elle s'adresse à l'administrateur dont la forge tourne déjà, avec ses comptes et son fournisseur d'identité, et qui veut la rendre résistante avant d'y brancher des équipes.

Vous repartez avec cinq preuves : des webhooks vers des adresses internes refusés à la livraison, une tentative de force brute bannie puis rétablie, un compte de secours qui se connecte malgré une source d'identité externe, un jeton à portée limitée qui échoue là où il doit échouer, et des commits signés par l'instance puis par un utilisateur. Les commandes ont été exécutées sur Forgejo 15.0.10, AlmaLinux 10 avec SELinux en mode enforcing, avec la pile de la leçon Installer Forgejo LTS.

  • Restreindre les hôtes que les webhooks peuvent appeler, avec ALLOWED_HOST_LIST.
  • Bannir les tentatives de connexion répétées avec fail2ban, derrière Docker et Caddy.
  • Garder un compte administrateur de secours qui survit à une panne du fournisseur d'identité.
  • Limiter un jeton d'accès par ses portées et constater ce qu'il refuse.
  • Signer les commits créés par l'instance, et vérifier ceux d'un utilisateur.
  • Compenser l'absence de journal d'audit par la journalisation de l'hôte.
  • Suivre les avis de sécurité de Forgejo et les correctifs de la branche 15 LTS.

Il vous faut la forge des leçons précédentes : Forgejo 15 LTS derrière Caddy dans /opt/forgejo-lab, le nom forge.lab.test et le compte fjadmin (installation), avec la liste COMPOSE_FILE du .env et les fichiers Compose de surcharge expliqués dans Configuration initiale. La connexion par fournisseur d'identité et le second facteur obligatoire sont ceux de Brancher Forgejo sur OIDC, et la révocation d'un jeton celle de Assister les utilisateurs. Le réglage GLOBAL_TWO_FACTOR_REQUIREMENT doit être revenu à none, comme la leçon OIDC le fait en fin de parcours : sinon le jeton de fjadmin et les appels en Basic répondent 403.

Vous devez savoir lire un code HTTP avec curl, éditer un fichier en root avec sudo, et comprendre ce qu'est une règle de pare-feu. Il faut aussi deux machines : la VM qui héberge la forge et un poste distinct, parce que la preuve de bannissement exige une adresse cliente qui ne soit pas celle de l'hôte. Les secrets restent dans des variables du shell, jamais écrits en clair dans une commande. Pour comprendre pourquoi tant de réglages tournent autour du droit minimal, le principe est celui du moindre privilège.

Préparez le shell de la VM : le nom de la forge, l'adresse de l'hôte (celle du lab, à remplacer par la vôtre), un jeton d'administrateur (un jeton d'accès est un secret que l'API accepte à la place du mot de passe), et deux fonctions qui servent à chaque section.

Fenêtre de terminal
D=forge.lab.test
H=10.10.110.11
fj() { sudo docker exec -u git forgejo forgejo "$@"; }
ATK=$(fj admin user generate-access-token --username fjadmin --token-name "secu-$$" --scopes all --raw)
api() { curl -sk -H "Authorization: token $ATK" -H 'Content-Type: application/json' "$@"; }
attendre() { for i in $(seq 1 60); do c=$(curl -sk -o /dev/null -w '%{http_code}' https://$D/user/login); [ "$c" = 200 ] && { echo "prêt"; return; }; sleep 2; done; echo "pas prêt (code $c)"; }
reglage() { cd /opt/forgejo-lab && sudo sed -i "/^$1=/d" .env && echo "$1=$2" | sudo tee -a .env >/dev/null && sudo docker compose up -d server >/dev/null 2>&1 && attendre; }

fj exécute forgejo en tant qu'utilisateur git dans le conteneur, api appelle l'API avec le jeton, attendre interroge la page de connexion jusqu'au premier 200, et reglage écrit une variable dans le .env puis relance le service. Le périmètre all du jeton ATK ne convient qu'à un lab jetable : la section sur les jetons montre pourquoi. L'option -k de curl ignore le certificat, acceptable parce que l'autorité est celle du lab.

Une forge concentre du code, des secrets de CI et des droits de déploiement : six surfaces méritent un réglage explicite, et chacune a sa section. Le tableau les relie à leur risque et à leur parade. Il sert de table de décision pour la leçon : si vous ne savez pas par où commencer, lisez la colonne « Risque » et choisissez celle qui vous inquiète le plus.

SurfaceRisqueParade dans cette leçon
Webhooksla forge est amenée à appeler un service interne (SSRF, définie juste après)[webhook] ALLOWED_HOST_LIST
Page de connexion et APIessais de mots de passe en massefail2ban sur le journal
Comptes administrateurspanne d'accès, ou trop de comptes puissantscompte de secours, liste courte
Jetons d'accèsun jeton volé ouvre tous les droits de son propriétaireportées minimales
Commitsun commit dont on ne peut pas prouver l'auteursignature par l'instance ou l'utilisateur
Traçabilitéaucune trace de qui a fait quoijournalisation de l'hôte

Un mot sur la première ligne. Une SSRF (Server-Side Request Forgery, falsification de requête côté serveur) est une faille où un attaquant fait envoyer par le serveur une requête vers une cible que lui-même ne peut pas atteindre, par exemple un service qui n'écoute que sur le réseau interne. Un webhook, c'est exactement cela : l'utilisateur fournit une URL, et Forgejo l'appelle. Le classement OWASP Top 10 2025 situe cette famille de failles dans sa catégorie de contrôle d'accès défaillant.

Comment empêcher un webhook d'atteindre le réseau interne ?

Section intitulée « Comment empêcher un webhook d'atteindre le réseau interne ? »

Le réglage [webhook] ALLOWED_HOST_LIST liste les adresses que les webhooks peuvent appeler ; sa valeur par défaut est external, qui exclut le réseau interne. Un webhook vers une adresse refusée est quand même accepté à la création (HTTP 201) et n'échoue qu'à la livraison, c'est-à-dire à l'envoi effectif de la requête vers l'URL, avec un message dans le journal. La feuille de configuration de Forgejo v15 définit les valeurs loopback (127.0.0.0/8 et ::1, l'hôte lui-même), private (les plages privées : RFC 1918, RFC 4193 et 100.64.0.0/10), external (toute adresse unicast qui n'est ni privée ni de bouclage, donc une adresse publique ordinaire), * (tout) et des listes d'adresses ou de CIDR (une plage écrite 172.30.0.0/24) séparées par des virgules. Ne mettez jamais * : ce serait rouvrir toute la porte.

Sans aucun réglage, c'est donc la valeur par défaut qui s'applique, et nous l'avons rejouée : un webhook vers 127.0.0.1 est créé (201), puis sa livraison est refusée. La preuve détaillée porte ensuite sur une liste resserrée, le réglage à viser quand un webhook doit joindre un service interne précis. Commencez par un dépôt de test (l'option auto_init y crée un premier commit), et vérifiez que app.ini ne porte aucune section [webhook].

Fenêtre de terminal
api -X POST https://$D/api/v1/user/repos -d '{"name":"whk","auto_init":true,"default_branch":"main"}' -o /dev/null -w 'HTTP %{http_code}\n'
sudo docker exec forgejo sh -c "grep -n -A3 '^\[webhook\]' /data/gitea/conf/app.ini || echo '(pas de section [webhook] : valeur par défaut du binaire)'"

La première commande répond HTTP 201, la seconde affiche (pas de section [webhook] : valeur par défaut du binaire). Il faut maintenant une cible : un petit serveur HTTP qui note l'adresse de chaque appelant. Enregistrez ce script sous listener.py sur la VM et lancez-le en arrière-plan.

import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get('Content-Length', 0))
self.rfile.read(n)
open('/tmp/listener.log', 'a').write('POST recu de %s\n' % self.client_address[0])
self.send_response(200)
self.end_headers()
self.wfile.write(b'ok')
def log_message(self, *a):
pass
http.server.HTTPServer(('0.0.0.0', 9000), H).serve_forever()
Fenêtre de terminal
: > /tmp/listener.log
setsid nohup python3 listener.py >/dev/null 2>&1 < /dev/null &

Le serveur écoute sur le port 9000 de l'hôte et répond 200 ok à tout POST. Ajoutez maintenant le fichier de surcharge qui porte le réglage. La valeur vient d'une variable du .env, pour changer de liste sans réécrire le fichier. Créez /opt/forgejo-lab/webhooks.yaml :

services:
server:
environment:
FORGEJO__webhook__ALLOWED_HOST_LIST: ${HOOK_HOSTS:-external}

Ce fichier applique la convention des leçons précédentes : une variable FORGEJO__section__CLE que l'image recopie dans app.ini au démarrage. Le grep ci-dessous le vérifie : la variable atterrit bien dans la section [webhook]. Le piège est que reglage masque les erreurs de docker compose : si webhooks.yaml manque alors qu'il figure dans COMPOSE_FILE, la commande affiche quand même prêt avec l'ancien conteneur. Créez donc le fichier avant la ligne sed. Posez la première liste, qui ne contient que l'hôte.

Fenêtre de terminal
cd /opt/forgejo-lab
sudo sed -i 's#^COMPOSE_FILE=.*#&:webhooks.yaml#' .env
reglage HOOK_HOSTS $H
sudo docker exec forgejo grep -A2 '^\[webhook\]' /data/gitea/conf/app.ini

La dernière commande affiche [webhook] puis ALLOWED_HOST_LIST = 10.10.110.11 (votre adresse). Créez maintenant quatre webhooks vers quatre cibles : le port 3000 de Forgejo lui-même sur 127.0.0.1, l'hôte, la passerelle du réseau Docker de la pile (172.30.0.1, premier hôte du sous-réseau 172.30.0.0/24) et une adresse privée quelconque (192.168.99.9). La fonction creer crée chaque webhook, affiche le code de création et déclenche un envoi de test.

Fenêtre de terminal
creer() {
c=$(curl -sk -o /tmp/hook.json -w '%{http_code}' -H "Authorization: token $ATK" -H 'Content-Type: application/json' -X POST https://$D/api/v1/repos/fjadmin/whk/hooks -d "{\"type\":\"forgejo\",\"active\":true,\"events\":[\"push\"],\"config\":{\"url\":\"$1\",\"content_type\":\"json\"}}")
id=$(python3 -c 'import json;print(json.load(open("/tmp/hook.json"))["id"])')
api -X POST https://$D/api/v1/repos/fjadmin/whk/hooks/$id/tests >/dev/null
echo "$1 -> création HTTP $c (webhook $id)"
}
for u in http://127.0.0.1:3000/ http://$H:9000/ http://172.30.0.1:9000/ http://192.168.99.9:9000/; do creer $u; done
sleep 8

Les quatre créations répondent HTTP 201, y compris celles qui seront refusées : à la création, Forgejo ne contrôle pas la cible. Avec la valeur par défaut external, nous avons observé la même chose, hôte compris puisque 10.10.110.11 est une adresse privée : 201, puis is_succeed = f avec le même message de refus. Le contrôle a lieu à la livraison, que la table hook_task de PostgreSQL enregistre.

Fenêtre de terminal
sudo docker exec forgejo-db psql -U forgejo -d forgejo -c "select left(w.url,32) as url, t.is_delivered, t.is_succeed, left(replace(t.response_content,E'\n',' '),300) as reponse from hook_task t join webhook w on w.id=t.hook_id order by w.id"
cat /tmp/listener.log
sudo docker logs forgejo --since 40s 2>&1 | grep -i 'webhook' | cut -c1-260

Seule la ligne de l'hôte porte is_succeed = t et une réponse "status":200. Les trois autres portent is_succeed = f et la réponse webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '127.0.0.1(127.0.0.1:3000)', avec l'adresse refusée entre apostrophes. Le fichier du serveur ne contient qu'une ligne, POST recu de 172.30.0.3 : c'est le conteneur Forgejo, qui a joint l'hôte. Le journal de Forgejo porte, pour chaque refus, une ligne [E] Unable to deliver webhook task[…] qui répète le même message.

Resserrez maintenant autrement : la liste passe à 172.30.0.0/24,loopback.

Fenêtre de terminal
reglage HOOK_HOSTS 172.30.0.0/24,loopback
sudo docker exec forgejo grep -A2 '^\[webhook\]' /data/gitea/conf/app.ini

Supprimez d'abord les webhooks précédents, sinon les anciens se redéclenchent aussi, puis recréez les quatre avec creer et relisez hook_task : le tableau montre que la liste décide seule de ce qui passe.

Fenêtre de terminal
for id in $(api https://$D/api/v1/repos/fjadmin/whk/hooks | python3 -c 'import json,sys;print(*[h["id"] for h in json.load(sys.stdin)])'); do api -X DELETE https://$D/api/v1/repos/fjadmin/whk/hooks/$id -o /dev/null -w "suppression $id -> HTTP %{http_code}\n"; done

Chaque suppression répond HTTP 204.

CibleListe = hôteListe = 172.30.0.0/24,loopback
http://127.0.0.1:3000/refuséelivrée, réponse 405 de Forgejo lui-même
http://10.10.110.11:9000/livrée (200)refusée
http://172.30.0.1:9000/refuséelivrée (200)
http://192.168.99.9:9000/refuséerefusée

La ligne 405 est la plus instructive. Avec loopback dans la liste, le webhook atteint le port interne de Forgejo : le serveur a répondu 405 Method Not Allowed parce qu'il n'accepte pas de POST sur cette URL, mais il a bien répondu. C'est le chemin d'une SSRF vers un service qui ne devrait jamais être joignable. Le piège est donc de dépanner un webhook refusé en ajoutant loopback ou private : listez l'adresse ou le CIDR précis du service voulu, rien de plus.

Pour revenir au défaut du lab, reglage HOOK_HOSTS external suffit : la variable garde la valeur du :-external du fichier. Supprimez aussi les webhooks de test (voir la section sur le nettoyage).

Pourquoi Forgejo ne bloque-t-il pas la force brute lui-même ?

Section intitulée « Pourquoi Forgejo ne bloque-t-il pas la force brute lui-même ? »

Dans Forgejo 15.0.10, aucune limite de tentatives de connexion n'a pu être démontrée : le seul réglage voisin est le captcha à la connexion, qui n'est pas un verrouillage. La feuille de configuration de la v15 liste REQUIRE_CAPTCHA_FOR_LOGIN (à false par défaut, avec ENABLE_CAPTCHA requis). Nous ne l'avons pas rejoué. La parade éprouvée est donc externe : lire le journal de Forgejo et bannir l'adresse source au pare-feu, ce que fait fail2ban, un démon qui surveille des journaux et bloque les adresses qui échouent trop souvent. Son fonctionnement général (la prison, ou jail, qui associe un filtre, un seuil et une action de blocage ; les délais) est expliqué dans le guide Fail2ban ; cette section ne traite que ce qui est propre à Forgejo derrière Docker et Caddy.

Forgejo écrit deux sortes de lignes d'échec. Un échec de connexion web laisse une ligne [W] explicite. Un échec API en Basic (nom et mot de passe) ne laisse, lui, que la ligne du routeur, avec le code 401. Voici ce que le journal contenait après trois essais ratés depuis un poste.

SignInPost() [W] Failed authentication attempt for fjadmin from 10.10.110.1:0: user's password is invalid [uid: 1, name: fjadmin]
router: completed GET /api/v1/user for 10.10.110.1:0, 401 Unauthorized in 86.0ms @ shared/middleware.go:63(shared.Middlewares.apiAuthentication)

Les deux lignes portent l'adresse du client suivie de :0, et c'est elle que fail2ban doit extraire. Cette adresse est la vraie, et non celle de Caddy, grâce à REVERSE_PROXY_TRUSTED_PROXIES, que la leçon d'installation a fixé sur l'adresse du proxy. Le piège : un essai lancé depuis l'hôte lui-même est vu comme venant de 172.30.0.1, la passerelle Docker. Testez toujours depuis un autre poste.

Comment lire le journal de Forgejo depuis fail2ban ?

Section intitulée « Comment lire le journal de Forgejo depuis fail2ban ? »

On bascule le pilote de journalisation du conteneur sur journald, et la prison fail2ban lit le journal systemd en filtrant sur le nom du conteneur. Le pilote décide où Docker envoie la sortie d'un conteneur (par défaut un fichier JSON par conteneur, json-file) ; journald est le service de journal de systemd, qu'on lit avec journalctl. Nous avons d'abord essayé de lire le fichier de journal du volume Docker, puis un relais docker logs vers /var/log. Le pilote journald est plus propre : pas de relais à superviser, et aucun refus SELinux relevé (la recherche ausearch -m avc est restée vide). Installez fail2ban, qui vient du dépôt EPEL sur AlmaLinux 10.

Fenêtre de terminal
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client --version | head -1

La dernière commande affiche Fail2Ban v1.1.0. Créez ensuite /opt/forgejo-lab/journald.yaml, qui change le pilote du seul service server. Il s'ajoute à COMPOSE_FILE comme les autres surcharges : le lab a joué ce contenu sous le nom compose.override.yaml, sur une pile sans COMPOSE_FILE, mais dès que cette variable existe, Compose ne charge plus compose.override.yaml tout seul. Le fichier nommé et listé évite ce piège et ne casse pas la surcharge Keycloak de la leçon OIDC.

services:
server:
logging:
driver: journald
options:
tag: forgejo
Fenêtre de terminal
cd /opt/forgejo-lab
sudo sed -i 's#^COMPOSE_FILE=.*#&:journald.yaml#' .env
sudo docker compose config --quiet && echo "compose valide"
sudo docker compose up -d server
attendre
sudo docker inspect forgejo --format 'log driver: {{.HostConfig.LogConfig.Type}}'
sudo journalctl CONTAINER_NAME=forgejo -n 3 --no-pager -o short-iso | cut -c1-200

Vous lisez compose valide, puis log driver: journald, puis trois lignes de Forgejo dans le journal. Ce qui compte est le champ CONTAINER_NAME=forgejo que le pilote ajoute à chaque entrée : la prison s'en sert pour ne lire que ce conteneur. Le piège : sans répertoire /var/log/journal, le journal systemd est volatil et se vide au redémarrage ; fail2ban ne retient alors que ce qui s'est passé depuis.

Le filtre contient deux expressions régulières, une par sorte de ligne d'échec ; la prison y ajoute le seuil, la durée et, surtout, la chaîne DOCKER-USER. Une chaîne est une liste de règles du pare-feu de l'hôte. Docker publie les ports de ses conteneurs par des règles nat, et le trafic qui y entre traverse les chaînes FORWARD puis DOCKER-USER (celle que Docker réserve aux règles de l'administrateur) mais ne passe pas par INPUT : une règle de bannissement posée là n'arrêterait rien. Créez le filtre.

Fenêtre de terminal
sudo tee /etc/fail2ban/filter.d/forgejo.conf >/dev/null <<'EOF'
[Definition]
failregex = Failed authentication attempt for .* from <HOST>:\d+
router: completed .* for <HOST>:\d+, 401 Unauthorized .*apiAuthentication
ignoreregex =
EOF

<HOST> est la balise de fail2ban qui capture l'adresse. La première expression attrape l'échec web, la seconde le 401 de l'API. Créez la prison.

Fenêtre de terminal
sudo tee /etc/fail2ban/jail.d/forgejo.local >/dev/null <<'EOF'
[forgejo]
enabled = true
backend = systemd
journalmatch = CONTAINER_NAME=forgejo
filter = forgejo
maxretry = 3
findtime = 600
bantime = 600
banaction = iptables-multiport
chain = DOCKER-USER
port = http,https
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status forgejo

maxretry = 3 dans findtime = 600 secondes déclenche un bannissement de bantime = 600 secondes ; ces valeurs sont celles du lab, courtes pour la démonstration. La commande de statut affiche la prison avec Journal matches: CONTAINER_NAME=forgejo, Currently banned: 0 et Total banned: 0. Testez le filtre contre le journal.

Fenêtre de terminal
sudo fail2ban-regex -m CONTAINER_NAME=forgejo systemd-journal /etc/fail2ban/filter.d/forgejo.conf

La sortie liste Failregex: 3 total avec les deux expressions et leur nombre de correspondances (une pour la ligne web, deux pour les 401 de l'API) si des échecs sont déjà dans le journal. Le piège : la forme systemd-journal[journalmatch=…] fait planter la commande ; c'est l'option -m qui porte le filtre sur le conteneur.

Le schéma résume le chemin de bout en bout, de haut en bas. Un essai de connexion entre par un port publié par Docker, donc par FORWARD puis DOCKER-USER, jamais par INPUT. Forgejo écrit l'échec dans journald, fail2ban le lit, et au troisième échec il pose la règle de ban dans DOCKER-USER, là où passe vraiment le trafic.

Bannissement fail2ban derrière Docker : le poste de l'attaquant envoie trois échecs de connexion en HTTPS ; le pare-feu de l'hôte fait passer ce port publié par Docker par FORWARD puis DOCKER-USER vers Caddy et Forgejo, alors qu'une règle posée dans INPUT reste sans effet. Forgejo écrit Failed authentication attempt ou 401 apiAuthentication dans journald avec CONTAINER_NAME=forgejo ; fail2ban, en backend systemd, compte trois échecs en 600 secondes et pose une règle de ban dans DOCKER-USER, qui bloque le poste.HTTPS 443port publié par Docker :FORWARD, puis DOCKER-USERINPUT : jamais traversépar ces paquetsFailed authentication attemptou 401 apiAuthentication3 échecs en 600 sbloquePoste de l'attaquant3 échecs de connexionPare-feu de l'hôteCaddyRègle posée dans INPUTsans effetForgejojournaldCONTAINER_NAME=forgejofail2banbackend systemdRègle de bandans DOCKER-USER
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Trois échecs depuis le poste font passer Currently banned à 1, ajoutent une règle dans DOCKER-USER, coupent le poste (code 000) sans toucher la VM, et unbanip le rétablit. Lancez ceci depuis le poste, pas depuis la VM. L'option --resolve fait résoudre forge.lab.test vers la VM sans toucher au DNS du poste, et les tableaux R rendent la commande identique sous bash et zsh.

Fenêtre de terminal
MYIP=$(ip route get 10.10.110.11 | grep -o 'src [0-9.]*' | cut -d' ' -f2)
R=(--resolve forge.lab.test:443:10.10.110.11)
echo "IP du poste : $MYIP"
curl -sk -m 8 "${R[@]}" -o /dev/null -w "avant -> HTTP %{http_code}\n" https://forge.lab.test/api/v1/version
for i in 1 2; do curl -sk -m 8 "${R[@]}" -u fjadmin:faux -o /dev/null -w "API Basic faux #$i -> HTTP %{http_code}\n" https://forge.lab.test/api/v1/user; done
J=$(mktemp); curl -sk -m 8 "${R[@]}" -c $J https://forge.lab.test/user/login -o $J.html
CS=$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' $J.html | head -1)
curl -sk -m 8 "${R[@]}" -b $J -o /dev/null -w "web faux -> HTTP %{http_code}\n" https://forge.lab.test/user/login --data-urlencode "_csrf=$CS" --data-urlencode user_name=fjadmin --data-urlencode password=faux
rm -f $J $J.html

Les sorties sont avant -> HTTP 200, deux API Basic faux -> HTTP 401 et web faux -> HTTP 200 (la page de connexion s'affiche de nouveau, avec son message d'erreur). Cela fait trois échecs : deux 401 de l'API et un échec web. Si la restriction REQUIRE_SIGNIN_VIEW de la leçon de configuration est active, la première ligne ne vaut plus 200 ; ce qui compte est qu'elle ne vaille pas 000. Attendez quelques secondes, puis lisez l'état sur la VM.

Fenêtre de terminal
sudo fail2ban-client status forgejo
sudo iptables -S DOCKER-USER | head -4

Le statut affiche Total failed: 3, Currently banned: 1 et la liste des adresses bannies, qui contient 10.10.110.1 (l'adresse du poste du lab). DOCKER-USER contient la règle -A DOCKER-USER -p tcp -m multiport --dports 80,443 -j f2b-forgejo. Vérifiez l'effet des deux côtés.

Fenêtre de terminal
curl -sk -m 6 "${R[@]}" -o /dev/null -w "poste banni -> HTTP %{http_code} (000 = bloqué)\n" https://forge.lab.test/api/v1/version
ssh forgejo-alma.lab 'curl -sk -m 6 -o /dev/null -w "VM -> HTTP %{http_code}\n" https://forge.lab.test/api/v1/version'

Le poste obtient HTTP 000 (aucune réponse avant le délai de 6 secondes) et la VM, qui n'est pas bannie, HTTP 200 : le bannissement vise l'adresse, pas le service. Remplacez forgejo-alma.lab par l'accès SSH de votre VM. Débannissez le poste.

Fenêtre de terminal
sudo fail2ban-client set forgejo unbanip 10.10.110.1
sudo fail2ban-client status forgejo | tail -3

La commande répond 1, le statut montre Currently banned: 0 et Total banned: 1, et le poste obtient de nouveau HTTP 200. Deux limites à connaître. Le filtre du 401 compte aussi les appels anonymes vers une route protégée, qui répondent 401 : le risque qu'un utilisateur légitime se banne tout seul n'a pas été mesuré, surveillez Total failed avant de réduire maxretry. Et la prison ne couvre que les ports http,https : l'authentification SSH sur le port 2222 n'a pas été étudiée.

Pourquoi garder un compte administrateur de secours ?

Section intitulée « Pourquoi garder un compte administrateur de secours ? »

Un compte administrateur local, au mot de passe conservé hors de la forge, reste utilisable quand le fournisseur d'identité tombe : c'est la porte de service qui évite de se réfugier dans la base de données. On l'appelle compte break-glass (« briser la vitre »), réservé aux urgences. Dans notre essai, avec une source d'authentification externe déclarée, ce compte se connecte toujours par mot de passe, sur le web comme par l'API. Listez d'abord les administrateurs, puis créez-le avec --admin.

Fenêtre de terminal
fj admin user list --admin
BG=$(openssl rand -hex 12)
fj admin user create --admin --username breakglass --email breakglass@example.test --password "$BG" --must-change-password=false
fj admin user list --admin

La première liste ne contient que fjadmin, la création répond New user 'breakglass' has been successfully created!, la seconde liste montre les deux comptes. Copiez $BG dans un coffre-fort hors de la forge (un mot de passe passé avec --password est visible dans ps, voir la leçon Assister les utilisateurs). Déclarons maintenant une source externe pour constater que le compte local passe. Une source OIDC dont la découverte (la lecture de l'adresse .well-known/openid-configuration du fournisseur) ne répond pas est refusée dès l'ajout.

Fenêtre de terminal
fj admin auth add-oauth --name factice-oidc --provider openidConnect --key fake-id --secret fake-secret --auto-discover-url https://127.0.0.1:1/.well-known/openid-configuration 2>&1 | tail -1
fj admin auth add-oauth --name factice-github --provider github --key fake-id --secret fake-secret
fj admin auth list

La première commande se termine par Command error: Failed to initialize OpenID Connect Provider with name 'factice-oidc' et la raison connection refused : rien n'est créé. La deuxième, avec le fournisseur github qui n'a pas de découverte à interroger, est acceptée. La liste montre une ligne factice-github OAuth2 true. Connectez-vous maintenant avec le compte de secours.

Fenêtre de terminal
J=$(mktemp); curl -sk -c $J https://$D/user/login -o $J.html
CS=$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' $J.html | head -1)
curl -sk -b $J -c $J -o /dev/null -w 'POST /user/login -> HTTP %{http_code} redirect=%{redirect_url}\n' https://$D/user/login --data-urlencode "_csrf=$CS" --data-urlencode user_name=breakglass --data-urlencode "password=$BG"
curl -sk -b $J -o /dev/null -w 'GET /user/settings -> HTTP %{http_code}\n' https://$D/user/settings
curl -sk -u "breakglass:$BG" -o /dev/null -w 'GET /api/v1/user (Basic) -> HTTP %{http_code}\n' https://$D/api/v1/user
rm -f $J $J.html

Les trois lignes affichent HTTP 303 redirect=https://forge.lab.test/, puis HTTP 200 pour les réglages (la session est ouverte), puis HTTP 200 pour l'API. Supprimez la source factice avec fj admin auth delete --id <ID>, l'identifiant étant celui de fj admin auth list.

La doctrine qui en découle tient en trois règles : au moins un administrateur local de secours, un mot de passe conservé hors de la forge, et un second facteur sur ce compte. Attention à l'ordre : avec GLOBAL_TWO_FACTOR_REQUIREMENT à all (voir imposer la double authentification), un compte sans second facteur est redirigé vers sa page de sécurité et refusé par l'API. Enrôlez donc le second facteur du compte de secours, depuis sa page de sécurité, avant d'imposer ce réglage. Gardez la liste des administrateurs courte : l'aide de user create propose aussi --restricted, que nous n'avons pas essayée.

Comment limiter ce qu'un jeton d'accès peut faire ?

Section intitulée « Comment limiter ce qu'un jeton d'accès peut faire ? »

Un jeton se génère avec --scopes ; un jeton à portée réduite reçoit un 403 qui nomme la portée manquante, et une portée inconnue est refusée dès la génération. Une portée (scope) est le droit précis qu'un jeton emporte (lire les dépôts, écrire sur le profil…). Un jeton de portée all hérite de tous les droits de son propriétaire : s'il fuit, c'est la prise de contrôle du compte. Créez un compte et un jeton qui ne lit que les dépôts.

Fenêtre de terminal
PW=$(openssl rand -hex 12)
fj admin user create --username ro --email ro@example.test --password "$PW" --must-change-password=false >/dev/null 2>&1
RO=$(fj admin user generate-access-token --username ro --token-name ro-repo-read --scopes read:repository --raw)
curl -sk -H "Authorization: token $RO" -H 'Content-Type: application/json' -X POST https://$D/api/v1/user/repos -d '{"name":"interdit"}'
curl -sk -o /dev/null -w '\nGET /user -> HTTP %{http_code}\n' -H "Authorization: token $RO" https://$D/api/v1/user

La création répond token does not have at least one of required scope(s): [write:user] (HTTP 403). La seconde commande n'affiche que le code 403, car elle écarte le corps de la réponse ; sans -o /dev/null, il nomme [read:user]. La surprise est que la création d'un dépôt utilisateur demande la portée write:user, pas seulement write:repository : un second jeton write:repository seul, que nous avons essayé, reçoit le même 403 sur [write:user]. Lisez le message d'erreur avant d'élargir : il donne la portée exacte à ajouter. Essayez maintenant une portée qui n'existe pas.

Fenêtre de terminal
fj admin user generate-access-token --username ro --token-name bad --scopes foo:bar --raw 2>&1 | head -1
curl -sk -u "ro:$PW" https://$D/api/v1/users/ro/tokens

La génération répond Command error: invalid access token scope provided: invalid access token scope: foo:bar. La liste, lue en authentification Basic, affiche le nom, les portées et le champ repositories de chaque jeton, jamais sa valeur. Ce champ repositories est la seule autre restriction que le schéma de l'API prévoit ; nous ne l'avons pas rejoué.

Le point le plus important est négatif : aucune expiration de jeton n'est possible en 15.0.10. Le schéma de l'API ne définit pas de champ de date pour AccessToken, et un expires_at ajouté à la création est accepté sans effet. Un jeton vit donc jusqu'à sa révocation. La conduite qui en découle : des jetons nommés et à portée minimale, un inventaire régulier avec la liste ci-dessus, et la révocation par l'administrateur en Basic, détaillée dans la leçon Assister les utilisateurs.

On génère une clé GPG dans le trousseau que Forgejo utilise réellement (/data/gitea/home/.gnupg), on la déclare dans [repository.signing], et les commits créés par l'API ou l'interface sont alors vérifiés. La signature d'un commit prouve que son auteur détient une clé ; elle ne dit pas que le contenu est bon. Pour la comparaison entre GPG, SSH et la signature sans clé, voir GitSign. Ici, c'est l'instance qui signe, pour les commits qu'elle crée elle-même (premier commit d'un dépôt auto_init, édition par l'interface, fusion).

Le piège est le trousseau, le répertoire où gpg range les clés. docker exec -u git donne à gpg le répertoire personnel /data/git, alors que Forgejo cherche ses clés dans /data/gitea/home/.gnupg (le réglage [git] HOME_PATH, dont la valeur par défaut est %(APP_DATA_PATH)s/home, soit /data/gitea/home dans l'image Docker). Une clé générée avec la commande par défaut est dans le mauvais trousseau. Générez-la, relevez l'empreinte (l'identifiant complet de la clé), puis déplacez-la.

Fenêtre de terminal
sudo docker exec -u git forgejo gpg --batch --passphrase '' --quick-generate-key "Forgejo Lab <forge@forge.lab.test>" default default never
FPR=$(sudo docker exec -u git forgejo gpg --list-secret-keys --with-colons | awk -F: '/^fpr/ {print $10; exit}')
echo "empreinte : $FPR"

La clé n'a pas de phrase secrète parce que Forgejo ne peut pas en saisir une ; elle vit dans le volume de données, que votre sauvegarde contient, et quiconque y lit peut signer au nom de l'instance. Un --homedir /data/gitea/home/.gnupg dès la génération éviterait le déplacement ; nous ne l'avons pas rejoué sous cette forme. Déplacez la clé par un tube, sans fichier sur l'hôte, en arrêtant d'abord les démons gpg (des processus d'arrière-plan qui gardent le trousseau ouvert) et en supprimant leurs verrous.

Fenêtre de terminal
OLD=/data/git/.gnupg
NEW=/data/gitea/home/.gnupg
g() { sudo docker exec -i -u git forgejo "$@"; }
g gpgconf --homedir $OLD --kill all; g gpgconf --homedir $NEW --kill all
sudo docker exec forgejo find $OLD $NEW \( -name '*.lock' -o -name '.#lk*' \) -print -delete
g gpg --homedir $OLD --batch --pinentry-mode loopback --passphrase '' --export-secret-keys $FPR | g gpg --homedir $NEW --batch --pinentry-mode loopback --passphrase '' --import

Déclarez maintenant la clé. Créez /opt/forgejo-lab/signature.yaml, la valeur de l'empreinte venant du .env.

services:
server:
environment:
FORGEJO__repository.signing__SIGNING_KEY: ${SIGNING_KEY}
FORGEJO__repository.signing__SIGNING_NAME: Forgejo Lab
FORGEJO__repository.signing__SIGNING_EMAIL: forge@forge.lab.test
FORGEJO__repository.signing__INITIAL_COMMIT: always
FORGEJO__repository.signing__CRUD_ACTIONS: always
FORGEJO__repository.signing__MERGES: always
Fenêtre de terminal
cd /opt/forgejo-lab
sudo sed -i 's#^COMPOSE_FILE=.*#&:signature.yaml#' .env
reglage SIGNING_KEY $FPR
g gpgconf --homedir $NEW --kill all; g gpgconf --homedir $OLD --kill all
sudo docker exec forgejo find $OLD $NEW \( -name '*.lock' -o -name '.#lk*' \) -print -delete
sudo docker exec forgejo grep -A7 '^\[repository.signing\]' /data/gitea/conf/app.ini
curl -sk -o /dev/null -w 'signing-key.gpg -> HTTP %{http_code}\n' https://$D/api/v1/signing-key.gpg

Le bloc affiché contient les six clés avec SIGNING_KEY égale à l'empreinte complète, et la clé publique de l'instance répond HTTP 200. Si la leçon de configuration a déjà posé DEFAULT_TRUST_MODEL dans cette même section, les deux réglages coexistent. Prouvez la signature avec un dépôt auto_init et un commit par l'API.

Fenêtre de terminal
sig() { python3 -c 'import json,sys;d=json.load(sys.stdin);v=d.get("commit",d).get("verification") or d.get("verification");print({k:v.get(k) for k in ("verified","reason")},"signer =",(v.get("signer") or {}).get("email"))'; }
api -X POST https://$D/api/v1/user/repos -d '{"name":"sigtest","auto_init":true,"default_branch":"main"}' -o /dev/null -w 'dépôt -> HTTP %{http_code}\n'
api https://$D/api/v1/repos/fjadmin/sigtest/git/commits/HEAD | sig
api -X POST https://$D/api/v1/repos/fjadmin/sigtest/contents/a.txt -d "{\"content\":\"$(echo bonjour | base64)\",\"message\":\"commit via API\"}" | sig

Chaque commit affiche 'verified': True, une raison de la forme Forgejo Lab / 367D2B38A690E376 (la fin de l'empreinte, propre à votre clé) et le signataire forge@forge.lab.test. Le résultat tient aussi après un redémarrage du conteneur dans notre essai de validation.

Le piège : si un POST répond 500 avec dans le journal waiting for lock (held by N) puis keydb_search failed, ce sont des verrous périmés du démon gpg : l'import les crée et reglage recrée le conteneur, qui tue le démon sans les retirer. Sans les deux lignes qui suivent reglage, nous avons obtenu HTTP 500 sur signing-key.gpg et sur la création du dépôt. Pour le corriger après coup, relancez ces deux lignes (gpgconf --kill all puis la suppression des *.lock et .#lk*) ; il faut aussi les relancer avant de relire une empreinte avec gpg --list-secret-keys si le conteneur a été recréé depuis.

Comment vérifier les commits signés par un utilisateur ?

Section intitulée « Comment vérifier les commits signés par un utilisateur ? »

L'utilisateur dépose sa clé GPG publique avec POST /user/gpg_keys, et ses commits signés par cette clé sont verified = True ; un commit non signé reçoit gpg.error.not_signed_commit. Forgejo ne signe pas à la place de l'utilisateur : il vérifie. Pour l'essai, créez un compte sigdev, un trousseau temporaire et une clé dont l'e-mail est celui du compte.

Fenêtre de terminal
W=$(mktemp -d); export GNUPGHOME=$W/gnupg; mkdir -m 700 $GNUPGHOME
export GIT_SSL_NO_VERIFY=1 GIT_TERMINAL_PROMPT=0
PW=$(openssl rand -hex 12)
fj admin user create --username sigdev --email sigdev@example.test --password "$PW" --must-change-password=false | tail -1
UTK=$(fj admin user generate-access-token --username sigdev --token-name t --scopes all --raw)
uapi() { curl -sk -H "Authorization: token $UTK" -H 'Content-Type: application/json' "$@"; }
gpg --batch --passphrase '' --quick-generate-key "sigdev <sigdev@example.test>" ed25519 sign never
KID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/ {print $10; exit}')
gpg --armor --export $KID > $W/pub.asc
uapi -X POST https://$D/api/v1/user/gpg_keys -d "$(python3 -c 'import json,sys;print(json.dumps({"armored_public_key":open(sys.argv[1]).read()}))' $W/pub.asc)" -o /dev/null -w 'clé GPG -> HTTP %{http_code}\n'
uapi -X POST https://$D/api/v1/user/repos -d '{"name":"sigrepo","auto_init":true,"default_branch":"main"}' -o /dev/null -w 'dépôt -> HTTP %{http_code}\n'

Les deux appels répondent HTTP 201. La clé est générée dans un répertoire temporaire (GNUPGHOME) : elle n'est jamais mêlée au trousseau de l'instance. Dans l'essai, l'e-mail de la clé était celui du compte ; une clé dont l'e-mail n'appartient pas au compte n'a pas été essayée. Clonez, faites un commit signé et un commit non signé, poussez, puis lisez les vérifications.

Fenêtre de terminal
cd $W && git -c credential.helper= -c "http.extraHeader=Authorization: token $UTK" clone -q https://$D/sigdev/sigrepo.git h && cd h
git config user.email sigdev@example.test; git config user.name sigdev
echo gpg > g.txt; git add .; git -c user.signingkey=$KID commit -q -S -m "commit signe gpg utilisateur"
echo ns > n.txt; git add .; git commit -q --no-gpg-sign -m "commit non signe"
git -c credential.helper= -c "http.extraHeader=Authorization: token $UTK" push -q origin main && echo push-ok
uapi "https://$D/api/v1/repos/sigdev/sigrepo/commits?limit=3" | python3 -c '
import json,sys
for c in json.load(sys.stdin):
v=c["commit"].get("verification") or c.get("verification") or {}
print(c["sha"][:8], c["commit"]["message"].strip()[:30], "| verified =", v.get("verified"), "| reason =", v.get("reason"))'

Après push-ok, trois lignes : commit non signe avec verified = False et gpg.error.not_signed_commit, commit signe gpg utilisateur avec verified = True et une raison sigdev / … (l'identifiant de la clé), et l'Initial commit vérifié par la clé de l'instance. Le piège : la signature SSH n'a pas abouti dans notre essai (clé SSH enregistrée avec verified = None, vérification par jeton signé en erreur 500 sans message, commit en gpg.error.no_gpg_keys_found). Nous ne la décrivons donc pas : pour les utilisateurs, enseignez la signature GPG ci-dessus.

Que reste-t-il de la traçabilité sans journal d'audit ?

Section intitulée « Que reste-t-il de la traçabilité sans journal d'audit ? »

Forgejo 15.0.10 n'a aucun journal d'audit dédié : ni route dans l'API (aucune ne contient « audit »), ni réglage dans la feuille de configuration. Ce qui en tient lieu, ce sont les lignes du routeur dans le journal du conteneur. Elles disent qu'un geste d'administration a eu lieu, pas qui l'a fait. Trois gestes joués par l'API le montrent (extrait de l'essai initial, non rejoué lors du rejeu final de la leçon).

router: completed POST /api/v1/admin/users for 172.30.0.1:0, 201 Created in 99.4ms @ admin/user.go:55(admin.CreateUser)
router: completed PATCH /api/v1/admin/users/audit-probe2 for 172.30.0.1:0, 200 OK in 9.6ms @ admin/user.go:166(admin.EditUser)
router: completed DELETE /api/v1/admin/users/audit-probe2?purge=true for 172.30.0.1:0, 204 No Content in 35.4ms @ admin/user.go:275(admin.DeleteUser)

Chaque ligne porte la méthode, la route, l'adresse source et le code, mais aucune identité d'utilisateur : on sait qu'un PATCH a promu audit-probe2 administrateur, pas avec quel jeton. La journalisation en place est [log] MODE = console et LEVEL = info. Et les actions faites par la CLI forgejo admin (création d'un compte, génération d'un jeton) n'ont laissé aucune ligne du routeur dans notre essai.

La conséquence tient à l'hôte. Quiconque peut lancer docker exec dans le conteneur agit sans trace dans Forgejo : limitez l'appartenance au groupe docker et à sudo, et journalisez les sessions shell de l'hôte avec l'outil de votre choix. Conservez aussi le journal du conteneur : le pilote journald installé plus haut le garde, sous réserve de rendre le journal systemd persistant.

Deux sources à surveiller : les annonces de sécurité sur codeberg.org/forgejo/security-announcements et les versions publiées sur forgejo.org/releases/. Une forge exposée est un service que l'on met à jour au rythme de ses correctifs ; la branche 15 LTS reçoit des versions de maintenance, appliquées avec la procédure de la leçon mettre à jour Forgejo. Nous n'avons pas trouvé d'autre page officielle fiable : les adresses /docs/v15.0/admin/security/ et /security-releases/ répondent 404.

Que faire quand un réglage ne produit pas l'effet attendu ?

Section intitulée « Que faire quand un réglage ne produit pas l'effet attendu ? »

Les échecs de cette leçon se lisent dans la table hook_task, dans l'état de la prison ou dans le message d'erreur de l'API. Le tableau relie chacun à sa cause.

SymptômeCauseCorrection
Webhook créé (201), jamais reçu ; webhook can only call allowed HTTP servers dans hook_taskla cible n'est pas dans ALLOWED_HOST_LISTlister l'adresse ou le CIDR précis, jamais *
Le webhook de 127.0.0.1 répond 405loopback est autorisé : Forgejo s'est appelé lui-mêmeretirer loopback de la liste
Total failed: 0 malgré des échecsle pilote de journal n'est pas journald, ou la prison ne filtre pas le bon conteneurdocker inspect pour le pilote, Journal matches pour le filtre
La prison compte, mais l'adresse bannie passerègle posée dans INPUT au lieu de DOCKER-USERchain = DOCKER-USER dans la prison
Le poste de test ne se fait jamais bannirl'essai part de l'hôte, vu comme 172.30.0.1tester depuis un autre poste
fail2ban-regex plante avec systemd-journal[journalmatch=…]forme non acceptée-m CONTAINER_NAME=forgejo systemd-journal …
403 … required scope(s): [write:user]la portée du jeton est trop étroiteajouter la portée citée, pas all
HTTP 500 sur un commit, waiting for lock (held by N)verrous gpg périmés après un redémarragegpgconf --kill all, supprimer *.lock et .#lk*
gpg: signing failed: No secret keyla clé n'est pas dans le trousseau que Forgejo utilisela déplacer dans /data/gitea/home/.gnupg
Forgejo ne redémarre plus, Unable to init config provider … permission deniedapp.ini édité en root dans le conteneurchown 1000:1000 /data/gitea/conf/app.ini* et chmod 644

Le dernier cas vient d'une habitude à éviter : préférez les variables FORGEJO__… à l'édition de app.ini. Une édition en root dans le conteneur rend le fichier propriétaire root en mode 600, et Forgejo, qui tourne en utilisateur 1000, ne peut plus le lire.

On supprime les comptes et les dépôts de test, on révoque le jeton de l'administrateur, on retire la prison et on remet les réglages à leur valeur de départ. Retirer un fichier de COMPOSE_FILE ne retire pas la clé de app.ini, comme l'indique la leçon de configuration : on fixe la valeur de départ.

Fenêtre de terminal
api -X DELETE https://$D/api/v1/repos/fjadmin/whk -o /dev/null -w 'whk -> HTTP %{http_code}\n'
api -X DELETE https://$D/api/v1/repos/fjadmin/sigtest -o /dev/null -w 'sigtest -> HTTP %{http_code}\n'
fj admin user delete --username breakglass --purge
fj admin user delete --username sigdev --purge
fj admin user delete --username ro --purge
pkill -f '[l]istener.py'
reglage HOOK_HOSTS external

Les dépôts répondent HTTP 204, et chaque fj admin user delete affiche des lignes d'initialisation du stockage avant de finir : c'est normal. Si la source factice factice-github existe encore, retirez-la avec fj admin auth delete --id <ID>. Le jeton secu-… de fjadmin reste valable après la fermeture du shell : révoquez-le comme dans la leçon Assister les utilisateurs, par un DELETE en Basic (curl -sk -u fjadmin:<mot de passe> -X DELETE https://$D/api/v1/users/fjadmin/tokens/<nom>, réponse 204). Chaque nouvelle ouverture du shell en crée un nouveau, d'où plusieurs jetons à révoquer si vous avez rouvert le shell.

Pour la prison, supprimez /etc/fail2ban/jail.d/forgejo.local et /etc/fail2ban/filter.d/forgejo.conf puis redémarrez fail2ban : sans prison, le service s'arrête (fail2ban-client status répond Failed to access socket path) et DOCKER-USER redevient vide. Retirez ensuite signature.yaml, journald.yaml et webhooks.yaml de COMPOSE_FILE et relancez server : le pilote de journal redevient json-file. En revanche, app.ini garde les sections [webhook] (ALLOWED_HOST_LIST = external) et [repository.signing] avec la clé d'instance : la signature reste donc active tant que vous ne fixez pas INITIAL_COMMIT, CRUD_ACTIONS et MERGES à la valeur voulue. La feuille de configuration v15 donne pour défauts always (INITIAL_COMMIT), pubkey, twofa, parentsigned (CRUD_ACTIONS) et pubkey, twofa, basesigned, commitssigned (MERGES), et never les désactive (cette dernière correction n'a pas été rejouée).

  • [webhook] ALLOWED_HOST_LIST vaut external par défaut ; un webhook vers une adresse refusée est créé (201) et échoue à la livraison avec webhook can only call allowed HTTP servers. Listez l'adresse ou le CIDR précis, jamais *.
  • Avec loopback dans la liste, un webhook atteint le port 3000 de Forgejo lui-même (réponse 405) : c'est le chemin d'une SSRF.
  • Forgejo 15.0.10 n'a pas de limite de tentatives de connexion démontrée ; fail2ban lit le journal journald du conteneur (journalmatch = CONTAINER_NAME=forgejo) et bannit dans DOCKER-USER, pas INPUT.
  • Un échec web laisse Failed authentication attempt for … from <IP>, un échec API en Basic seulement une ligne du routeur en 401 Unauthorized ; testez depuis un autre poste que l'hôte.
  • Gardez au moins un administrateur local de secours, mot de passe hors de la forge, second facteur enrôlé avant d'imposer GLOBAL_TWO_FACTOR_REQUIREMENT à all : il se connecte même quand une source externe est déclarée.
  • Un jeton read:repository ne crée pas de dépôt (403 sur [write:user]), une portée inconnue est refusée à la génération, et aucun jeton n'expire en 15.0.10.
  • La clé de signature de l'instance doit être dans /data/gitea/home/.gnupg ; les commits qu'elle crée sont verified = True, un commit non signé répond gpg.error.not_signed_commit. La signature SSH n'a pas abouti dans l'essai.
  • Pas de journal d'audit dédié : les lignes du routeur n'ont pas d'identité, et la CLI forgejo admin ne laisse aucune ligne. Il faut tracer l'hôte et limiter l'accès à docker exec.
  • Les avis de sécurité se suivent sur codeberg.org/forgejo/security-announcements et forgejo.org/releases/ ; les correctifs de la branche 15 LTS s'appliquent avec la procédure de mise à jour.

Six questions tirées sur huit : la livraison d'un webhook interne, la chaîne DOCKER-USER, la trace d'un échec d'API, la portée d'un jeton, le trousseau de signature et ses verrous.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn