chore: add helm chart and docker

This commit is contained in:
Nuxgrid
2026-07-27 21:06:11 +02:00
parent 23a4f280c6
commit 356604fba4
46 changed files with 1390 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-backend
description: Go API backend — gestion-commande
type: application
version: 0.1.0
appVersion: "1.0.0"
+47
View File
@@ -0,0 +1,47 @@
# =========================================================
# Stage 1: Build Go
# =========================================================
FROM golang:1.24-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates tzdata gcc musl-dev
COPY backend/gestion/go.mod backend/gestion/go.sum ./
ENV GOPROXY=https://proxy.golang.org,direct
ENV GOSUMDB=sum.golang.org
ENV CGO_ENABLED=0
RUN go mod download
COPY backend/gestion/ .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-w -s -X main.Version=1.0.0 -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o /app/server .
# =========================================================
# Stage 2: Runtime
# =========================================================
FROM alpine:latest AS runtime
RUN apk --no-cache add ca-certificates tzdata wget
RUN addgroup -g 101 app && adduser -u 101 -S app -G app
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY helm/backend/entrypoint.sh .
RUN chmod +x entrypoint.sh
RUN mkdir -p /app/uploads/images /app/uploads/videos && \
chown -R app:app /app
USER app
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -e
# Créer le dossier uploads s'il n'existe pas (le volume le crée en root)
# Puis créer les sous-dossiers
if [ ! -d "/app/uploads" ]; then
mkdir -p /app/uploads
fi
# Créer les sous-répertoires
mkdir -p /app/uploads/images /app/uploads/videos 2>/dev/null || true
echo "✅ Répertoires uploads prêts"
# Lancer l'application
exec ./server
@@ -0,0 +1,21 @@
{{- define "backend.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "backend.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "backend.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "backend.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "backend.selectorLabels" -}}
app.kubernetes.io/name: {{ include "backend.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "backend.fullname" . }}
labels:
{{- include "backend.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "backend.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "backend.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: backend
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: http
containerPort: 8080
protocol: TCP
envFrom:
- secretRef:
name: {{ include "backend.fullname" . }}-secrets
env:
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.persistence.uploads.enabled }}
volumeMounts:
- name: uploads
mountPath: /app/uploads
{{- end }}
livenessProbe:
httpGet:
path: /api/v1/app-settings
port: http
initialDelaySeconds: 15
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/v1/app-settings
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.persistence.uploads.enabled }}
volumes:
- name: uploads
persistentVolumeClaim:
claimName: {{ include "backend.fullname" . }}-uploads
{{- end }}
@@ -0,0 +1,22 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "backend.fullname" . }}
labels:
{{- include "backend.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "backend.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
@@ -0,0 +1,17 @@
{{- if .Values.persistence.uploads.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "backend.fullname" . }}-uploads
labels:
{{- include "backend.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.uploads.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.uploads.size }}
{{- if .Values.persistence.uploads.storageClass }}
storageClassName: {{ .Values.persistence.uploads.storageClass }}
{{- end }}
{{- end }}
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "backend.fullname" . }}-secrets
labels:
{{- include "backend.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- range $k, $v := .Values.secrets }}
{{ $k }}: {{ $v | quote }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "backend.fullname" . }}
labels:
{{- include "backend.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "backend.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
+70
View File
@@ -0,0 +1,70 @@
replicaCount: 2
image:
repository: backend-mln
tag: helm
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 70
persistence:
uploads:
enabled: true
size: 5Gi
storageClass: ""
accessMode: ReadWriteOnce
env:
DB_HOST: postgres
DB_PORT: "5432"
DB_USER: postgres
DB_NAME: gestion_db
DB_SSLMODE: disable
REDIS_HOST: redis
REDIS_PORT: "6379"
API_PORT: "8080"
# Valeurs sensibles — à surcharger via --set-string ou un Secret Manager externe
secrets:
DB_PASSWORD: ""
SESSION_SECRET: ""
USER_JWT_SECRET: ""
USER_JWT_SECRET_OLD: ""
ADMIN_JWT_SECRET: ""
ADMIN_JWT_SECRET_OLD: ""
REDIS_PASSWORD: ""
TOMTOM_API_KEY: ""
TELEGRAM_WEBHOOK_URL: ""
TELEGRAM_WEBHOOK_SECRET: ""
NOWPAYMENTS_IPN_SECRET: ""
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "▶ Build"
docker build -f "$SCRIPT_DIR/frontend/Dockerfile" -t gestion-frontend:latest "$PROJECT_ROOT"
echo "▶ Export"
docker save gestion-frontend:latest -o /tmp/gestion-frontend.tar
for NODE in 192.168.1.43 192.168.1.44 192.168.1.45; do
echo "$NODE"
ssh xor@$NODE "sudo k3s ctr images rm docker.io/library/gestion-frontend:latest 2>/dev/null; true"
scp /tmp/gestion-frontend.tar xor@$NODE:/tmp/
ssh xor@$NODE "sudo k3s ctr images import /tmp/gestion-frontend.tar && rm /tmp/gestion-frontend.tar"
done
rm -f /tmp/gestion-frontend.tar
echo "✓ Done"
+15
View File
@@ -0,0 +1,15 @@
:8080 {
root * /srv
encode gzip
# SPA fallback — toutes les routes inconnues renvoient index.html
try_files {path} /index.html
file_server
header {
-Server
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
}
}
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-frontend
description: React/Vite frontend — gestion-commande
type: application
version: 0.1.0
appVersion: "1.0.0"
+28
View File
@@ -0,0 +1,28 @@
# =========================================================
# Stage 1: Build React/Vite
# =========================================================
FROM node:22-alpine AS builder
WORKDIR /app
COPY frontend-prep/package.json frontend-prep/package-lock.json* ./
RUN npm ci --ignore-scripts
COPY frontend-prep/ .
RUN npm run build
# =========================================================
# Stage 2: Caddy — SPA static server (TLS terminé par Traefik)
# =========================================================
FROM caddy:alpine AS runtime
# Retire la file capability CAP_NET_BIND_SERVICE — incompatible avec
# allowPrivilegeEscalation: false. On écoute sur 8080 à la place.
RUN apk add --no-cache libcap && setcap -r /usr/bin/caddy && apk del libcap
COPY helm/frontend/Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /app/dist /srv
EXPOSE 8080
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
@@ -0,0 +1,21 @@
{{- define "frontend.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "frontend.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "frontend.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "frontend.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "frontend.selectorLabels" -}}
app.kubernetes.io/name: {{ include "frontend.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "frontend.fullname" . }}-config
labels:
{{- include "frontend.labels" . | nindent 4 }}
data:
config.js: |
window.__APP_CONFIG__ = {
apiUrl: {{ .Values.apiUrl | quote }}
};
@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "frontend.fullname" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "frontend.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "frontend.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: frontend
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: http
containerPort: 8080
protocol: TCP
volumeMounts:
- name: tmp
mountPath: /tmp
- name: caddy-data
mountPath: /data
- name: app-config
mountPath: /srv/config.js
subPath: config.js
readOnly: true
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: tmp
emptyDir: {}
- name: caddy-data
emptyDir: {}
- name: app-config
configMap:
name: {{ include "frontend.fullname" . }}-config
@@ -0,0 +1,22 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "frontend.fullname" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "frontend.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "frontend.fullname" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "frontend.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
+38
View File
@@ -0,0 +1,38 @@
replicaCount: 2
apiUrl: ""
image:
repository: frontend-mln
tag: helm
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podSecurityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-postgresql
description: PostgreSQL 16 — gestion-commande
type: application
version: 0.1.0
appVersion: "16"
@@ -0,0 +1,16 @@
{{- define "postgresql.fullname" -}}
{{- printf "%s-postgresql" .Release.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "postgresql.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: postgresql
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "postgresql.selectorLabels" -}}
app.kubernetes.io/name: postgresql
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,20 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "postgresql.fullname" . }}-config
labels:
{{- include "postgresql.labels" . | nindent 4 }}
data:
postgresql.conf: |
max_connections = {{ .Values.config.maxConnections }}
shared_buffers = {{ .Values.config.sharedBuffers }}
effective_cache_size = {{ .Values.config.effectiveCacheSize }}
maintenance_work_mem = {{ .Values.config.maintenanceWorkMem }}
wal_buffers = {{ .Values.config.walBuffers }}
default_statistics_target = {{ .Values.config.defaultStatisticsTarget }}
random_page_cost = {{ .Values.config.randomPageCost }}
log_min_duration_statement = {{ .Values.config.logMinDurationStatement }}
log_timezone = 'UTC'
timezone = 'UTC'
# Connexions sécurisées uniquement depuis le réseau interne cluster
listen_addresses = '*'
@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "postgresql.fullname" . }}-data
labels:
{{- include "postgresql.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "postgresql.fullname" . }}
labels:
{{- include "postgresql.labels" . | nindent 4 }}
type: Opaque
stringData:
POSTGRES_PASSWORD: {{ .Values.auth.password | quote }}
POSTGRES_USER: {{ .Values.auth.username | quote }}
POSTGRES_DB: {{ .Values.auth.database | quote }}
@@ -0,0 +1,32 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "postgresql.fullname" . }}
labels:
{{- include "postgresql.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "postgresql.selectorLabels" . | nindent 4 }}
ports:
- name: postgresql
port: {{ .Values.service.port }}
targetPort: postgresql
protocol: TCP
---
# Service headless pour le StatefulSet
apiVersion: v1
kind: Service
metadata:
name: {{ include "postgresql.fullname" . }}-headless
labels:
{{- include "postgresql.labels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
selector:
{{- include "postgresql.selectorLabels" . | nindent 4 }}
ports:
- name: postgresql
port: {{ .Values.service.port }}
targetPort: postgresql
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "postgresql.fullname" . }}
labels:
{{- include "postgresql.labels" . | nindent 4 }}
spec:
serviceName: {{ include "postgresql.fullname" . }}-headless
replicas: 1
selector:
matchLabels:
{{- include "postgresql.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "postgresql.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: postgresql
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: postgresql
containerPort: 5432
protocol: TCP
envFrom:
- secretRef:
name: {{ include "postgresql.fullname" . }}
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /etc/postgresql/postgresql.conf
subPath: postgresql.conf
args:
- "-c"
- "config_file=/etc/postgresql/postgresql.conf"
livenessProbe:
exec:
command:
- sh
- -c
- pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 6
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ include "postgresql.fullname" . }}-data
- name: config
configMap:
name: {{ include "postgresql.fullname" . }}-config
@@ -0,0 +1,50 @@
image:
repository: postgres
tag: "16-alpine"
pullPolicy: IfNotPresent
auth:
username: postgres
database: gestion_db
# À surcharger via --set auth.password=xxx
password: ""
persistence:
size: 10Gi
storageClass: ""
accessMode: ReadWriteOnce
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
service:
type: ClusterIP
port: 5432
# Paramètres PostgreSQL (postgresql.conf)
config:
maxConnections: "200"
sharedBuffers: "256MB"
effectiveCacheSize: "768MB"
maintenanceWorkMem: "64MB"
walBuffers: "16MB"
defaultStatisticsTarget: "100"
randomPageCost: "1.1"
logMinDurationStatement: "1000"
podSecurityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: gestion-redis
description: Redis 7 — gestion-commande
type: application
version: 0.1.0
appVersion: "7"
@@ -0,0 +1,16 @@
{{- define "redis.fullname" -}}
{{- printf "%s-redis" .Release.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "redis.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: redis
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "redis.selectorLabels" -}}
app.kubernetes.io/name: redis
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,20 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "redis.fullname" . }}-config
labels:
{{- include "redis.labels" . | nindent 4 }}
data:
redis.conf: |
maxmemory {{ .Values.config.maxmemory }}
maxmemory-policy {{ .Values.config.maxmemoryPolicy }}
appendonly {{ .Values.config.appendonly }}
appendfsync {{ .Values.config.appendfsync }}
{{- if .Values.config.save }}
save {{ .Values.config.save }}
{{- else }}
save ""
{{- end }}
protected-mode no
loglevel notice
# Le mot de passe est injecté au démarrage via --requirepass
@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "redis.fullname" . }}-data
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "redis.fullname" . }}
labels:
{{- include "redis.labels" . | nindent 4 }}
type: Opaque
stringData:
REDIS_PASSWORD: {{ .Values.auth.password | quote }}
@@ -0,0 +1,31 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis.fullname" . }}
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "redis.selectorLabels" . | nindent 4 }}
ports:
- name: redis
port: {{ .Values.service.port }}
targetPort: redis
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis.fullname" . }}-headless
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
selector:
{{- include "redis.selectorLabels" . | nindent 4 }}
ports:
- name: redis
port: {{ .Values.service.port }}
targetPort: redis
@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "redis.fullname" . }}
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
serviceName: {{ include "redis.fullname" . }}-headless
replicas: 1
selector:
matchLabels:
{{- include "redis.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "redis.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: redis
containerPort: 6379
protocol: TCP
command:
- redis-server
- /etc/redis/redis.conf
- "--requirepass"
- "$(REDIS_PASSWORD)"
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "redis.fullname" . }}
key: REDIS_PASSWORD
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/redis/redis.conf
subPath: redis.conf
- name: tmp
mountPath: /tmp
livenessProbe:
exec:
command:
- sh
- -c
- redis-cli --no-auth-warning -a $REDIS_PASSWORD ping | grep PONG
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 6
readinessProbe:
exec:
command:
- sh
- -c
- redis-cli --no-auth-warning -a $REDIS_PASSWORD ping | grep PONG
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ include "redis.fullname" . }}-data
- name: config
configMap:
name: {{ include "redis.fullname" . }}-config
- name: tmp
emptyDir: {}
+46
View File
@@ -0,0 +1,46 @@
image:
repository: redis
tag: "7-alpine"
pullPolicy: IfNotPresent
auth:
# À surcharger via --set auth.password=xxx
password: ""
persistence:
size: 2Gi
storageClass: ""
accessMode: ReadWriteOnce
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
service:
type: ClusterIP
port: 6379
config:
maxmemory: "256mb"
# allkeys-lru : expulse les clés les moins récemment utilisées quand la mémoire est pleine
maxmemoryPolicy: "allkeys-lru"
appendonly: "yes"
appendfsync: "everysec"
# Désactiver les snapshots RDB (AOF suffit)
save: ""
podSecurityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v2
name: gestion-traefik
description: Traefik ingress + Coraza WAF (OWASP CRS) — gestion-commande
type: application
version: 0.1.0
appVersion: "3.1.0"
dependencies:
- name: traefik
version: ">=30.0.0"
repository: https://helm.traefik.io/traefik
@@ -0,0 +1,12 @@
{{- define "gestion-traefik.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "gestion-traefik.certResolver" -}}
{{- if eq .Values.tls.mode "letsencrypt" -}}
letsencrypt
{{- end }}
{{- end }}
@@ -0,0 +1,84 @@
---
# HTTP → HTTPS redirect
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gestion-http
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
entryPoints:
- web
routes:
- match: Host(`{{ .Values.domain }}`)
kind: Rule
middlewares:
- name: redirect-https
services:
- name: noop@internal
kind: TraefikService
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: {{ .Release.Namespace }}
spec:
redirectScheme:
scheme: https
permanent: true
---
# HTTPS — routes principales
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gestion-https
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
entryPoints:
- websecure
routes:
# API backend — WAF + headers + rate limit
- match: Host(`{{ .Values.domain }}`) && (PathPrefix(`/api/`) || PathPrefix(`/webhook/`))
kind: Rule
priority: 20
middlewares:
- name: waf-chain
services:
- name: {{ .Values.backend.serviceName }}
namespace: {{ .Values.servicesNamespace }}
port: {{ .Values.backend.servicePort }}
# Fichiers uploadés — WAF désactivé dans les règles Coraza pour ce path
- match: Host(`{{ .Values.domain }}`) && PathPrefix(`/uploads/`)
kind: Rule
priority: 15
middlewares:
- name: security-headers
services:
- name: {{ .Values.backend.serviceName }}
namespace: {{ .Values.servicesNamespace }}
port: {{ .Values.backend.servicePort }}
# Frontend SPA — WAF + headers + rate limit
- match: Host(`{{ .Values.domain }}`)
kind: Rule
priority: 1
middlewares:
- name: waf-chain
services:
- name: {{ .Values.frontend.serviceName }}
namespace: {{ .Values.servicesNamespace }}
port: {{ .Values.frontend.servicePort }}
tls:
{{- if eq .Values.tls.mode "letsencrypt" }}
certResolver: letsencrypt
{{- else }}
secretName: {{ .Values.tls.secretName }}
{{- end }}
domains:
- main: {{ .Values.domain }}
@@ -0,0 +1,13 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: waf-chain
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
chain:
middlewares:
- name: rate-limit
- name: coraza-waf
- name: security-headers
@@ -0,0 +1,27 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
frameDeny: true
contentTypeNosniff: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "geolocation=(), microphone=(), camera=(), payment=()"
customResponseHeaders:
X-XSS-Protection: "0"
Content-Security-Policy: >-
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data:;
connect-src 'self' https:;
frame-ancestors 'self';
Server: ""
@@ -0,0 +1,17 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
rateLimit:
average: {{ .Values.rateLimit.average }}
burst: {{ .Values.rateLimit.burst }}
period: 1m
sourceCriterion:
ipStrategy:
# depth=1 : utilise la première IP de X-Forwarded-For
# (Traefik pose lui-même ce header depuis la vraie IP TCP, non spoofable)
depth: 1
@@ -0,0 +1,61 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: coraza-waf
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
plugin:
coraza-traefik:
directives:
# -------------------------------------------------------
# Moteur et body inspection
# -------------------------------------------------------
- "SecRuleEngine On"
- "SecRequestBodyAccess On"
- "SecRequestBodyLimit 104857600"
- "SecRequestBodyInMemoryLimit 131072"
- "SecResponseBodyAccess Off"
- "SecDebugLogLevel 0"
# -------------------------------------------------------
# OWASP CRS — inclus dans le plugin Coraza
# -------------------------------------------------------
- "Include @coraza.conf-recommended"
- "Include @crs-setup.conf.example"
- "Include @owasp_crs/*.conf"
# -------------------------------------------------------
# Niveau de paranoia {{ .Values.waf.paranoia }}
# -------------------------------------------------------
- "SecAction \"id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level={{ .Values.waf.paranoia }}\""
- "SecAction \"id:900001,phase:1,nolog,pass,t:none,setvar:tx.inbound_anomaly_score_threshold={{ .Values.waf.anomalyInbound }}\""
- "SecAction \"id:900002,phase:1,nolog,pass,t:none,setvar:tx.outbound_anomaly_score_threshold={{ .Values.waf.anomalyOutbound }}\""
# -------------------------------------------------------
# Suppressions CRS pour les routes légitimes
# -------------------------------------------------------
- "SecRuleRemoveById 932235"
- "SecRuleRemoveById 911100"
- "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\""
# Désactiver le WAF sur les uploads (fichiers statiques en lecture)
- "SecRule REQUEST_URI \"@beginsWith /uploads/\" \"id:1000,phase:1,pass,nolog,ctl:ruleEngine=Off\""
# -------------------------------------------------------
# IP banning automatique sur attaques détectées
# Note : en-mémoire par pod — pour un ban distribué utiliser Redis + Coraza persistence
# -------------------------------------------------------
- "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'\""
- "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'\""
@@ -0,0 +1,16 @@
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: {{ .Release.Namespace }}
labels:
{{- include "gestion-traefik.labels" . | nindent 4 }}
spec:
{{- if eq .Values.tls.mode "secret" }}
defaultCertificate:
secretName: {{ .Values.tls.secretName }}
{{- end }}
defaultGeneratedCert:
resolver: {{ include "gestion-traefik.certResolver" . | default "letsencrypt" }}
domain:
main: {{ .Values.domain }}
+107
View File
@@ -0,0 +1,107 @@
# =============================================================
# Paramètres spécifiques à ce chart (IngressRoutes, Middlewares)
# =============================================================
# Namespace des services backend et frontend
servicesNamespace: gestion
domain: example.com
tls:
# "letsencrypt" | "secret"
mode: letsencrypt
email: admin@example.com
# Si mode=secret : nom du Secret TLS Kubernetes
secretName: gestion-tls
backend:
serviceName: gestion-backend
servicePort: 8080
frontend:
serviceName: gestion-frontend
servicePort: 80
waf:
paranoia: 2
anomalyInbound: 5
anomalyOutbound: 4
rateLimit:
# requêtes moyennes par seconde par IP
average: 20
burst: 50
# =============================================================
# Valeurs passées au subchart officiel traefik
# https://github.com/traefik/traefik-helm-chart
# =============================================================
traefik:
# Expose les ports 80 et 443
ports:
web:
port: 8000
expose:
default: true
redirectTo:
port: websecure
websecure:
port: 8443
expose:
default: true
tls:
enabled: true
# Logs
logs:
general:
level: INFO
access:
enabled: true
# Plugin Coraza WAF — Traefik télécharge le plugin au démarrage
# (le cluster doit avoir accès à internet ou utiliser un plugin local)
experimental:
plugins:
coraza-traefik:
moduleName: "github.com/corazawaf/coraza-traefik"
version: "v0.3.0"
# Cert resolver Let's Encrypt (ignoré si tls.mode=secret)
certResolvers:
letsencrypt:
email: admin@example.com
storage: /data/acme.json
httpChallenge:
entryPoint: web
# ACME storage persistant
persistence:
enabled: true
size: 128Mi
# Traefik lit les IngressRoutes dans tous les namespaces
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: true
# IP réelle via X-Forwarded-For (profondeur 1 = proxy direct)
# Le rate-limiter et les logs utilisent cette IP
service:
spec:
externalTrafficPolicy: Local
# Métriques Prometheus
metrics:
prometheus:
enabled: true
# Ressources
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
+73
View File
@@ -0,0 +1,73 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-omnex}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-omnex}
POSTGRES_DB: ${POSTGRES_DB:-omnex}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-omnex} -d ${POSTGRES_DB:-omnex}"]
interval: 5s
timeout: 3s
retries: 10
# Pas de port exposé : accès interne uniquement (défense en profondeur).
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-omnexredis}", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-omnexredis}", "ping"]
interval: 5s
timeout: 3s
retries: 10
web:
image: xor1234/omnex-web:latest
restart: unless-stopped
depends_on:
api:
condition: service_healthy
ports:
- "3000:80"
api:
image: xor1234/omnex-api:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
OMNEX_ENV: ${OMNEX_ENV:-dev}
OMNEX_ADDR: ":8080"
OMNEX_JWT_SECRET: ${OMNEX_JWT_SECRET}
OMNEX_ALLOWED_ORIGINS: ${OMNEX_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}
OMNEX_DATABASE_URL: "host=postgres user=${POSTGRES_USER:-omnex} password=${POSTGRES_PASSWORD:-omnex} dbname=${POSTGRES_DB:-omnex} port=5432 sslmode=disable"
OMNEX_REDIS_URL: "redis://:${REDIS_PASSWORD:-omnexredis}@redis:6379/0"
OMNEX_SEED_USERNAME: ${OMNEX_SEED_USERNAME:-admin}
OMNEX_SEED_PASSWORD: ${OMNEX_SEED_PASSWORD}
OMNEX_DEMO_DOMAIN: ${OMNEX_DEMO_DOMAIN:-demo.omnex.app}
FRONTEND_IMAGE_APP: ${FRONTEND_IMAGE_APP:-xor1234/frontend-mln:latest}
BACKEND_IMAGE_APP: ${BACKEND_IMAGE_APP:-xor1234/backend-mln:latest}
KUBECONFIG: /kubeconfig/config
volumes:
- ./deploy/chart-gestion:/charts:ro
- /home/xor_fakers/.kube/config:/kubeconfig/config:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 5s
timeout: 3s
retries: 10
ports:
- "8080:8080"
volumes:
pgdata:
redisdata: