diff --git a/control-plane/api/internal/demos/helm_provisioner.go b/control-plane/api/internal/demos/helm_provisioner.go index 5baaede..0832a97 100644 --- a/control-plane/api/internal/demos/helm_provisioner.go +++ b/control-plane/api/internal/demos/helm_provisioner.go @@ -813,8 +813,9 @@ func (h *HelmProvisioner) deleteNamespace(name string) error { const veleroNamespace = "velero" var ( - veleroScheduleGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "schedules"} - veleroBackupGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backups"} + veleroScheduleGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "schedules"} + veleroBackupGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backups"} + veleroBackupRepoGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backuprepositories"} ) // demoBackupScheduleName : nom du Schedule Velero dédié à ce namespace de @@ -857,14 +858,24 @@ func (h *HelmProvisioner) ensureDemoBackupSchedule(namespace string) error { return nil } -// deleteDemoBackups supprime le Schedule Velero de cette démo ainsi que -// toutes les sauvegardes déjà prises par ce planning (la suppression d'un -// Backup déclenche le nettoyage asynchrone des données Kopia associées côté -// Velero — pas de suppression directe dans le bucket S3, qui risquerait de -// casser le dépôt Kopia partagé par les autres démos). Appelée depuis -// Teardown (expiration TTL et destruction forcée passent toutes les deux -// par ce même chemin) et lors d'une migration premium réussie (l'ancien -// namespace disparaît, son planning de sauvegarde n'a plus de sens). +// deleteDemoBackups supprime le Schedule Velero de cette démo, toutes les +// sauvegardes déjà prises par ce planning, et le BackupRepository associé +// (la suppression d'un Backup déclenche le nettoyage asynchrone des données +// Kopia associées côté Velero — pas de suppression directe dans le bucket +// S3, qui risquerait de casser le dépôt Kopia partagé par les autres +// démos). Appelée depuis Teardown (expiration TTL et destruction forcée +// passent toutes les deux par ce même chemin) et lors d'une migration +// premium réussie (l'ancien namespace disparaît, son planning de sauvegarde +// n'a plus de sens). +// +// Le BackupRepository (l'objet qui référence le dépôt Kopia du namespace, +// nommé "-default-kopia") n'est PAS supprimé automatiquement par +// Velero quand ses Backup disparaissent — sans ce nettoyage explicite, il +// reste indéfiniment et Velero continue de lancer des jobs de maintenance +// périodiques dessus (kopia-maintain-job) pour un dépôt qui ne sert plus à +// rien. Supprimer l'objet ne touche pas aux données déjà écrites dans le +// bucket S3 (même logique que pour les Backup ci-dessus), juste le +// bookkeeping Velero. func (h *HelmProvisioner) deleteDemoBackups(namespace string) error { name := demoBackupScheduleName(namespace) @@ -886,6 +897,13 @@ func (h *HelmProvisioner) deleteDemoBackups(namespace string) error { if err != nil && !k8sErrors.IsNotFound(err) { return fmt.Errorf("suppression planning %s: %w", name, err) } + + repoName := namespace + "-default-kopia" + err = h.dynamicClient.Resource(veleroBackupRepoGVR).Namespace(veleroNamespace). + Delete(context.Background(), repoName, metav1.DeleteOptions{}) + if err != nil && !k8sErrors.IsNotFound(err) { + return fmt.Errorf("suppression backuprepository %s: %w", repoName, err) + } return nil } diff --git a/deploy/beszel-agent-secret.yml b/deploy/beszel-agent-secret.yml index cbedc63..a3e0908 100644 --- a/deploy/beszel-agent-secret.yml +++ b/deploy/beszel-agent-secret.yml @@ -1,8 +1,17 @@ -# 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). +# Les nœuds k3s ne sont pas sur le VPN (voir omnex/ansible/inventory/host.ini) +# et le dashboard Beszel (8090) doit rester accessible uniquement via le VPN +# (10.0.0.0/24) — donc pas question de pointer HUB_URL dessus. Le Hub expose +# à la place un port dédié 10002 (nginx, restreint aux 4 IP publiques des +# nœuds k3s-prod + chemin /api/ uniquement — voir +# ansible-mln/monitoring/security/nginx/nginx.conf et +# firewall-monitoring-uber.sh) : c'est CE port que les agents utilisent pour +# s'auto-enregistrer, jamais le dashboard. +# +# TOKEN : token universel généré dans le Hub (Add System > Universal token, +# expire au bout d'1h mais un agent déjà enregistré garde son identité). +# KEY : clé publique SSH du Hub (une seule pour tous les agents, quel que +# soit le système) — même valeur que celle déjà utilisée par l'agent +# s3-uber (voir ansible-mln/monitoring/s3-backup/docker-compose.yml). apiVersion: v1 kind: Secret metadata: @@ -10,6 +19,6 @@ metadata: namespace: monitoring type: Opaque stringData: - HUB_URL: "" # ex: https://beszel.exemple.com + HUB_URL: "https://185.103.167.138:10002" TOKEN: "" # token universel généré dans le Hub - KEY: "" # clé publique SSH affichée par le Hub + KEY: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwRVn66crRDX5iL0bxNOnrp3OhTiXHKs+COx/bo5vmQ beszel-agent" diff --git a/deploy/beszel-networkpolicy.yml b/deploy/beszel-networkpolicy.yml index 1075c20..e228100 100644 --- a/deploy/beszel-networkpolicy.yml +++ b/deploy/beszel-networkpolicy.yml @@ -1,9 +1,11 @@ # Isolation réseau du namespace monitoring (beszel-agent) — deny-by-default + # allowlist : -# - Egress : HTTPS sortant vers le Hub (HUB_URL, hôte externe arbitraire — -# voir beszel-agent-secret.yml) pour l'auto-enregistrement au démarrage. +# - Egress : HTTPS sortant vers le Hub sur son port dédié agents (10002, +# pas 8090/dashboard — voir beszel-agent-secret.yml et +# ansible-mln/monitoring/security/nginx/nginx.conf) pour +# l'auto-enregistrement au démarrage. # - Ingress : le Hub se connecte à l'agent en SSH sur le port LISTEN -# (45876) pour récupérer les métriques — IP du Hub non fixe/connue +# (10002) pour récupérer les métriques — IP du Hub non fixe/connue # d'avance, ouvert largement mais authentifié par TOKEN/KEY (voir # beszel-agent-secret.yml), pas par l'origine réseau. # @@ -14,7 +16,7 @@ # GlobalNetworkPolicy Calico, une ressource différente, pour un enforcement # réel au niveau du nœud). Ce manifeste reste appliqué pour la correction # d'intention et si hostNetwork est retiré un jour, mais ne pas compter -# dessus comme protection effective du port 45876 en l'état — un pare-feu +# dessus comme protection effective du port 10002 en l'état — un pare-feu # hôte (iptables/nftables/ufw) ou une GlobalNetworkPolicy Calico est # nécessaire pour une vraie restriction. @@ -43,7 +45,7 @@ spec: - from: [] ports: - protocol: TCP - port: 45876 + port: 10002 --- apiVersion: networking.k8s.io/v1 @@ -86,4 +88,4 @@ spec: - 192.168.0.0/16 ports: - protocol: TCP - port: 443 + port: 10002 diff --git a/deploy/chart-gestion/waf-cluster/templates/configmap.yaml b/deploy/chart-gestion/waf-cluster/templates/configmap.yaml index b8958a3..ef11740 100644 --- a/deploy/chart-gestion/waf-cluster/templates/configmap.yaml +++ b/deploy/chart-gestion/waf-cluster/templates/configmap.yaml @@ -83,6 +83,13 @@ data: # mécanisme que docker/waf (omnex-prod). SecAuditLogParts ABIFHZ + # 911100 (REQUEST-911-METHOD-ENFORCEMENT) n'autorise par défaut que GET + # HEAD POST OPTIONS — bloque tout DELETE/PUT/PATCH légitime de l'API + # REST. Même correctif appliqué côté docker/waf/custom-rules.conf + # (omnex-prod) et côté Coraza/Traefik (deploy/chart-gestion/traefik/ + # templates/middleware-waf.yaml). + SecRuleRemoveById 911100 + SecRule IP:BANNED "@eq 1" \ "id:100000,phase:1,deny,status:403,log,\ msg:'IP is banned'" diff --git a/deploy/daemonset-beszel.yml b/deploy/daemonset-beszel.yml index 729b3cc..0c3a383 100644 --- a/deploy/daemonset-beszel.yml +++ b/deploy/daemonset-beszel.yml @@ -45,7 +45,7 @@ spec: name: beszel-agent-hub env: - name: LISTEN - value: "45876" + value: "10002" - name: SYSTEM_NAME valueFrom: fieldRef: diff --git a/deploy/rbac/control-plane.yml b/deploy/rbac/control-plane.yml index 0a97c0f..479ff05 100644 --- a/deploy/rbac/control-plane.yml +++ b/deploy/rbac/control-plane.yml @@ -137,6 +137,36 @@ roleRef: kind: ClusterRole name: omnex-namespace-manager --- +# "nodes" n'est pas un type namespaced : impossible de le scoper via un +# RoleBinding par démo comme omnex-demo-workload-manager, d'où une +# ClusterRole/ClusterRoleBinding dédiée. Portée strictement à "get" sur le +# sous-type "nodes/proxy" — c'est tout ce dont checkPVCUsage a besoin +# (voir alerts/resources.go:fetchNodeSummary, équivalent de +# `kubectl get --raw /api/v1/nodes//proxy/stats/summary`) pour lire +# le remplissage des PVC. Aucun accès en écriture, aucun accès aux autres +# sous-ressources de Node. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: omnex-node-stats-reader +rules: + - apiGroups: [""] + resources: [nodes/proxy] + verbs: [get] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: omnex-control-plane-node-stats +subjects: + - kind: ServiceAccount + name: omnex-control-plane + namespace: omnex-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: omnex-node-stats-reader +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -166,8 +196,8 @@ type: kubernetes.io/service-account-token # namespace fixe préexistant, pas un namespace de démo créé dynamiquement # par le control-plane — pas besoin du mécanisme "bind" utilisé pour # omnex-demo-workload-manager). Portée strictement limitée aux Schedule/ -# Backup Velero — aucun accès aux Secrets (dont cloud-credentials, la clé -# du bucket S3) ni au reste du namespace velero. +# Backup/BackupRepository Velero — aucun accès aux Secrets (dont +# cloud-credentials, la clé du bucket S3) ni au reste du namespace velero. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -178,8 +208,10 @@ rules: # Un Schedule + les Backup qu'il crée par démo (voir # HelmProvisioner.ensureDemoBackupSchedule/deleteDemoBackups) — jamais # de wildcard multi-namespace, pour pouvoir supprimer les sauvegardes - # d'UNE démo sans toucher aux autres. - resources: [schedules, backups] + # d'UNE démo sans toucher aux autres. backuprepositories : nettoyage du + # dépôt Kopia du namespace détruit (deleteDemoBackups) — sans quoi + # Velero continue de lancer des jobs de maintenance dessus indéfiniment. + resources: [schedules, backups, backuprepositories] verbs: [get, list, watch, create, delete] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/web/src/pages/backoffice/Demos.tsx b/web/src/pages/backoffice/Demos.tsx index e93889c..940e604 100644 --- a/web/src/pages/backoffice/Demos.tsx +++ b/web/src/pages/backoffice/Demos.tsx @@ -61,7 +61,9 @@ export function Demos() { // Les démos passées en abonnement payant vivent désormais dans le // dashboard Premium (n'expirent plus, stockage persistant) — ne plus // les mélanger avec les démos d'essai ici. - setDemos((res.items ?? []).filter((d) => d.type_abonnement !== 'premium')) + setDemos( + (res.items ?? []).filter((d) => d.type_abonnement !== 'premium' && d.status !== 'expired'), + ) } catch (err) { if (err instanceof ApiError && err.status === 401) navigate('/admin/login') else toast({ status: 'error', title: 'Chargement des démos impossible' }) diff --git a/web/src/pages/backoffice/PremiumDemos.tsx b/web/src/pages/backoffice/PremiumDemos.tsx index fe70041..dc35e81 100644 --- a/web/src/pages/backoffice/PremiumDemos.tsx +++ b/web/src/pages/backoffice/PremiumDemos.tsx @@ -56,7 +56,9 @@ export function PremiumDemos() { const load = useCallback(async () => { try { const res = await api.listDemos() - setDemos((res.items ?? []).filter((d) => d.type_abonnement === 'premium')) + setDemos( + (res.items ?? []).filter((d) => d.type_abonnement === 'premium' && d.status !== 'expired'), + ) } catch (err) { if (err instanceof ApiError && err.status === 401) navigate('/admin/login') else toast({ status: 'error', title: 'Chargement des démos impossible' })