@@ -117,6 +117,14 @@ func main() {
|
|||||||
log.Fatalf("helm provisioner: %v", err)
|
log.Fatalf("helm provisioner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traefik partagé + WAF Coraza : installé une fois pour toutes les démos
|
||||||
|
// (idempotent). Non-fatal : si ça échoue, l'API démarre quand même, mais
|
||||||
|
// les démos provisionnées ensuite ne seront ni routées ni protégées tant
|
||||||
|
// que ce n'est pas corrigé.
|
||||||
|
if err := helmProv.EnsureSharedInfra(); err != nil {
|
||||||
|
log.Printf("ATTENTION: infra partagée (Traefik/WAF) indisponible: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Service de démos avec le provisioner Helm
|
// Service de démos avec le provisioner Helm
|
||||||
demoSvc := demos.NewService(demoStore, demoPool, helmProv, demos.Config{
|
demoSvc := demos.NewService(demoStore, demoPool, helmProv, demos.Config{
|
||||||
BaseDomain: cfg.DemoDomain,
|
BaseDomain: cfg.DemoDomain,
|
||||||
|
|||||||
@@ -68,12 +68,22 @@ func NewHelmProvisioner(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sharedTraefikNamespace / sharedTraefikRelease : le routeur Traefik + WAF
|
||||||
|
// Coraza est installé UNE SEULE FOIS pour toutes les démos (voir
|
||||||
|
// EnsureSharedInfra), jamais par démo — chaque démo installe seulement le
|
||||||
|
// chart "ingressroute" qui la raccorde à ce Traefik partagé.
|
||||||
|
const (
|
||||||
|
sharedTraefikNamespace = "traefik"
|
||||||
|
sharedTraefikRelease = "gestion-traefik"
|
||||||
|
)
|
||||||
|
|
||||||
// Provision déploie une démo avec Helm :
|
// Provision déploie une démo avec Helm :
|
||||||
// 1. Crée le namespace
|
// 1. Crée le namespace
|
||||||
// 2. Installe PostgreSQL pour la démo
|
// 2. Installe PostgreSQL pour la démo
|
||||||
// 3. Installe Redis pour la démo
|
// 3. Installe Redis pour la démo
|
||||||
// 4. Installe le chart backend
|
// 4. Installe le chart backend
|
||||||
// 5. Installe le chart frontend
|
// 5. Installe le chart frontend
|
||||||
|
// 6. Raccorde la démo au Traefik partagé (WAF + routage HTTPS du sous-domaine)
|
||||||
func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error {
|
func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error {
|
||||||
// Créer le namespace
|
// Créer le namespace
|
||||||
if err := h.createNamespace(d.Namespace); err != nil {
|
if err := h.createNamespace(d.Namespace); err != nil {
|
||||||
@@ -85,6 +95,7 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error
|
|||||||
redisValues := h.buildRedisValues(d)
|
redisValues := h.buildRedisValues(d)
|
||||||
backendValues := h.buildBackendValues(d, resources)
|
backendValues := h.buildBackendValues(d, resources)
|
||||||
frontendValues := h.buildFrontendValues(d)
|
frontendValues := h.buildFrontendValues(d)
|
||||||
|
ingressValues := h.buildIngressRouteValues(d)
|
||||||
|
|
||||||
// Installer PostgreSQL (nécessaire pour le backend)
|
// Installer PostgreSQL (nécessaire pour le backend)
|
||||||
if err := h.installChart(d.Namespace, "postgresql", postgresValues); err != nil {
|
if err := h.installChart(d.Namespace, "postgresql", postgresValues); err != nil {
|
||||||
@@ -110,6 +121,13 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error
|
|||||||
return fmt.Errorf("déploiement frontend: %w", err)
|
return fmt.Errorf("déploiement frontend: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raccorder au Traefik partagé : sans ça la démo n'est ni routée ni
|
||||||
|
// protégée par le WAF, même si backend/frontend tournent.
|
||||||
|
if err := h.installChart(d.Namespace, "ingressroute", ingressValues); err != nil {
|
||||||
|
h.deleteNamespace(d.Namespace)
|
||||||
|
return fmt.Errorf("déploiement ingressroute: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Attendre que les pods soient prêts
|
// Attendre que les pods soient prêts
|
||||||
if err := h.waitForRollout(d.Namespace); err != nil {
|
if err := h.waitForRollout(d.Namespace); err != nil {
|
||||||
log.Printf("Warning: rollout check échoué pour %s: %v", d.Namespace, err)
|
log.Printf("Warning: rollout check échoué pour %s: %v", d.Namespace, err)
|
||||||
@@ -118,6 +136,33 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureSharedInfra installe (ou met à jour, de façon idempotente) le
|
||||||
|
// Traefik partagé + WAF Coraza. À appeler une fois au démarrage de l'API,
|
||||||
|
// avant toute démo — voir cmd/api/main.go. Contrairement à Provision, ceci
|
||||||
|
// n'est PAS répété par démo.
|
||||||
|
func (h *HelmProvisioner) EnsureSharedInfra() error {
|
||||||
|
if err := h.createNamespace(sharedTraefikNamespace); err != nil {
|
||||||
|
return fmt.Errorf("création namespace %s: %w", sharedTraefikNamespace, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chartPath := filepath.Join(h.chartsDir, "traefik")
|
||||||
|
|
||||||
|
// Le chart traefik dépend du chart officiel traefik/traefik (subchart) :
|
||||||
|
// il faut le récupérer avant l'install (nécessite un accès internet
|
||||||
|
// sortant depuis ce pod, comme pour le plugin Coraza téléchargé par
|
||||||
|
// Traefik lui-même au démarrage).
|
||||||
|
if err := h.runHelmDependencyUpdate(chartPath); err != nil {
|
||||||
|
return fmt.Errorf("helm dependency update traefik: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedTraefikRelease, "traefik", nil); err != nil {
|
||||||
|
return fmt.Errorf("déploiement traefik partagé: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Traefik partagé (WAF Coraza + rate-limit) prêt dans le namespace %s", sharedTraefikNamespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Teardown détruit une démo en supprimant son namespace (cascading).
|
// Teardown détruit une démo en supprimant son namespace (cascading).
|
||||||
func (h *HelmProvisioner) Teardown(d Demo) error {
|
func (h *HelmProvisioner) Teardown(d Demo) error {
|
||||||
return h.deleteNamespace(d.Namespace)
|
return h.deleteNamespace(d.Namespace)
|
||||||
@@ -148,42 +193,93 @@ func (h *HelmProvisioner) deleteNamespace(name string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// installChart installe un chart Helm avec des valeurs personnalisées.
|
// installChart installe (première install, jamais réappliqué) un chart Helm
|
||||||
|
// avec des valeurs personnalisées. Release name : "<namespace>-<chartName>".
|
||||||
|
// Adapté aux charts par démo (namespace toujours neuf).
|
||||||
func (h *HelmProvisioner) installChart(namespace, chartName string, values map[string]interface{}) error {
|
func (h *HelmProvisioner) installChart(namespace, chartName string, values map[string]interface{}) error {
|
||||||
chartPath := filepath.Join(h.chartsDir, chartName)
|
release := fmt.Sprintf("%s-%s", namespace, chartName)
|
||||||
|
valuesFile, cleanup, err := h.writeValuesFile(namespace, chartName, values)
|
||||||
// Créer un fichier values.yaml temporaire
|
|
||||||
valuesFile := filepath.Join("/tmp", fmt.Sprintf("values-%s-%s.yaml", namespace, chartName))
|
|
||||||
valuesYAML, err := yaml.Marshal(values)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal YAML: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(valuesFile, valuesYAML, 0644); err != nil {
|
defer cleanup()
|
||||||
return fmt.Errorf("write values file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(valuesFile)
|
|
||||||
|
|
||||||
// Construire la commande helm install
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"install",
|
"install",
|
||||||
fmt.Sprintf("%s-%s", namespace, chartName),
|
release,
|
||||||
chartPath,
|
filepath.Join(h.chartsDir, chartName),
|
||||||
"--namespace", namespace,
|
"--namespace", namespace,
|
||||||
"--values", valuesFile,
|
"--values", valuesFile,
|
||||||
"--wait",
|
"--wait",
|
||||||
"--timeout", "5m",
|
"--timeout", "5m",
|
||||||
}
|
}
|
||||||
|
if err := h.runHelm(args...); err != nil {
|
||||||
|
return fmt.Errorf("helm install %s: %w", chartName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Chart %s installé dans %s", chartName, namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeInstallChart installe ou met à jour un chart de façon idempotente
|
||||||
|
// (helm upgrade --install) — pour l'infra partagée (Traefik), appelée à
|
||||||
|
// chaque démarrage de l'API et qui ne doit pas échouer si déjà en place.
|
||||||
|
func (h *HelmProvisioner) upgradeInstallChart(namespace, release, chartName string, values map[string]interface{}) error {
|
||||||
|
args := []string{
|
||||||
|
"upgrade", release,
|
||||||
|
filepath.Join(h.chartsDir, chartName),
|
||||||
|
"--install",
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--wait",
|
||||||
|
"--timeout", "5m",
|
||||||
|
}
|
||||||
|
if values != nil {
|
||||||
|
valuesFile, cleanup, err := h.writeValuesFile(namespace, chartName, values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
args = append(args, "--values", valuesFile)
|
||||||
|
}
|
||||||
|
if err := h.runHelm(args...); err != nil {
|
||||||
|
return fmt.Errorf("helm upgrade --install %s: %w", chartName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Chart %s (release %s) à jour dans %s", chartName, release, namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runHelmDependencyUpdate télécharge les subcharts déclarés dans
|
||||||
|
// Chart.yaml (dependencies) — requis avant d'installer un chart qui, comme
|
||||||
|
// traefik, dépend d'un chart tiers.
|
||||||
|
func (h *HelmProvisioner) runHelmDependencyUpdate(chartPath string) error {
|
||||||
|
return h.runHelm("dependency", "update", chartPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeValuesFile sérialise des valeurs Helm dans un fichier temporaire et
|
||||||
|
// retourne son chemin + une fonction de nettoyage à appeler en defer.
|
||||||
|
func (h *HelmProvisioner) writeValuesFile(namespace, chartName string, values map[string]interface{}) (string, func(), error) {
|
||||||
|
valuesFile := filepath.Join("/tmp", fmt.Sprintf("values-%s-%s.yaml", namespace, chartName))
|
||||||
|
valuesYAML, err := yaml.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("marshal YAML: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(valuesFile, valuesYAML, 0644); err != nil {
|
||||||
|
return "", nil, fmt.Errorf("write values file: %w", err)
|
||||||
|
}
|
||||||
|
return valuesFile, func() { os.Remove(valuesFile) }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runHelm exécute une commande helm et remonte stdout/stderr en cas d'erreur.
|
||||||
|
func (h *HelmProvisioner) runHelm(args ...string) error {
|
||||||
cmd := exec.Command(h.helmPath, args...)
|
cmd := exec.Command(h.helmPath, args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("helm install %s: %v (stdout: %s, stderr: %s)", chartName, err, stdout.String(), stderr.String())
|
return fmt.Errorf("%v (stdout: %s, stderr: %s)", err, stdout.String(), stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Chart %s installé dans %s", chartName, namespace)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +340,9 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
|||||||
|
|
||||||
// buildFrontendValues construit les valeurs pour le chart frontend.
|
// buildFrontendValues construit les valeurs pour le chart frontend.
|
||||||
func (h *HelmProvisioner) buildFrontendValues(d Demo) map[string]interface{} {
|
func (h *HelmProvisioner) buildFrontendValues(d Demo) map[string]interface{} {
|
||||||
backendURL := fmt.Sprintf("http://%s-backend.%s.svc.cluster.local:8080", d.Namespace, d.Namespace)
|
// Nom réel du Service backend : "<namespace>-backend" (release helm) +
|
||||||
|
// "-gestion-backend" (nom du chart, ajouté par le helper fullname).
|
||||||
|
backendURL := fmt.Sprintf("http://%s-backend-gestion-backend.%s.svc.cluster.local:8080", d.Namespace, d.Namespace)
|
||||||
// Parser l'image frontend pour séparer repository et tag
|
// Parser l'image frontend pour séparer repository et tag
|
||||||
frontendRepo, frontendTag := parseImage(h.frontendImage)
|
frontendRepo, frontendTag := parseImage(h.frontendImage)
|
||||||
|
|
||||||
@@ -266,6 +364,15 @@ func (h *HelmProvisioner) buildFrontendValues(d Demo) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildIngressRouteValues construit les valeurs pour le chart ingressroute,
|
||||||
|
// qui raccorde cette démo au Traefik partagé (WAF + routage HTTPS).
|
||||||
|
func (h *HelmProvisioner) buildIngressRouteValues(d Demo) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"host": fmt.Sprintf("%s.%s", d.Namespace, h.baseDomain),
|
||||||
|
"traefikNamespace": sharedTraefikNamespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// buildSecrets extrait les références des secrets des ressources externes.
|
// buildSecrets extrait les références des secrets des ressources externes.
|
||||||
func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]string {
|
func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]string {
|
||||||
secrets := map[string]string{}
|
secrets := map[string]string{}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Identifiants à récupérer dans le Hub Beszel : Add System > Universal token.
|
||||||
|
# Le token universel expire au bout d'1h mais permet l'auto-enregistrement de
|
||||||
|
# TOUS les agents du DaemonSet sans les déclarer un par un dans le Hub ; une
|
||||||
|
# fois qu'un agent s'est enregistré, il garde son identité même après
|
||||||
|
# l'expiration du token (donc pas besoin de le renouveler après coup).
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: beszel-agent-hub
|
||||||
|
namespace: monitoring
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
HUB_URL: "" # ex: https://beszel.exemple.com
|
||||||
|
TOKEN: "" # token universel généré dans le Hub
|
||||||
|
KEY: "" # clé publique SSH affichée par le Hub
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: monitoring
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Prérequis (à faire une seule fois, hors de ces manifestes) :
|
||||||
|
# helm repo add jetstack https://charts.jetstack.io
|
||||||
|
# helm install cert-manager jetstack/cert-manager \
|
||||||
|
# --namespace cert-manager --create-namespace --set crds.enabled=true
|
||||||
|
#
|
||||||
|
# Un certificat wildcard (*.demo.demo-uber.club) ne peut être émis que via un
|
||||||
|
# challenge DNS-01 (le challenge HTTP-01 ne supporte pas les wildcards) : il
|
||||||
|
# faut donc que cert-manager puisse créer des enregistrements TXT chez votre
|
||||||
|
# fournisseur DNS. Exemple ci-dessous avec Cloudflare (le plus courant) —
|
||||||
|
# remplacez le bloc `cloudflare:` par le solver de votre fournisseur si ce
|
||||||
|
# n'est pas celui-ci (route53, digitalocean, ovh via webhook, etc.).
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
namespace: cert-manager
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Token Cloudflare avec la permission "Zone:DNS:Edit" sur la zone demo-uber.club.
|
||||||
|
# https://dash.cloudflare.com/profile/api-tokens
|
||||||
|
api-token: "" # À REMPLIR avant application
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-dns
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
email: admin@demo-uber.club # À adapter
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-dns-account-key
|
||||||
|
solvers:
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "demo-uber.club"
|
||||||
|
|
||||||
|
---
|
||||||
|
# Issuer de test (ne compte pas dans les limites de rate-limit de Let's Encrypt
|
||||||
|
# prod — utile pour valider la chaîne DNS-01 avant de basculer sur letsencrypt-dns).
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-dns-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
email: admin@demo-uber.club # À adapter
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-dns-staging-account-key
|
||||||
|
solvers:
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "demo-uber.club"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Un seul certificat wildcard, renouvelé automatiquement par cert-manager
|
||||||
|
# (par défaut ~30 jours avant expiration), stocké dans le Secret ci-dessous.
|
||||||
|
# Le chart traefik (voir deploy/chart-gestion/traefik) le consomme comme
|
||||||
|
# certificat par défaut de son TLSStore : aucune démo n'a besoin de son
|
||||||
|
# propre certificat, toutes partagent celui-ci.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-demo-tls
|
||||||
|
namespace: traefik # doit être le même namespace que le déploiement Traefik partagé
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-demo-tls
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-dns
|
||||||
|
kind: ClusterIssuer
|
||||||
|
commonName: "demo.demo-uber.club"
|
||||||
|
dnsNames:
|
||||||
|
- "demo.demo-uber.club"
|
||||||
|
- "*.demo.demo-uber.club"
|
||||||
@@ -61,6 +61,8 @@ podSecurityContext:
|
|||||||
runAsUser: 101
|
runAsUser: 101
|
||||||
runAsGroup: 101
|
runAsGroup: 101
|
||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ podSecurityContext:
|
|||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
runAsUser: 101
|
runAsUser: 101
|
||||||
runAsGroup: 101
|
runAsGroup: 101
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: gestion-ingressroute
|
||||||
|
description: Routage Traefik d'une démo (Host dynamique -> Services de son propre namespace) — installé une fois par démo, pointe vers le Traefik partagé (deploy/chart-gestion/traefik)
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{{- define "ingressroute.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 "ingressroute.backendService" -}}
|
||||||
|
{{- .Values.backend.serviceName | default (printf "%s-backend-gestion-backend" .Release.Namespace) }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "ingressroute.frontendService" -}}
|
||||||
|
{{- .Values.frontend.serviceName | default (printf "%s-frontend-gestion-frontend" .Release.Namespace) }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{{- if not .Values.host }}
|
||||||
|
{{- fail "ingressroute: .Values.host est requis (ex: demo-abc123.demo.demo-uber.club)" }}
|
||||||
|
{{- end }}
|
||||||
|
# Routage HTTPS de cette démo. Le TLS n'est pas précisé : Traefik utilise
|
||||||
|
# automatiquement le certificat wildcard par défaut du TLSStore partagé
|
||||||
|
# (voir deploy/chart-gestion/traefik/templates/tlsstore.yaml). Le redirect
|
||||||
|
# HTTP -> HTTPS est géré au niveau de l'entrypoint Traefik (pas dupliqué ici).
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: {{ .Release.Namespace }}-routes
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "ingressroute.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
# API backend — WAF + headers + rate limit (middlewares partagés, définis
|
||||||
|
# dans le namespace du Traefik commun, référencés cross-namespace).
|
||||||
|
- match: Host(`{{ .Values.host }}`) && (PathPrefix(`/api/`) || PathPrefix(`/webhook/`))
|
||||||
|
kind: Rule
|
||||||
|
priority: 20
|
||||||
|
middlewares:
|
||||||
|
- name: waf-chain
|
||||||
|
namespace: {{ .Values.traefikNamespace }}
|
||||||
|
services:
|
||||||
|
- name: {{ include "ingressroute.backendService" . }}
|
||||||
|
port: {{ .Values.backend.servicePort }}
|
||||||
|
|
||||||
|
# Fichiers uploadés — WAF désactivé dans les règles Coraza pour ce path
|
||||||
|
- match: Host(`{{ .Values.host }}`) && PathPrefix(`/uploads/`)
|
||||||
|
kind: Rule
|
||||||
|
priority: 15
|
||||||
|
middlewares:
|
||||||
|
- name: security-headers
|
||||||
|
namespace: {{ .Values.traefikNamespace }}
|
||||||
|
services:
|
||||||
|
- name: {{ include "ingressroute.backendService" . }}
|
||||||
|
port: {{ .Values.backend.servicePort }}
|
||||||
|
|
||||||
|
# Frontend SPA — WAF + headers + rate limit
|
||||||
|
- match: Host(`{{ .Values.host }}`)
|
||||||
|
kind: Rule
|
||||||
|
priority: 1
|
||||||
|
middlewares:
|
||||||
|
- name: waf-chain
|
||||||
|
namespace: {{ .Values.traefikNamespace }}
|
||||||
|
services:
|
||||||
|
- name: {{ include "ingressroute.frontendService" . }}
|
||||||
|
port: {{ .Values.frontend.servicePort }}
|
||||||
|
|
||||||
|
tls: {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Sous-domaine complet de la démo, ex: demo-abc123.demo.demo-uber.club
|
||||||
|
# (doit correspondre au wildcard *.demo.demo-uber.club émis par cert-manager
|
||||||
|
# et à l'URL calculée par le control-plane pour cette démo — voir
|
||||||
|
# control-plane/api/internal/demos/service.go).
|
||||||
|
host: ""
|
||||||
|
|
||||||
|
# Namespace où tourne le Traefik partagé et ses Middlewares (waf-chain,
|
||||||
|
# rate-limit, coraza-waf, security-headers) — voir deploy/chart-gestion/traefik.
|
||||||
|
traefikNamespace: traefik
|
||||||
|
|
||||||
|
# Services de CE namespace de démo (mêmes noms que ceux générés par les
|
||||||
|
# charts backend/frontend : <namespace>-backend-gestion-backend, etc.)
|
||||||
|
# Laissés vides par défaut : calculés automatiquement à partir de
|
||||||
|
# .Release.Namespace dans le template si non surchargés ici.
|
||||||
|
backend:
|
||||||
|
serviceName: ""
|
||||||
|
servicePort: 8080
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
serviceName: ""
|
||||||
|
servicePort: 80
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: gestion-network-policy
|
||||||
|
description: Isolation réseau stricte d'une démo — deny-by-default, aucune communication inter-démos, pas d'accès à l'API Kubernetes ni à kube-dns au-delà du port 53
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 1. Deny-by-default : sans règle explicite plus bas, AUCUN trafic entrant
|
||||||
|
# ni sortant n'est autorisé pour les pods de ce namespace. C'est ce qui
|
||||||
|
# empêche par défaut toute communication vers un autre namespace de démo
|
||||||
|
# et vers l'API Kubernetes.
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny-all
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
|
||||||
|
---
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 2. Trafic intra-démo : frontend <-> backend <-> postgresql/redis, dans
|
||||||
|
# CE namespace uniquement (le podSelector du namespaceSelector par défaut
|
||||||
|
# de Kubernetes limite déjà à ce namespace : pas de namespaceSelector ici
|
||||||
|
# équivaut à "même namespace").
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-intra-namespace
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector: {}
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- podSelector: {}
|
||||||
|
|
||||||
|
---
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3. Ingress externe autorisé : uniquement depuis le Traefik partagé
|
||||||
|
# (aucune autre démo, aucun autre client, ne peut ouvrir de connexion
|
||||||
|
# directe vers les pods de cette démo).
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-ingress-from-traefik
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: {{ .Values.traefikNamespace }}
|
||||||
|
|
||||||
|
---
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 4. DNS uniquement vers kube-system, port 53. C'est la SEULE ouverture vers
|
||||||
|
# kube-system : aucun accès à autre chose dans ce namespace, et l'API
|
||||||
|
# server (kubernetes.default.svc, namespace "default") n'est jamais
|
||||||
|
# autorisé nulle part dans ce chart -> bloqué par le deny-by-default.
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-egress-dns
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Egress
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: {{ .Values.kubeSystemNamespace }}
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
|
||||||
|
---
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 5. Internet public sortant (webhooks Telegram/NowPayments, GPS TomTom...),
|
||||||
|
# en excluant les plages privées : ça couvre par construction l'IP de
|
||||||
|
# l'API server et celles des autres namespaces, sans avoir besoin de la
|
||||||
|
# connaître explicitement.
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-egress-internet
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Egress
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- ipBlock:
|
||||||
|
cidr: 0.0.0.0/0
|
||||||
|
except:
|
||||||
|
{{- toYaml .Values.blockedEgressCIDRs | nindent 14 }}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# IMPORTANT : les NetworkPolicy ne sont appliquées que si le CNI du cluster
|
||||||
|
# les supporte (Calico, Cilium, Weave...). Avec un CNI qui ne les enforce pas
|
||||||
|
# (ex: flannel seul), ce chart s'installe sans erreur mais n'isole RIEN —
|
||||||
|
# à vérifier avant de considérer l'isolation comme active.
|
||||||
|
|
||||||
|
# Namespace où tourne CoreDNS (résolution de noms) — seul port autorisé en
|
||||||
|
# egress vers ce namespace : 53 (DNS). Aucun autre accès à kube-system,
|
||||||
|
# donc pas d'accès à l'API server (kubernetes.default.svc vit dans le
|
||||||
|
# namespace "default", pas dans kube-system, et n'est de toute façon jamais
|
||||||
|
# ajouté aux règles autorisées ci-dessous : bloqué par omission).
|
||||||
|
kubeSystemNamespace: kube-system
|
||||||
|
|
||||||
|
# Namespace du Traefik partagé (voir deploy/chart-gestion/traefik) : seule
|
||||||
|
# origine externe autorisée à ouvrir une connexion ingress vers cette démo.
|
||||||
|
traefikNamespace: traefik
|
||||||
|
|
||||||
|
# Plages privées exclues de l'egress "internet" autorisé (RFC1918) : empêche
|
||||||
|
# tout mouvement latéral vers d'autres pods/namespaces/nœuds/l'API server par
|
||||||
|
# IP directe, tout en laissant sortir vers l'internet public (Telegram,
|
||||||
|
# NowPayments, TomTom, etc.). Si le CIDR des pods/services de votre cluster
|
||||||
|
# ne tombe pas dans ces plages standard, ajoutez-le ici.
|
||||||
|
blockedEgressCIDRs:
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
@@ -13,3 +14,4 @@ spec:
|
|||||||
{{- if .Values.persistence.storageClass }}
|
{{- if .Values.persistence.storageClass }}
|
||||||
storageClassName: {{ .Values.persistence.storageClass }}
|
storageClassName: {{ .Values.persistence.storageClass }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ spec:
|
|||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: {{ include "postgresql.fullname" . }}-data
|
claimName: {{ include "postgresql.fullname" . }}-data
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: {{ include "postgresql.fullname" . }}-config
|
name: {{ include "postgresql.fullname" . }}-config
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ auth:
|
|||||||
password: ""
|
password: ""
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
|
enabled: true
|
||||||
size: 10Gi
|
size: 10Gi
|
||||||
storageClass: ""
|
storageClass: ""
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
@@ -38,9 +39,12 @@ config:
|
|||||||
logMinDurationStatement: "1000"
|
logMinDurationStatement: "1000"
|
||||||
|
|
||||||
podSecurityContext:
|
podSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
runAsUser: 999
|
runAsUser: 999
|
||||||
runAsGroup: 999
|
runAsGroup: 999
|
||||||
fsGroup: 999
|
fsGroup: 999
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
@@ -13,3 +14,4 @@ spec:
|
|||||||
{{- if .Values.persistence.storageClass }}
|
{{- if .Values.persistence.storageClass }}
|
||||||
storageClassName: {{ .Values.persistence.storageClass }}
|
storageClassName: {{ .Values.persistence.storageClass }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ spec:
|
|||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: {{ include "redis.fullname" . }}-data
|
claimName: {{ include "redis.fullname" . }}-data
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: {{ include "redis.fullname" . }}-config
|
name: {{ include "redis.fullname" . }}-config
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ auth:
|
|||||||
password: ""
|
password: ""
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
|
enabled: true
|
||||||
size: 2Gi
|
size: 2Gi
|
||||||
storageClass: ""
|
storageClass: ""
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
@@ -34,9 +35,12 @@ config:
|
|||||||
save: ""
|
save: ""
|
||||||
|
|
||||||
podSecurityContext:
|
podSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
runAsUser: 999
|
runAsUser: 999
|
||||||
runAsGroup: 999
|
runAsGroup: 999
|
||||||
fsGroup: 999
|
fsGroup: 999
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
dependencies:
|
||||||
|
- name: traefik
|
||||||
|
repository: https://helm.traefik.io/traefik
|
||||||
|
version: 41.1.0
|
||||||
|
digest: sha256:62635ee99f7e539f6a9206239003249a65f4a4cf0e5606fecc9578ed0cfa31ef
|
||||||
|
generated: "2026-07-31T12:07:20.158740949+02:00"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: gestion-traefik
|
name: gestion-traefik
|
||||||
description: Traefik ingress + Coraza WAF (OWASP CRS) — gestion-commande
|
description: Traefik partagé multi-tenant + Coraza WAF (OWASP CRS) — routeur unique pour toutes les démos, installé une seule fois (voir deploy/chart-gestion/ingressroute pour le routage par démo)
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
appVersion: "3.1.0"
|
appVersion: "3.1.0"
|
||||||
|
|||||||
Binary file not shown.
@@ -4,9 +4,3 @@ app.kubernetes.io/name: {{ .Chart.Name }}
|
|||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- define "gestion-traefik.certResolver" -}}
|
|
||||||
{{- if eq .Values.tls.mode "letsencrypt" -}}
|
|
||||||
letsencrypt
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
# 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 }}
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# Certificat par défaut pour TOUTES les IngressRoute (ce namespace et les
|
||||||
|
# namespaces de démo) qui ne précisent pas tls.secretName — c'est le wildcard
|
||||||
|
# émis par cert-manager (voir deploy/cert-manager/wildcard-certificate.yml).
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: TLSStore
|
kind: TLSStore
|
||||||
metadata:
|
metadata:
|
||||||
@@ -6,11 +9,5 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "gestion-traefik.labels" . | nindent 4 }}
|
{{- include "gestion-traefik.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
{{- if eq .Values.tls.mode "secret" }}
|
|
||||||
defaultCertificate:
|
defaultCertificate:
|
||||||
secretName: {{ .Values.tls.secretName }}
|
secretName: {{ .Values.tls.secretName }}
|
||||||
{{- end }}
|
|
||||||
defaultGeneratedCert:
|
|
||||||
resolver: {{ include "gestion-traefik.certResolver" . | default "letsencrypt" }}
|
|
||||||
domain:
|
|
||||||
main: {{ .Values.domain }}
|
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
# =============================================================
|
# =============================================================
|
||||||
# Paramètres spécifiques à ce chart (IngressRoutes, Middlewares)
|
# Traefik partagé multi-tenant : un seul déploiement pour toutes les démos.
|
||||||
|
# Le routage par démo (Host -> Service) est fourni par le chart
|
||||||
|
# deploy/chart-gestion/ingressroute, installé une fois par namespace de démo.
|
||||||
# =============================================================
|
# =============================================================
|
||||||
|
|
||||||
# Namespace des services backend et frontend
|
# TLS : certificat wildcard émis une fois par cert-manager
|
||||||
servicesNamespace: gestion
|
# (voir deploy/cert-manager/), référencé ici comme certificat par défaut
|
||||||
|
# du TLSStore. Toutes les IngressRoute (dans ce namespace ou les namespaces
|
||||||
domain: example.com
|
# de démo) qui ne précisent pas de tls.secretName utilisent automatiquement
|
||||||
|
# ce certificat.
|
||||||
tls:
|
tls:
|
||||||
# "letsencrypt" | "secret"
|
secretName: wildcard-demo-tls
|
||||||
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:
|
waf:
|
||||||
paranoia: 2
|
paranoia: 2
|
||||||
@@ -37,27 +27,34 @@ rateLimit:
|
|||||||
# https://github.com/traefik/traefik-helm-chart
|
# https://github.com/traefik/traefik-helm-chart
|
||||||
# =============================================================
|
# =============================================================
|
||||||
traefik:
|
traefik:
|
||||||
# Expose les ports 80 et 443
|
# 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,
|
||||||
|
# voir charts/traefik-41.1.0.tgz).
|
||||||
ports:
|
ports:
|
||||||
web:
|
web:
|
||||||
port: 8000
|
port: 8000
|
||||||
expose:
|
expose:
|
||||||
default: true
|
default: true
|
||||||
redirectTo:
|
http:
|
||||||
port: websecure
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
websecure:
|
websecure:
|
||||||
port: 8443
|
port: 8443
|
||||||
expose:
|
expose:
|
||||||
default: true
|
default: true
|
||||||
tls:
|
http:
|
||||||
enabled: true
|
tls:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs:
|
log:
|
||||||
general:
|
level: INFO
|
||||||
level: INFO
|
accessLog:
|
||||||
access:
|
enabled: true
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Plugin Coraza WAF — Traefik télécharge le plugin au démarrage
|
# Plugin Coraza WAF — Traefik télécharge le plugin au démarrage
|
||||||
# (le cluster doit avoir accès à internet ou utiliser un plugin local)
|
# (le cluster doit avoir accès à internet ou utiliser un plugin local)
|
||||||
@@ -67,20 +64,18 @@ traefik:
|
|||||||
moduleName: "github.com/corazawaf/coraza-traefik"
|
moduleName: "github.com/corazawaf/coraza-traefik"
|
||||||
version: "v0.3.0"
|
version: "v0.3.0"
|
||||||
|
|
||||||
# Cert resolver Let's Encrypt (ignoré si tls.mode=secret)
|
# ACME interne désactivé : c'est cert-manager qui gère le cycle de vie du
|
||||||
certResolvers:
|
# certificat wildcard (voir deploy/cert-manager/), Traefik ne fait que le
|
||||||
letsencrypt:
|
# consommer via le TLSStore (templates/tlsstore.yaml).
|
||||||
email: admin@example.com
|
|
||||||
storage: /data/acme.json
|
|
||||||
httpChallenge:
|
|
||||||
entryPoint: web
|
|
||||||
|
|
||||||
# ACME storage persistant
|
# ACME storage persistant — plus nécessaire sans resolver Traefik interne,
|
||||||
|
# conservé désactivé par défaut.
|
||||||
persistence:
|
persistence:
|
||||||
enabled: true
|
enabled: false
|
||||||
size: 128Mi
|
|
||||||
|
|
||||||
# Traefik lit les IngressRoutes dans tous les namespaces
|
# Traefik lit les IngressRoute/Middleware dans tous les namespaces de démo,
|
||||||
|
# et peut référencer les Middlewares partagés (waf-chain) définis ici depuis
|
||||||
|
# une IngressRoute d'un autre namespace.
|
||||||
providers:
|
providers:
|
||||||
kubernetesCRD:
|
kubernetesCRD:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Installation de Calico (CNI qui applique réellement les NetworkPolicy) —
|
||||||
|
# nécessaire pour que deploy/chart-gestion/network-policy soit autre chose
|
||||||
|
# qu'une déclaration ignorée par le cluster.
|
||||||
|
#
|
||||||
|
# ⚠️ Si le cluster a déjà un CNI actif (flannel, etc.), installer Calico en
|
||||||
|
# remplacement est disruptif : tous les pods doivent être recréés pour
|
||||||
|
# obtenir une IP du nouveau CNI. À faire lors d'une fenêtre de maintenance,
|
||||||
|
# pas à chaud sur un cluster avec des démos clients actives. Si vous voulez
|
||||||
|
# juste ajouter l'enforcement de policy SANS remplacer le CNI existant, dites-
|
||||||
|
# le : Calico peut aussi tourner en mode "policy-only" au-dessus de flannel
|
||||||
|
# (Canal), c'est un manifeste différent de celui-ci.
|
||||||
|
#
|
||||||
|
# Étape 1 (à lancer manuellement, pas inclus ici : c'est un manifeste upstream
|
||||||
|
# de plusieurs milliers de lignes, à récupérer directement depuis le projet) :
|
||||||
|
#
|
||||||
|
# kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.1/manifests/tigera-operator.yaml
|
||||||
|
#
|
||||||
|
# Étape 2 : une fois l'operator up (kubectl get pods -n tigera-operator),
|
||||||
|
# appliquer ce fichier :
|
||||||
|
#
|
||||||
|
# kubectl apply -f deploy/cni/calico-install.yml
|
||||||
|
#
|
||||||
|
# Étape 3 : vérifier l'enforcement avec deploy/cni/network-policy-test.yml.
|
||||||
|
|
||||||
|
apiVersion: operator.tigera.io/v1
|
||||||
|
kind: Installation
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
spec:
|
||||||
|
calicoNetwork:
|
||||||
|
ipPools:
|
||||||
|
# ⚠️ Ce CIDR doit correspondre au pod CIDR réellement utilisé par le
|
||||||
|
# cluster (ou être libre s'il s'agit d'une installation neuve). Ne pas
|
||||||
|
# appliquer tel quel sans vérifier qu'il ne chevauche pas l'existant.
|
||||||
|
- name: default-ipv4-ippool
|
||||||
|
cidr: 192.168.0.0/16
|
||||||
|
encapsulation: VXLAN
|
||||||
|
natOutgoing: Enabled
|
||||||
|
nodeSelector: all()
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: operator.tigera.io/v1
|
||||||
|
kind: APIServer
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
spec: {}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Vérification que le CNI applique bien les NetworkPolicy avant de faire
|
||||||
|
# confiance à deploy/chart-gestion/network-policy pour l'isolation des démos.
|
||||||
|
#
|
||||||
|
# kubectl apply -f deploy/cni/network-policy-test.yml
|
||||||
|
# kubectl wait -n netpol-test --for=condition=ready pod -l app=target --timeout=60s
|
||||||
|
#
|
||||||
|
# # Doit réussir (client autorisé) :
|
||||||
|
# kubectl exec -n netpol-test allowed-client -- wget -qO- --timeout=2 target
|
||||||
|
#
|
||||||
|
# # Doit échouer / timeout (client non autorisé — c'est le résultat attendu) :
|
||||||
|
# kubectl exec -n netpol-test blocked-client -- wget -qO- --timeout=2 target
|
||||||
|
#
|
||||||
|
# Si "blocked-client" arrive quand même à joindre "target", le CNI n'applique
|
||||||
|
# PAS les NetworkPolicy : deploy/chart-gestion/network-policy ne protège rien.
|
||||||
|
#
|
||||||
|
# Nettoyage : kubectl delete namespace netpol-test
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: netpol-test
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: target
|
||||||
|
namespace: netpol-test
|
||||||
|
labels:
|
||||||
|
app: target
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: target
|
||||||
|
namespace: netpol-test
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: target
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: allowed-client
|
||||||
|
namespace: netpol-test
|
||||||
|
labels:
|
||||||
|
role: allowed
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: busybox
|
||||||
|
image: busybox:1.37
|
||||||
|
command: ["sh", "-c", "sleep 3600"]
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: blocked-client
|
||||||
|
namespace: netpol-test
|
||||||
|
labels:
|
||||||
|
role: blocked
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: busybox
|
||||||
|
image: busybox:1.37
|
||||||
|
command: ["sh", "-c", "sleep 3600"]
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-only-labeled
|
||||||
|
namespace: netpol-test
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: target
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
role: allowed
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Applique aussi beszel-namespace.yml et beszel-agent-secret.yml (avant celui-ci).
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: beszel-agent
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app: beszel-agent
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: beszel-agent
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: beszel-agent
|
||||||
|
spec:
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: node-role.kubernetes.io/master
|
||||||
|
operator: Exists
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: beszel-agent
|
||||||
|
image: henrygd/beszel-agent:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: beszel-agent-hub
|
||||||
|
env:
|
||||||
|
- name: LISTEN
|
||||||
|
value: "45876"
|
||||||
|
- name: SYSTEM_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/beszel-agent
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 16Mi
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/beszel-agent
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
# - name: docker-sock
|
||||||
|
# hostPath:
|
||||||
|
# path: /var/run/docker.sock
|
||||||
|
# type: Socket
|
||||||
Reference in New Issue
Block a user