diff --git a/deploy/chart-gestion/backend/Chart.yaml b/deploy/chart-gestion/backend/Chart.yaml new file mode 100644 index 0000000..2ed118d --- /dev/null +++ b/deploy/chart-gestion/backend/Chart.yaml @@ -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" diff --git a/deploy/chart-gestion/backend/Dockerfile b/deploy/chart-gestion/backend/Dockerfile new file mode 100644 index 0000000..f423cf7 --- /dev/null +++ b/deploy/chart-gestion/backend/Dockerfile @@ -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"] diff --git a/deploy/chart-gestion/backend/entrypoint.sh b/deploy/chart-gestion/backend/entrypoint.sh new file mode 100755 index 0000000..4c1b857 --- /dev/null +++ b/deploy/chart-gestion/backend/entrypoint.sh @@ -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 diff --git a/deploy/chart-gestion/backend/templates/_helpers.tpl b/deploy/chart-gestion/backend/templates/_helpers.tpl new file mode 100644 index 0000000..8d46518 --- /dev/null +++ b/deploy/chart-gestion/backend/templates/_helpers.tpl @@ -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 }} diff --git a/deploy/chart-gestion/backend/templates/deployment.yaml b/deploy/chart-gestion/backend/templates/deployment.yaml new file mode 100644 index 0000000..3d7b554 --- /dev/null +++ b/deploy/chart-gestion/backend/templates/deployment.yaml @@ -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 }} diff --git a/deploy/chart-gestion/backend/templates/hpa.yaml b/deploy/chart-gestion/backend/templates/hpa.yaml new file mode 100644 index 0000000..35dca0b --- /dev/null +++ b/deploy/chart-gestion/backend/templates/hpa.yaml @@ -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 }} diff --git a/deploy/chart-gestion/backend/templates/pvc.yaml b/deploy/chart-gestion/backend/templates/pvc.yaml new file mode 100644 index 0000000..9ec8398 --- /dev/null +++ b/deploy/chart-gestion/backend/templates/pvc.yaml @@ -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 }} diff --git a/deploy/chart-gestion/backend/templates/secret.yaml b/deploy/chart-gestion/backend/templates/secret.yaml new file mode 100644 index 0000000..76bb5b4 --- /dev/null +++ b/deploy/chart-gestion/backend/templates/secret.yaml @@ -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 }} diff --git a/deploy/chart-gestion/backend/templates/service.yaml b/deploy/chart-gestion/backend/templates/service.yaml new file mode 100644 index 0000000..6cb1209 --- /dev/null +++ b/deploy/chart-gestion/backend/templates/service.yaml @@ -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 diff --git a/deploy/chart-gestion/backend/values.yaml b/deploy/chart-gestion/backend/values.yaml new file mode 100644 index 0000000..5900f97 --- /dev/null +++ b/deploy/chart-gestion/backend/values.yaml @@ -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 diff --git a/deploy/chart-gestion/distribute-image.sh b/deploy/chart-gestion/distribute-image.sh new file mode 100644 index 0000000..5a6000e --- /dev/null +++ b/deploy/chart-gestion/distribute-image.sh @@ -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" diff --git a/deploy/chart-gestion/frontend/Caddyfile b/deploy/chart-gestion/frontend/Caddyfile new file mode 100644 index 0000000..e1461c7 --- /dev/null +++ b/deploy/chart-gestion/frontend/Caddyfile @@ -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" + } +} diff --git a/deploy/chart-gestion/frontend/Chart.yaml b/deploy/chart-gestion/frontend/Chart.yaml new file mode 100644 index 0000000..ca65ccc --- /dev/null +++ b/deploy/chart-gestion/frontend/Chart.yaml @@ -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" diff --git a/deploy/chart-gestion/frontend/Dockerfile b/deploy/chart-gestion/frontend/Dockerfile new file mode 100644 index 0000000..98873cc --- /dev/null +++ b/deploy/chart-gestion/frontend/Dockerfile @@ -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"] diff --git a/deploy/chart-gestion/frontend/templates/_helpers.tpl b/deploy/chart-gestion/frontend/templates/_helpers.tpl new file mode 100644 index 0000000..64ce3c9 --- /dev/null +++ b/deploy/chart-gestion/frontend/templates/_helpers.tpl @@ -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 }} diff --git a/deploy/chart-gestion/frontend/templates/configmap.yaml b/deploy/chart-gestion/frontend/templates/configmap.yaml new file mode 100644 index 0000000..a037251 --- /dev/null +++ b/deploy/chart-gestion/frontend/templates/configmap.yaml @@ -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 }} + }; diff --git a/deploy/chart-gestion/frontend/templates/deployment.yaml b/deploy/chart-gestion/frontend/templates/deployment.yaml new file mode 100644 index 0000000..326c3bf --- /dev/null +++ b/deploy/chart-gestion/frontend/templates/deployment.yaml @@ -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 diff --git a/deploy/chart-gestion/frontend/templates/hpa.yaml b/deploy/chart-gestion/frontend/templates/hpa.yaml new file mode 100644 index 0000000..5108d65 --- /dev/null +++ b/deploy/chart-gestion/frontend/templates/hpa.yaml @@ -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 }} diff --git a/deploy/chart-gestion/frontend/templates/service.yaml b/deploy/chart-gestion/frontend/templates/service.yaml new file mode 100644 index 0000000..eaf0712 --- /dev/null +++ b/deploy/chart-gestion/frontend/templates/service.yaml @@ -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 diff --git a/deploy/chart-gestion/frontend/values.yaml b/deploy/chart-gestion/frontend/values.yaml new file mode 100644 index 0000000..3ccae34 --- /dev/null +++ b/deploy/chart-gestion/frontend/values.yaml @@ -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 diff --git a/deploy/chart-gestion/postgresql/Chart.yaml b/deploy/chart-gestion/postgresql/Chart.yaml new file mode 100644 index 0000000..e830eb1 --- /dev/null +++ b/deploy/chart-gestion/postgresql/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: gestion-postgresql +description: PostgreSQL 16 — gestion-commande +type: application +version: 0.1.0 +appVersion: "16" diff --git a/deploy/chart-gestion/postgresql/templates/_helpers.tpl b/deploy/chart-gestion/postgresql/templates/_helpers.tpl new file mode 100644 index 0000000..97c7e5e --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/_helpers.tpl @@ -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 }} diff --git a/deploy/chart-gestion/postgresql/templates/configmap.yaml b/deploy/chart-gestion/postgresql/templates/configmap.yaml new file mode 100644 index 0000000..7fdb2c6 --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/configmap.yaml @@ -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 = '*' diff --git a/deploy/chart-gestion/postgresql/templates/pvc.yaml b/deploy/chart-gestion/postgresql/templates/pvc.yaml new file mode 100644 index 0000000..badd67e --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/pvc.yaml @@ -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 }} diff --git a/deploy/chart-gestion/postgresql/templates/secret.yaml b/deploy/chart-gestion/postgresql/templates/secret.yaml new file mode 100644 index 0000000..b82810a --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/secret.yaml @@ -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 }} diff --git a/deploy/chart-gestion/postgresql/templates/service.yaml b/deploy/chart-gestion/postgresql/templates/service.yaml new file mode 100644 index 0000000..919644f --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/service.yaml @@ -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 diff --git a/deploy/chart-gestion/postgresql/templates/statefulset.yaml b/deploy/chart-gestion/postgresql/templates/statefulset.yaml new file mode 100644 index 0000000..b099456 --- /dev/null +++ b/deploy/chart-gestion/postgresql/templates/statefulset.yaml @@ -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 diff --git a/deploy/chart-gestion/postgresql/values.yaml b/deploy/chart-gestion/postgresql/values.yaml new file mode 100644 index 0000000..35522d9 --- /dev/null +++ b/deploy/chart-gestion/postgresql/values.yaml @@ -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 diff --git a/deploy/chart-gestion/redis/Chart.yaml b/deploy/chart-gestion/redis/Chart.yaml new file mode 100644 index 0000000..05336dc --- /dev/null +++ b/deploy/chart-gestion/redis/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: gestion-redis +description: Redis 7 — gestion-commande +type: application +version: 0.1.0 +appVersion: "7" diff --git a/deploy/chart-gestion/redis/templates/_helpers.tpl b/deploy/chart-gestion/redis/templates/_helpers.tpl new file mode 100644 index 0000000..94b833d --- /dev/null +++ b/deploy/chart-gestion/redis/templates/_helpers.tpl @@ -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 }} diff --git a/deploy/chart-gestion/redis/templates/configmap.yaml b/deploy/chart-gestion/redis/templates/configmap.yaml new file mode 100644 index 0000000..631b48c --- /dev/null +++ b/deploy/chart-gestion/redis/templates/configmap.yaml @@ -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 diff --git a/deploy/chart-gestion/redis/templates/pvc.yaml b/deploy/chart-gestion/redis/templates/pvc.yaml new file mode 100644 index 0000000..776e7f2 --- /dev/null +++ b/deploy/chart-gestion/redis/templates/pvc.yaml @@ -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 }} diff --git a/deploy/chart-gestion/redis/templates/secret.yaml b/deploy/chart-gestion/redis/templates/secret.yaml new file mode 100644 index 0000000..f91bc5f --- /dev/null +++ b/deploy/chart-gestion/redis/templates/secret.yaml @@ -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 }} diff --git a/deploy/chart-gestion/redis/templates/service.yaml b/deploy/chart-gestion/redis/templates/service.yaml new file mode 100644 index 0000000..8c12c0f --- /dev/null +++ b/deploy/chart-gestion/redis/templates/service.yaml @@ -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 diff --git a/deploy/chart-gestion/redis/templates/statefulset.yaml b/deploy/chart-gestion/redis/templates/statefulset.yaml new file mode 100644 index 0000000..0473c98 --- /dev/null +++ b/deploy/chart-gestion/redis/templates/statefulset.yaml @@ -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: {} diff --git a/deploy/chart-gestion/redis/values.yaml b/deploy/chart-gestion/redis/values.yaml new file mode 100644 index 0000000..d982301 --- /dev/null +++ b/deploy/chart-gestion/redis/values.yaml @@ -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 diff --git a/deploy/chart-gestion/traefik/Chart.yaml b/deploy/chart-gestion/traefik/Chart.yaml new file mode 100644 index 0000000..4462e0b --- /dev/null +++ b/deploy/chart-gestion/traefik/Chart.yaml @@ -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 diff --git a/deploy/chart-gestion/traefik/templates/_helpers.tpl b/deploy/chart-gestion/traefik/templates/_helpers.tpl new file mode 100644 index 0000000..bf8166e --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/_helpers.tpl @@ -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 }} diff --git a/deploy/chart-gestion/traefik/templates/ingressroute.yaml b/deploy/chart-gestion/traefik/templates/ingressroute.yaml new file mode 100644 index 0000000..883a606 --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/ingressroute.yaml @@ -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 }} diff --git a/deploy/chart-gestion/traefik/templates/middleware-chain.yaml b/deploy/chart-gestion/traefik/templates/middleware-chain.yaml new file mode 100644 index 0000000..7226e7d --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/middleware-chain.yaml @@ -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 diff --git a/deploy/chart-gestion/traefik/templates/middleware-headers.yaml b/deploy/chart-gestion/traefik/templates/middleware-headers.yaml new file mode 100644 index 0000000..4b39cb5 --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/middleware-headers.yaml @@ -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: "" diff --git a/deploy/chart-gestion/traefik/templates/middleware-ratelimit.yaml b/deploy/chart-gestion/traefik/templates/middleware-ratelimit.yaml new file mode 100644 index 0000000..c24b003 --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/middleware-ratelimit.yaml @@ -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 diff --git a/deploy/chart-gestion/traefik/templates/middleware-waf.yaml b/deploy/chart-gestion/traefik/templates/middleware-waf.yaml new file mode 100644 index 0000000..0eddc04 --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/middleware-waf.yaml @@ -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'\"" diff --git a/deploy/chart-gestion/traefik/templates/tlsstore.yaml b/deploy/chart-gestion/traefik/templates/tlsstore.yaml new file mode 100644 index 0000000..aac3728 --- /dev/null +++ b/deploy/chart-gestion/traefik/templates/tlsstore.yaml @@ -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 }} diff --git a/deploy/chart-gestion/traefik/values.yaml b/deploy/chart-gestion/traefik/values.yaml new file mode 100644 index 0000000..7241ea2 --- /dev/null +++ b/deploy/chart-gestion/traefik/values.yaml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..52dc6bb --- /dev/null +++ b/docker-compose.yml @@ -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: