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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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.
Quels sont les prérequis ?
Section intitulée « Quels sont les prérequis ? »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.
D=forge.lab.testH=10.10.110.11fj() { 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.
Que faut-il protéger sur une forge ?
Section intitulée « Que faut-il protéger sur une forge ? »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.
| Surface | Risque | Parade dans cette leçon |
|---|---|---|
| Webhooks | la forge est amenée à appeler un service interne (SSRF, définie juste après) | [webhook] ALLOWED_HOST_LIST |
| Page de connexion et API | essais de mots de passe en masse | fail2ban sur le journal |
| Comptes administrateurs | panne d'accès, ou trop de comptes puissants | compte de secours, liste courte |
| Jetons d'accès | un jeton volé ouvre tous les droits de son propriétaire | portées minimales |
| Commits | un commit dont on ne peut pas prouver l'auteur | signature par l'instance ou l'utilisateur |
| Traçabilité | aucune trace de qui a fait quoi | journalisation 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].
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(): > /tmp/listener.logsetsid 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.
cd /opt/forgejo-labsudo sed -i 's#^COMPOSE_FILE=.*#&:webhooks.yaml#' .envreglage HOOK_HOSTS $Hsudo docker exec forgejo grep -A2 '^\[webhook\]' /data/gitea/conf/app.iniLa 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.
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; donesleep 8Les 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.
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.logsudo docker logs forgejo --since 40s 2>&1 | grep -i 'webhook' | cut -c1-260Seule 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.
reglage HOOK_HOSTS 172.30.0.0/24,loopbacksudo docker exec forgejo grep -A2 '^\[webhook\]' /data/gitea/conf/app.iniSupprimez 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.
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"; doneChaque suppression répond HTTP 204.
| Cible | Liste = hôte | Liste = 172.30.0.0/24,loopback |
|---|---|---|
http://127.0.0.1:3000/ | refusée | livré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ée | livrée (200) |
http://192.168.99.9:9000/ | refusée | refusé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.
sudo dnf install -y epel-releasesudo dnf install -y fail2bansudo systemctl enable --now fail2bansudo fail2ban-client --version | head -1La 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: forgejocd /opt/forgejo-labsudo sed -i 's#^COMPOSE_FILE=.*#&:journald.yaml#' .envsudo docker compose config --quiet && echo "compose valide"sudo docker compose up -d serverattendresudo docker inspect forgejo --format 'log driver: {{.HostConfig.LogConfig.Type}}'sudo journalctl CONTAINER_NAME=forgejo -n 3 --no-pager -o short-iso | cut -c1-200Vous 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.
Comment écrire le filtre et la prison fail2ban ?
Section intitulée « Comment écrire le filtre et la prison fail2ban ? »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.
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 .*apiAuthenticationignoreregex =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.
sudo tee /etc/fail2ban/jail.d/forgejo.local >/dev/null <<'EOF'[forgejo]enabled = truebackend = systemdjournalmatch = CONTAINER_NAME=forgejofilter = forgejomaxretry = 3findtime = 600bantime = 600banaction = iptables-multiportchain = DOCKER-USERport = http,httpsEOFsudo systemctl restart fail2bansudo fail2ban-client status forgejomaxretry = 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.
sudo fail2ban-regex -m CONTAINER_NAME=forgejo systemd-journal /etc/fail2ban/filter.d/forgejo.confLa 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.
Comment prouver qu'un bannissement fonctionne ?
Section intitulée « Comment prouver qu'un bannissement fonctionne ? »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.
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/versionfor 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; doneJ=$(mktemp); curl -sk -m 8 "${R[@]}" -c $J https://forge.lab.test/user/login -o $J.htmlCS=$(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=fauxrm -f $J $J.htmlLes 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.
sudo fail2ban-client status forgejosudo iptables -S DOCKER-USER | head -4Le 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.
curl -sk -m 6 "${R[@]}" -o /dev/null -w "poste banni -> HTTP %{http_code} (000 = bloqué)\n" https://forge.lab.test/api/v1/versionssh 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.
sudo fail2ban-client set forgejo unbanip 10.10.110.1sudo fail2ban-client status forgejo | tail -3La 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.
fj admin user list --adminBG=$(openssl rand -hex 12)fj admin user create --admin --username breakglass --email breakglass@example.test --password "$BG" --must-change-password=falsefj admin user list --adminLa 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.
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 -1fj admin auth add-oauth --name factice-github --provider github --key fake-id --secret fake-secretfj admin auth listLa 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.
J=$(mktemp); curl -sk -c $J https://$D/user/login -o $J.htmlCS=$(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/settingscurl -sk -u "breakglass:$BG" -o /dev/null -w 'GET /api/v1/user (Basic) -> HTTP %{http_code}\n' https://$D/api/v1/userrm -f $J $J.htmlLes 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.
PW=$(openssl rand -hex 12)fj admin user create --username ro --email ro@example.test --password "$PW" --must-change-password=false >/dev/null 2>&1RO=$(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/userLa 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.
fj admin user generate-access-token --username ro --token-name bad --scopes foo:bar --raw 2>&1 | head -1curl -sk -u "ro:$PW" https://$D/api/v1/users/ro/tokensLa 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.
Comment faire signer les commits par l'instance ?
Section intitulée « Comment faire signer les commits par l'instance ? »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.
sudo docker exec -u git forgejo gpg --batch --passphrase '' --quick-generate-key "Forgejo Lab <forge@forge.lab.test>" default default neverFPR=$(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.
OLD=/data/git/.gnupgNEW=/data/gitea/home/.gnupgg() { sudo docker exec -i -u git forgejo "$@"; }g gpgconf --homedir $OLD --kill all; g gpgconf --homedir $NEW --kill allsudo docker exec forgejo find $OLD $NEW \( -name '*.lock' -o -name '.#lk*' \) -print -deleteg gpg --homedir $OLD --batch --pinentry-mode loopback --passphrase '' --export-secret-keys $FPR | g gpg --homedir $NEW --batch --pinentry-mode loopback --passphrase '' --importDé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: alwayscd /opt/forgejo-labsudo sed -i 's#^COMPOSE_FILE=.*#&:signature.yaml#' .envreglage SIGNING_KEY $FPRg gpgconf --homedir $NEW --kill all; g gpgconf --homedir $OLD --kill allsudo docker exec forgejo find $OLD $NEW \( -name '*.lock' -o -name '.#lk*' \) -print -deletesudo docker exec forgejo grep -A7 '^\[repository.signing\]' /data/gitea/conf/app.inicurl -sk -o /dev/null -w 'signing-key.gpg -> HTTP %{http_code}\n' https://$D/api/v1/signing-key.gpgLe 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.
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 | sigapi -X POST https://$D/api/v1/repos/fjadmin/sigtest/contents/a.txt -d "{\"content\":\"$(echo bonjour | base64)\",\"message\":\"commit via API\"}" | sigChaque 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.
W=$(mktemp -d); export GNUPGHOME=$W/gnupg; mkdir -m 700 $GNUPGHOMEexport GIT_SSL_NO_VERIFY=1 GIT_TERMINAL_PROMPT=0PW=$(openssl rand -hex 12)fj admin user create --username sigdev --email sigdev@example.test --password "$PW" --must-change-password=false | tail -1UTK=$(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 neverKID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/ {print $10; exit}')gpg --armor --export $KID > $W/pub.ascuapi -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.
cd $W && git -c credential.helper= -c "http.extraHeader=Authorization: token $UTK" clone -q https://$D/sigdev/sigrepo.git h && cd hgit config user.email sigdev@example.test; git config user.name sigdevecho 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-okuapi "https://$D/api/v1/repos/sigdev/sigrepo/commits?limit=3" | python3 -c 'import json,sysfor 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.
Où suivre les avis de sécurité de Forgejo ?
Section intitulée « Où suivre les avis de sécurité de Forgejo ? »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ôme | Cause | Correction |
|---|---|---|
Webhook créé (201), jamais reçu ; webhook can only call allowed HTTP servers dans hook_task | la cible n'est pas dans ALLOWED_HOST_LIST | lister l'adresse ou le CIDR précis, jamais * |
| Le webhook de 127.0.0.1 répond 405 | loopback est autorisé : Forgejo s'est appelé lui-même | retirer loopback de la liste |
Total failed: 0 malgré des échecs | le pilote de journal n'est pas journald, ou la prison ne filtre pas le bon conteneur | docker inspect pour le pilote, Journal matches pour le filtre |
| La prison compte, mais l'adresse bannie passe | règle posée dans INPUT au lieu de DOCKER-USER | chain = DOCKER-USER dans la prison |
| Le poste de test ne se fait jamais bannir | l'essai part de l'hôte, vu comme 172.30.0.1 | tester 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 étroite | ajouter 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émarrage | gpgconf --kill all, supprimer *.lock et .#lk* |
gpg: signing failed: No secret key | la clé n'est pas dans le trousseau que Forgejo utilise | la déplacer dans /data/gitea/home/.gnupg |
Forgejo ne redémarre plus, Unable to init config provider … permission denied | app.ini édité en root dans le conteneur | chown 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.
Comment défaire cette configuration ?
Section intitulée « Comment défaire cette configuration ? »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.
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 --purgefj admin user delete --username sigdev --purgefj admin user delete --username ro --purgepkill -f '[l]istener.py'reglage HOOK_HOSTS externalLes 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).
À retenir
Section intitulée « À retenir »[webhook] ALLOWED_HOST_LISTvautexternalpar défaut ; un webhook vers une adresse refusée est créé (201) et échoue à la livraison avecwebhook can only call allowed HTTP servers. Listez l'adresse ou le CIDR précis, jamais*.- Avec
loopbackdans 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
journalddu conteneur (journalmatch = CONTAINER_NAME=forgejo) et bannit dansDOCKER-USER, pasINPUT. - Un échec web laisse
Failed authentication attempt for … from <IP>, un échec API en Basic seulement une ligne du routeur en401 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:repositoryne 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 sontverified = True, un commit non signé répondgpg.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 adminne 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-announcementsetforgejo.org/releases/; les correctifs de la branche 15 LTS s'appliquent avec la procédure de mise à jour.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Exploiter le runner Forgejo
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications