chore: build
ci-api / test (push) Successful in 25m49s

This commit is contained in:
Xor290
2026-08-18 20:07:52 +02:00
parent aa1aa60f00
commit dfc62c305d
25 changed files with 741 additions and 58 deletions
@@ -20,6 +20,10 @@ spec:
labels:
{{- include "backend.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
+5
View File
@@ -5,6 +5,11 @@ image:
tag: latest
pullPolicy: IfNotPresent
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials)
# si un compte Docker Hub authentifié est configuré — évite le rate-limit de
# pull anonyme partagé par IP de nœud. Vide = pull anonyme (défaut).
imagePullSecrets: []
service:
type: ClusterIP
port: 8080
@@ -16,6 +16,10 @@ spec:
labels:
{{- include "frontend.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
@@ -25,8 +29,13 @@ spec:
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
# L'image xor1234/frontend-mln (nginx) écoute en dur sur le port
# 80 (voir /etc/nginx/conf.d/default.conf embarqué dans l'image) —
# déjà anticipé par deploy/chart-gestion/ingressroute/values.yaml
# (frontend.servicePort: 80). 8080 ici ne correspondait à rien de
# réellement écouté, d'où le "connection refused" en probe.
- name: http
containerPort: 8080
containerPort: 80
protocol: TCP
volumeMounts:
- name: tmp
@@ -37,6 +46,15 @@ spec:
mountPath: /srv/config.js
subPath: config.js
readOnly: true
# readOnlyRootFilesystem:true (voir securityContext) empêche
# nginx de créer /var/cache/nginx/client_temp au démarrage
# ("mkdir() ... failed (30: Read-only file system)", CrashLoop) —
# nginx a besoin d'y écrire même quand il ne sert que du
# contenu statique.
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
livenessProbe:
httpGet:
path: /
@@ -56,6 +74,10 @@ spec:
emptyDir: {}
- name: caddy-data
emptyDir: {}
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
- name: app-config
configMap:
name: {{ include "frontend.fullname" . }}-config
+10 -1
View File
@@ -7,9 +7,18 @@ image:
tag: latest
pullPolicy: IfNotPresent
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials)
# si un compte Docker Hub authentifié est configuré — évite le rate-limit de
# pull anonyme partagé par IP de nœud. Vide = pull anonyme (défaut).
imagePullSecrets: []
service:
type: ClusterIP
port: 8080
# L'IngressRoute (deploy/chart-gestion/ingressroute) référence directement
# ce port de Service (frontend.servicePort, déjà à 80 dans son
# values.yaml — il anticipait correctement le vrai port de nginx). Doit
# rester égal à containerPort (voir templates/deployment.yaml).
port: 80
resources:
requests:
@@ -18,6 +18,10 @@ spec:
labels:
{{- include "lbtelegram.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
@@ -5,6 +5,11 @@ image:
tag: latest
pullPolicy: IfNotPresent
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials)
# si un compte Docker Hub authentifié est configuré — évite le rate-limit de
# pull anonyme partagé par IP de nœud. Vide = pull anonyme (défaut).
imagePullSecrets: []
resources:
requests:
cpu: 100m
@@ -15,6 +15,10 @@ spec:
labels:
{{- include "postgresql.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
@@ -3,6 +3,11 @@ image:
tag: "16-alpine"
pullPolicy: IfNotPresent
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials)
# si un compte Docker Hub authentifié est configuré — évite le rate-limit de
# pull anonyme partagé par IP de nœud. Vide = pull anonyme (défaut).
imagePullSecrets: []
auth:
username: postgres
database: gestion_db
@@ -15,6 +15,10 @@ spec:
labels:
{{- include "redis.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
+5
View File
@@ -3,6 +3,11 @@ image:
tag: "7-alpine"
pullPolicy: IfNotPresent
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials)
# si un compte Docker Hub authentifié est configuré — évite le rate-limit de
# pull anonyme partagé par IP de nœud. Vide = pull anonyme (défaut).
imagePullSecrets: []
auth:
# À surcharger via --set auth.password=xxx
password: ""
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-registry-credentials
description: Secret docker-registry (Docker Hub authentifié) installé par démo — évite le rate-limit de pull anonyme (100/6h par IP, contre 200/6h authentifié) partagé entre tous les pods du même nœud.
type: application
version: 0.1.0
appVersion: "1.0.0"
@@ -0,0 +1,9 @@
{{- if and .Values.username .Values.password }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.secretName }}
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: {{ printf `{"auths":{"%s":{"username":"%s","password":"%s","auth":"%s"}}}` .Values.registry .Values.username .Values.password (printf "%s:%s" .Values.username .Values.password | b64enc) | b64enc }}
{{- end }}
@@ -0,0 +1,9 @@
# Identifiants Docker Hub (compte dédié, droits de lecture seule suffisent —
# pas besoin d'un compte payant, le seul but est de doubler le rate-limit de
# pull anonyme : 100/6h par IP -> 200/6h authentifié). Jamais codés en dur,
# injectés par le control-plane (voir OMNEX_DOCKERHUB_USERNAME/PASSWORD dans
# internal/config/config.go) au moment de l'installation.
secretName: dockerhub-pull-secret
registry: https://index.docker.io/v1/
username: ""
password: ""
@@ -9,5 +9,14 @@ spec:
chain:
middlewares:
- name: rate-limit
- name: coraza-waf
# coraza-waf désactivé temporairement (2026-08-17) : fait planter
# Traefik (panic Go "unsafe.Slice: len out of range" dans le runtime
# WASM du plugin coraza-http-wasm-traefik v0.3.0) dès qu'une vraie
# IngressRoute de démo l'active — reproductible à 100% au chargement,
# crash-loop du pod partagé, bloque TOUTES les démos. Jamais exercé
# avant la toute première démo réelle. À investiguer (version du
# plugin ? directives ModSecurity trop volumineuses/complexes pour ce
# runtime WASM ?) avant réactivation — voir middleware-waf.yaml pour
# les directives complètes.
# - name: coraza-waf
- name: security-headers
+20
View File
@@ -27,6 +27,17 @@ rateLimit:
# https://github.com/traefik/traefik-helm-chart
# =============================================================
traefik:
# Injecté par le provisioner (voir deploy/chart-gestion/registry-credentials,
# installé dans le namespace traefik par EnsureSharedInfra) si un compte
# Docker Hub authentifié est configuré — évite le rate-limit de pull
# anonyme. Vide = pull anonyme (défaut). Sous "deployment", pas à la racine
# : c'est l'emplacement attendu par values.schema.json du chart officiel
# traefik/traefik (voir charts/traefik-41.1.0.tgz) — un imagePullSecrets à
# la racine est rejeté ("Additional property imagePullSecrets is not
# allowed").
deployment:
imagePullSecrets: []
# Expose les ports 80 et 443. Le port web redirige automatiquement vers
# websecure au niveau de l'entrypoint (pas besoin d'IngressRoute dédiée).
# Schéma valable pour le chart traefik/traefik >= 34 (vendored : 41.1.0,
@@ -96,8 +107,17 @@ traefik:
# prix de perdre l'IP réelle du client (SNAT par kube-proxy) — le
# rate-limiter/logs verront l'IP du nœud plutôt que celle du client tant
# qu'il n'y a pas de LoadBalancer (MetalLB) devant.
#
# type: ClusterIP — Traefik n'est plus exposé directement à internet
# depuis l'ajout du WAF ModSecurity natif devant lui (voir
# deploy/chart-gestion/waf-cluster, remplace coraza-http-wasm-traefik :
# bug mémoire non résolu upstream, jcchavezs/coraza-http-wasm-traefik#9).
# Le WAF termine le TLS public et rouvre une connexion vers l'entryPoint
# websecure (port 8443) de ce Service, désormais interne au cluster —
# aucun changement requis côté IngressRoute/entryPoints.
service:
spec:
type: ClusterIP
externalTrafficPolicy: Cluster
# Métriques Prometheus
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-waf-cluster
description: WAF ModSecurity/OWASP CRS natif (nginx, pas WASM) devant le Traefik partagé — remplace le plugin coraza-http-wasm-traefik (bug mémoire non résolu upstream, voir jcchavezs/coraza-http-wasm-traefik#9 et traefik/traefik#11436). Même image/pattern que docker/waf (omnex-prod).
type: application
version: 0.1.0
appVersion: "1.0.0"
@@ -0,0 +1,16 @@
{{- define "waf-cluster.fullname" -}}
{{ .Release.Name }}
{{- end -}}
{{- define "waf-cluster.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: Helm
{{- end -}}
{{- define "waf-cluster.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
@@ -0,0 +1,147 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "waf-cluster.fullname" . }}-conf
namespace: {{ .Release.Namespace }}
labels:
{{- include "waf-cluster.labels" . | nindent 4 }}
data:
app.conf: |
server {
listen 80;
listen [::]:80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name _;
server_tokens off;
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
client_max_body_size 10M;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
access_log /dev/stdout;
error_log /dev/stderr warn;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/custom-rules.conf;
# Terminaison TLS ici, puis nouvelle connexion TLS vers l'entryPoint
# websecure du Traefik partagé (deploy/chart-gestion/traefik) : aucun
# changement requis côté Traefik/IngressRoute, il continue de router
# par Host comme si le client s'y connectait directement. Host
# préservé pour que le routage par Host de Traefik fonctionne.
location / {
proxy_pass https://{{ .Values.upstream.host }}:{{ .Values.upstream.port }};
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
location = /healthz {
access_log off;
return 200 "ok";
add_header Content-Type text/plain;
}
}
custom-rules.conf: |
# Règles maison — le reste (SecRuleEngine, Include CRS, seuils de
# paranoïa) est déjà configuré par l'entrypoint de l'image
# owasp/modsecurity-crs à partir des variables d'env PARANOIA/
# ANOMALY_INBOUND/ANOMALY_OUTBOUND (voir values.yaml waf.*), même
# mécanisme que docker/waf (omnex-prod).
SecAuditLogParts ABIFHZ
SecRule IP:BANNED "@eq 1" \
"id:100000,phase:1,deny,status:403,log,\
msg:'IP is banned'"
SecRule IP:REPUTATION_SCORE "@ge 100" \
"id:100099,phase:1,deny,status:403,log,\
msg:'Critical reputation score',\
setvar:'ip.blocked=1',expirevar:'ip.blocked=86400'"
SecRule TX:SQL_INJECTION_SCORE "@ge 5" \
"id:100001,phase:2,deny,status:403,log,\
msg:'SQL Injection detected',\
setvar:'ip.banned=1',expirevar:'ip.banned=172800'"
SecRule TX:XSS_SCORE "@ge 5" \
"id:100010,phase:2,deny,status:403,log,\
msg:'XSS detected',\
setvar:'ip.banned=1',expirevar:'ip.banned=172800'"
SecRule TX:RCE_SCORE "@ge 5" \
"id:100020,phase:2,deny,status:403,log,\
msg:'RCE detected',\
setvar:'ip.banned=1',expirevar:'ip.banned=259200'"
SecRule TX:LFI_SCORE "@ge 5" \
"id:100030,phase:2,deny,status:403,log,\
msg:'LFI detected',\
setvar:'ip.banned=1',expirevar:'ip.banned=172800'"
SecRule TX:INBOUND_ANOMALY_SCORE "@ge 20" \
"id:100060,phase:2,deny,status:403,log,\
msg:'Critical anomaly score',\
setvar:'ip.banned=1',expirevar:'ip.banned=172800'"
# Suppressions CRS pour les routes légitimes de l'app démo (mêmes IDs
# que l'ancienne config coraza-http-wasm, voir
# deploy/chart-gestion/traefik/templates/middleware-waf.yaml — conservé
# à titre d'historique, plus utilisé, coraza-waf retiré de la chaîne).
SecRule REQUEST_URI "@streq /api/v2/admin/protected/products" \
"id:399001,phase:2,nolog,pass,ctl:ruleRemoveById=932235"
SecRule REQUEST_URI "@streq /api/v2/admin/protected/products" \
"id:399002,phase:2,nolog,pass,ctl:ruleRemoveById=920120,ctl:ruleRemoveById=920121"
SecRule REQUEST_URI "@streq /api/v1/panier/remove" \
"id:399010,phase:1,nolog,pass,ctl:ruleRemoveById=911100,ctl:ruleRemoveById=920350"
SecRule REQUEST_URI "@streq /api/v1/panier/clear" \
"id:399011,phase:1,nolog,pass,ctl:ruleRemoveById=911100,ctl:ruleRemoveById=920350"
# Fichiers uploadés — WAF désactivé (lecture seule, contenu déjà validé
# à l'upload).
SecRule REQUEST_URI "@beginsWith /uploads/" \
"id:1000,phase:1,pass,nolog,ctl:ruleEngine=Off"
# Rate limit basique par IP (indépendant du RateLimit du control-plane
# sur /auth/login, et du middleware rate-limit de Traefik en aval).
SecAction \
"id:400161,phase:1,nolog,pass,\
setvar:'ip.request_window_1sec=+1',\
expirevar:'ip.request_window_1sec=1'"
SecRule IP:REQUEST_WINDOW_1SEC "@gt 20" \
"id:400160,phase:1,deny,status:429,log,\
msg:'Too many requests'"
@@ -0,0 +1,105 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "waf-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "waf-cluster.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "waf-cluster.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "waf-cluster.selectorLabels" . | nindent 8 }}
annotations:
# Force un rollout des pods quand la ConfigMap (nginx.conf,
# custom-rules.conf) change — sinon nginx garde l'ancienne config en
# mémoire jusqu'au prochain redémarrage manuel (les changements de
# ConfigMap seule ne déclenchent pas de rollout par défaut).
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
# Pas de readOnlyRootFilesystem ici : l'image owasp/modsecurity-crs
# écrit à plusieurs endroits internes à /etc/nginx/modsec (activation
# des plugins CRS, génération de la config paranoia/anomaly) au
# démarrage — voir les logs "Running CRS plugin activation" /
# "Running CRS rule configuration". Même posture que docker/waf
# (omnex-prod), qui tourne avec cette même image sans souci. À
# durcir plus tard une fois le mapping exact des chemins écrits
# identifié (cf. la mésaventure readOnlyRootFilesystem sur le
# frontend, deploy/chart-gestion/frontend).
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: waf
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
env:
- name: DISABLE_MODSEC_ENV_SUBST
value: "true"
- name: PARANOIA
value: "{{ .Values.waf.paranoia }}"
- name: ANOMALY_INBOUND
value: "{{ .Values.waf.anomalyInbound }}"
- name: ANOMALY_OUTBOUND
value: "{{ .Values.waf.anomalyOutbound }}"
- name: MODSEC_AUDIT_LOG
value: "/var/log/modsec/modsec_audit.log"
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d/app.conf
subPath: app.conf
- name: conf
mountPath: /etc/nginx/modsec/custom-rules.conf
subPath: custom-rules.conf
- name: certs
mountPath: /etc/nginx/certs
readOnly: true
- name: modsec-log
mountPath: /var/log/modsec
livenessProbe:
httpGet:
path: /healthz
port: https
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /healthz
port: https
scheme: HTTPS
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: conf
configMap:
name: {{ include "waf-cluster.fullname" . }}-conf
- name: certs
secret:
secretName: {{ .Values.tls.secretName }}
- name: modsec-log
emptyDir: {}
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "waf-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "waf-cluster.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
# Cluster (pas Local) : même raison que Traefik (voir
# deploy/chart-gestion/traefik/values.yaml) — bare-metal multi-nœuds, tout
# nœud contacté doit pouvoir router vers le pod WAF où qu'il tourne.
externalTrafficPolicy: Cluster
selector:
{{- include "waf-cluster.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
- name: https
port: 443
targetPort: https
protocol: TCP
@@ -0,0 +1,45 @@
# WAF partagé (ModSecurity natif, nginx) devant le Traefik partagé — un seul
# déploiement pour toutes les démos, comme deploy/chart-gestion/traefik.
# Doit être installé dans le MÊME namespace que le Traefik partagé (accès au
# Secret TLS wildcard, pas de partage de Secret cross-namespace en k8s).
replicaCount: 2
image:
repository: owasp/modsecurity-crs
tag: nginx-alpine
pullPolicy: IfNotPresent
imagePullSecrets: []
# Certificat wildcard émis par cert-manager (voir deploy/cert-manager/), même
# Secret que consomme le TLSStore de Traefik (templates/tlsstore.yaml).
tls:
secretName: wildcard-demo-tls
# Service + port internes du Traefik partagé (voir deploy/chart-gestion/traefik) :
# ce WAF termine le TLS public puis rouvre une connexion TLS interne vers
# l'entryPoint websecure de Traefik (aucune modification requise côté
# IngressRoute/entryPoints — Traefik ne voit aucune différence).
upstream:
host: gestion-traefik.traefik.svc.cluster.local
# Port du Service Kubernetes "gestion-traefik" (websecure), pas le port
# interne du conteneur (8443, le Service l'abstrait derrière son propre
# port 443 — voir kubectl -n traefik get svc gestion-traefik).
port: 443
waf:
paranoia: 2
anomalyInbound: 5
anomalyOutbound: 4
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
service:
type: LoadBalancer