@@ -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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,197 @@
|
||||
# RBAC du control-plane (worker Go qui provisionne les démos) — moindre
|
||||
# privilège : ce ServiceAccount N'A PAS accès aux Secrets/Deployments/etc.
|
||||
# des autres namespaces (cert-manager, longhorn-system, kube-system...).
|
||||
#
|
||||
# Principe :
|
||||
# - omnex-namespace-manager (ClusterRole) : gérer les objets Namespace eux-
|
||||
# mêmes. Cluster-scoped par nature (RBAC ne permet pas de restreindre par
|
||||
# préfixe de nom "demo-*"/"premium-*").
|
||||
# - omnex-demo-workload-manager (ClusterRole) : tout ce dont les charts
|
||||
# Helm ont besoin (Deployment, StatefulSet, Secret, ConfigMap, PVC,
|
||||
# NetworkPolicy, ResourceQuota, LimitRange, HPA, les CRD Traefik). JAMAIS
|
||||
# liée cluster-wide — seulement via un RoleBinding créé par le code Go
|
||||
# lui-même dans CHAQUE namespace qu'il crée (voir
|
||||
# control-plane/api/internal/demos/helm_provisioner.go:createNamespace/
|
||||
# ensureWorkloadRoleBinding). C'est ce qui borne l'accès à un seul
|
||||
# namespace à la fois.
|
||||
# - omnex-rbac-binder (ClusterRole) : permet au ServiceAccount de créer ce
|
||||
# RoleBinding lui-même, dans n'importe quel namespace (obligatoire :
|
||||
# les namespaces de démo n'existent pas encore au moment où ce fichier
|
||||
# est appliqué). Verbe "bind" scopé par resourceName à
|
||||
# omnex-demo-workload-manager UNIQUEMENT — mécanisme RBAC standard
|
||||
# anti-escalade : posséder "bind" sur une ClusterRole précise autorise à
|
||||
# la référencer dans un RoleBinding sans posséder soi-même les droits
|
||||
# qu'elle contient. Le ServiceAccount ne peut donc lier AUCUNE autre
|
||||
# ClusterRole (ex: cluster-admin) à lui-même.
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: omnex-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: omnex-control-plane
|
||||
namespace: omnex-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: omnex-demo-workload-manager
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- configmaps
|
||||
- secrets
|
||||
- services
|
||||
- persistentvolumeclaims
|
||||
- resourcequotas
|
||||
- limitranges
|
||||
# Le subchart officiel traefik/traefik crée son propre ServiceAccount
|
||||
# (nom = release Helm) — pas utilisé par nos propres charts
|
||||
# (backend/frontend/postgresql/redis/lbtelegram), mais nécessaire pour
|
||||
# que "helm install traefik" passe.
|
||||
- serviceaccounts
|
||||
verbs: [get, list, watch, create, update, patch, delete]
|
||||
- apiGroups: [""]
|
||||
resources: [pods]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups: [""]
|
||||
# Exec dans le pod postgres pour pg_dump/restore lors d'un passage en
|
||||
# premium (voir HelmProvisioner.execInPod / migratePostgresData).
|
||||
resources: [pods/exec]
|
||||
verbs: [create]
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
# Usage CPU/mémoire live affiché dans le back-office (voir
|
||||
# HelmProvisioner.GetResourceState) — best-effort côté Go (montre juste
|
||||
# la phase du pod si absent), mais sans ce droit c'est TOUJOURS absent
|
||||
# (Forbidden, pas juste "metrics-server indisponible").
|
||||
resources: [pods]
|
||||
verbs: [get, list]
|
||||
- apiGroups: ["apps"]
|
||||
resources: [deployments, statefulsets]
|
||||
verbs: [get, list, watch, create, update, patch, delete]
|
||||
- apiGroups: ["apps"]
|
||||
# Lecture seule : "helm --wait" liste les ReplicaSet d'un Deployment pour
|
||||
# vérifier l'état du rollout (jamais créés/gérés directement par nous).
|
||||
resources: [replicasets]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups: ["autoscaling"]
|
||||
resources: [horizontalpodautoscalers]
|
||||
verbs: [get, list, watch, create, update, patch, delete]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: [networkpolicies]
|
||||
verbs: [get, list, watch, create, update, patch, delete]
|
||||
- apiGroups: ["traefik.io"]
|
||||
resources: [ingressroutes, middlewares, tlsstores]
|
||||
verbs: [get, list, watch, create, update, patch, delete]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: omnex-namespace-manager
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [namespaces]
|
||||
verbs: [get, list, watch, create, delete]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: omnex-rbac-binder
|
||||
rules:
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: [rolebindings]
|
||||
verbs: [get, list, create]
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: [clusterroles]
|
||||
resourceNames: [omnex-demo-workload-manager]
|
||||
verbs: [bind]
|
||||
# Lecture seule, cluster-wide : nécessaire pour que "helm upgrade --install"
|
||||
# du chart traefik partagé (voir EnsureSharedInfra) puisse vérifier l'état
|
||||
# du ClusterRole/ClusterRoleBinding que le subchart officiel traefik/traefik
|
||||
# crée pour SA PROPRE identité ("gestion-traefik-traefik", pas la nôtre).
|
||||
# Ne permet ni de créer ni de modifier quoi que ce soit — seulement de
|
||||
# constater qu'ils existent déjà (installés une fois par un opérateur admin,
|
||||
# voir le bootstrap manuel documenté dans le plan de déploiement).
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: [clusterroles, clusterrolebindings]
|
||||
verbs: [get, list, watch]
|
||||
# Même besoin : le subchart officiel traefik/traefik crée aussi son propre
|
||||
# IngressClass (ressource cluster-scoped), déjà installée une fois par un
|
||||
# opérateur admin — lecture seule ici aussi.
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: [ingressclasses]
|
||||
verbs: [get, list, watch]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: omnex-control-plane-namespaces
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: omnex-control-plane
|
||||
namespace: omnex-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: omnex-namespace-manager
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: omnex-control-plane-rbac-binder
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: omnex-control-plane
|
||||
namespace: omnex-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: omnex-rbac-binder
|
||||
---
|
||||
# Token longue durée : le control-plane tourne en docker-compose (hors du
|
||||
# cluster) avec un kubeconfig statique monté en fichier, pas de rotation
|
||||
# automatique de type projected-token. Voir ansible/playbook-deploy-site.yml.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: omnex-control-plane-token
|
||||
namespace: omnex-system
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: omnex-control-plane
|
||||
type: kubernetes.io/service-account-token
|
||||
---
|
||||
# Role namespacé (pas de RoleBinding auto-créé ici : "velero" est un
|
||||
# namespace fixe préexistant, pas un namespace de démo créé dynamiquement
|
||||
# par le control-plane — pas besoin du mécanisme "bind" utilisé pour
|
||||
# omnex-demo-workload-manager). Portée strictement limitée aux Schedule/
|
||||
# Backup Velero — aucun accès aux Secrets (dont cloud-credentials, la clé
|
||||
# du bucket S3) ni au reste du namespace velero.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: omnex-control-plane
|
||||
namespace: velero
|
||||
rules:
|
||||
- apiGroups: ["velero.io"]
|
||||
# Un Schedule + les Backup qu'il crée par démo (voir
|
||||
# HelmProvisioner.ensureDemoBackupSchedule/deleteDemoBackups) — jamais
|
||||
# de wildcard multi-namespace, pour pouvoir supprimer les sauvegardes
|
||||
# d'UNE démo sans toucher aux autres.
|
||||
resources: [schedules, backups]
|
||||
verbs: [get, list, watch, create, delete]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: omnex-control-plane
|
||||
namespace: velero
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: omnex-control-plane
|
||||
namespace: omnex-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: omnex-control-plane
|
||||
+23
-35
@@ -1,36 +1,24 @@
|
||||
# Sauvegarde quotidienne de tous les namespaces de démo (postgres, redis,
|
||||
# uploads, et le reste : Secrets/ConfigMaps/Deployments pour une
|
||||
# restauration complète) vers le bucket S3 configuré dans values.yaml.
|
||||
# S'applique par défaut à TOUT namespace non exclu ci-dessous — une nouvelle
|
||||
# démo créée dynamiquement (demo-xxxxx, premium-xxxxx) est donc couverte
|
||||
# automatiquement, sans modifier ce fichier.
|
||||
# Il n'y a plus de Schedule Velero statique/global ici (l'ancienne version de
|
||||
# ce fichier en définissait deux : demos-daily et demos-frequent, couvrant
|
||||
# TOUS les namespaces en un seul objet Backup par exécution).
|
||||
#
|
||||
# À appliquer après l'installation de Velero (voir values.yaml) :
|
||||
# kubectl apply -f deploy/velero/schedule.yml
|
||||
apiVersion: velero.io/v1
|
||||
kind: Schedule
|
||||
metadata:
|
||||
name: demos-daily
|
||||
namespace: velero
|
||||
spec:
|
||||
# 03h00 UTC, tous les jours — heure creuse pour ce cluster.
|
||||
schedule: "0 3 * * *"
|
||||
template:
|
||||
includedNamespaces:
|
||||
- "*"
|
||||
excludedNamespaces:
|
||||
- kube-system
|
||||
- kube-public
|
||||
- kube-node-lease
|
||||
- default
|
||||
- velero
|
||||
- traefik
|
||||
- longhorn-system
|
||||
- cert-manager
|
||||
# Sauvegarde le contenu réel des PVC (postgres/redis/uploads) via
|
||||
# File System Backup — voir values.yaml.
|
||||
defaultVolumesToFsBackup: true
|
||||
storageLocation: default
|
||||
# Conservation 30 jours : au-delà, la sauvegarde devient éligible à la
|
||||
# purge automatique (garbage collection Velero).
|
||||
ttl: 720h0m0s
|
||||
# Pourquoi ce changement : Velero ne permet de supprimer qu'un objet Backup
|
||||
# ENTIER, jamais une portion (ex: "juste les données de la démo X"). Avec un
|
||||
# planning global multi-namespace, détruire une démo n'aurait donc jamais pu
|
||||
# supprimer ses sauvegardes sans supprimer aussi celles des autres démos
|
||||
# présentes dans le même Backup.
|
||||
#
|
||||
# À la place, le control-plane crée maintenant un Schedule Velero DÉDIÉ par
|
||||
# démo (voir HelmProvisioner.ensureDemoBackupSchedule dans
|
||||
# control-plane/api/internal/demos/helm_provisioner.go) :
|
||||
# - à la création de la démo (Provision) et lors d'un passage en premium
|
||||
# (MigrateToPremiumNamespace, un nouveau namespace = un nouveau planning) ;
|
||||
# - toutes les 15 min, conservées 24h (fenêtre de perte de données
|
||||
# minimale en cas de problème sur le cluster) ;
|
||||
# - supprimé avec ses Backup déjà pris (HelmProvisioner.deleteDemoBackups)
|
||||
# quand la démo est détruite — expiration du TTL ou destruction forcée
|
||||
# par un admin, les deux passent par Service.Delete → Teardown.
|
||||
#
|
||||
# Rien à appliquer manuellement ici : ces Schedule sont gérés entièrement
|
||||
# par le control-plane, dans le namespace "velero", nommés
|
||||
# "demo-backup-<namespace>".
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
# helm install velero vmware-tanzu/velero \
|
||||
# --namespace velero --version 12.1.0 \
|
||||
# -f deploy/velero/values.yaml
|
||||
# kubectl apply -f deploy/velero/schedule.yml
|
||||
# Rien d'autre à appliquer : les plannings de sauvegarde sont créés
|
||||
# dynamiquement par démo par le control-plane, voir deploy/velero/schedule.yml.
|
||||
#
|
||||
# 5. Sécurité du bucket — à vérifier CÔTÉ RustFS/MinIO (rien de tout ça ne
|
||||
# se configure depuis ce repo) : chaque sauvegarde contient les Secrets
|
||||
@@ -68,21 +69,23 @@ configuration:
|
||||
- name: default
|
||||
provider: aws
|
||||
# Nom du bucket créé à l'étape 1. Requis.
|
||||
bucket: "CHANGE_ME_bucket_name"
|
||||
bucket: "backup-omnex"
|
||||
default: true
|
||||
credential:
|
||||
name: cloud-credentials
|
||||
key: cloud
|
||||
config:
|
||||
region: "us-east-1"
|
||||
s3Url: "CHANGE_ME_https://votre-endpoint-rustfs-ou-minio"
|
||||
s3Url: "https://backup-omnex.uber-stup.club"
|
||||
s3ForcePathStyle: "true"
|
||||
insecureSkipTLSVerify: "false"
|
||||
# Chiffrement côté serveur du contenu des sauvegardes (SSE-S3,
|
||||
# AES256 géré par le bucket) — RustFS et MinIO le supportent tous
|
||||
# les deux. Ne dispense pas de vérifier le chiffrement at-rest réel
|
||||
# du stockage sous-jacent (voir étape 5 ci-dessus).
|
||||
serverSideEncryption: "AES256"
|
||||
# Pas de serverSideEncryption ici : ce RustFS refuse le SSE-S3 tant
|
||||
# que RUSTFS_SSE_S3_MASTER_KEY (clé base64 32 octets) n'est pas
|
||||
# configurée côté serveur ("InvalidRequest: SSE-S3 requires
|
||||
# RUSTFS_SSE_S3_MASTER_KEY..."), réglage hors de portée depuis ce
|
||||
# repo. Le transport reste chiffré (HTTPS) ; pour du chiffrement
|
||||
# at-rest, configurer cette variable côté serveur RustFS puis
|
||||
# remettre serverSideEncryption: "AES256" ici.
|
||||
|
||||
volumeSnapshotLocation: []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user