Kubernetes¶
Deux manifestes déploient tout dans le namespace secreview :
examples/kubernetes/postgres.yaml
(PostgreSQL, image officielle) et
examples/kubernetes/security-review.yaml
(contrôleur et serveur MCP, image publiée ghcr.io/cyr-ius/security-review:latest).
Déploiement¶
Le bloc qui déploie tout, avec le PostgreSQL de postgres.yaml, est dans
Mise en service du serveur.
Avec un PostgreSQL existant : une base managée (Amazon RDS, Azure Database for PostgreSQL, Google Cloud SQL...) ou un
opérateur (CloudNativePG...) qui contient une base secreview vide, propriété d'un rôle
secreview. Remplacez <password> et <host> ; ajoutez ?sslmode=verify-full à l'URL
quand le serveur propose le TLS.
kubectl create namespace secreview
kubectl -n secreview create secret generic secreview-db \
--from-literal=SECREVIEW_DATABASE_URL='postgresql+psycopg2://secreview:<password>@<host>:5432/secreview'
kubectl -n secreview create secret generic secreview-controller \
--from-literal=SECREVIEW_SECRET_KEY="$(openssl rand -hex 32)"
kubectl -n secreview apply -f https://raw.githubusercontent.com/cyr-ius/security-review/v5.0.0/examples/kubernetes/security-review.yaml
kubectl -n secreview rollout status deploy/secreview-controller
kubectl -n secreview rollout status deploy/secreview-mcp
kubectl -n secreview exec deploy/secreview-controller -c controller -- \
security-review-controller user reset-password admin
Contenu des manifestes¶
postgres.yaml contient un StatefulSet secreview-postgres (image officielle
postgres:18-alpine, une instance, volume de 10 Gio, conteneur non root avec un système de
fichiers en lecture seule), son Service et une NetworkPolicy qui ne laisse que les pods de
security-review-mcp atteindre le port 5432. En production, préférez une base managée ou un
opérateur, qui gèrent la réplication, les sauvegardes et les mises à niveau.
security-review.yaml contient :
- un Deployment
secreview-controller(une réplique, stratégieRecreate) dont l'init container lancesecurity-review-controller db upgradeà chaque déploiement ; - un Deployment
secreview-mcp, qui vérifie seulement le schéma à son démarrage ; - une ConfigMap
secreview-configpartagée par les deux processus ; - un Service par processus, des sondes
readinessetlivenesssur/healthz, des conteneurs non root en système de fichiers en lecture seule, sans capacité Linux ; - une NetworkPolicy qui limite l'accès au contrôleur aux pods du namespace et à l'ingress controller (adaptez son label).
Le contrôleur lit aussi le Secret secreview-controller, créé à la main (voir
Secrets), avec SECREVIEW_SECRET_KEY : sans lui, le contrôleur tire un secret de
session aléatoire à chaque démarrage, les sessions web prendraient donc fin à chaque
redémarrage et ne seraient pas partagées entre réplicas (voir
Connexion et sessions).
Contenu des manifestes de la version v5.0.0, appliqués par ces commandes :
# Single-instance PostgreSQL for security-review-mcp, with the official image (postgres:18-alpine).
# For production, prefer a managed database or an operator (CloudNativePG...) that handles
# replication, backups and upgrades.
#
# kubectl -n secreview create secret generic secreview-postgres \
# --from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 24)"
# kubectl -n secreview apply -f examples/kubernetes/postgres.yaml
# kubectl -n secreview create secret generic secreview-db \
# --from-literal=SECREVIEW_DATABASE_URL="postgresql+psycopg2://secreview:$(kubectl -n secreview \
# get secret secreview-postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)@secreview-postgres:5432/secreview"
#
# The Secret is not in this file, so that applying it never replaces the password.
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: secreview-postgres
labels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/part-of: security-review-mcp
spec:
serviceName: secreview-postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/part-of: security-review-mcp
template:
metadata:
labels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/part-of: security-review-mcp
spec:
# uid and gid of the postgres user of the Alpine image
securityContext:
runAsNonRoot: true
runAsUser: 70
runAsGroup: 70
fsGroup: 70
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: postgres
image: postgres:18-alpine
env:
- name: POSTGRES_DB
value: secreview
- name: POSTGRES_USER
value: secreview
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: secreview-postgres
key: POSTGRES_PASSWORD
ports:
- name: postgres
containerPort: 5432
readinessProbe: &probe
exec:
command: ["pg_isready", "-U", "secreview", "-d", "secreview"]
periodSeconds: 10
livenessProbe: *probe
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
# Since PostgreSQL 18, the image keeps its data under /var/lib/postgresql/18/docker
- name: data
mountPath: /var/lib/postgresql
- name: run
mountPath: /var/run/postgresql
- name: tmp
mountPath: /tmp
volumes:
- name: run
emptyDir: {}
- name: tmp
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: secreview-postgres
spec:
selector:
app.kubernetes.io/name: postgresql
app.kubernetes.io/part-of: security-review-mcp
ports:
- name: postgres
port: 5432
targetPort: postgres
---
# Only the pods of security-review-mcp (controller, MCP server) reach the database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: secreview-postgres
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/part-of: security-review-mcp
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: security-review-mcp
ports:
- port: 5432
# security-review-mcp on Kubernetes: the MCP server and the controller in two Deployments
# sharing a PostgreSQL database (not included: examples/kubernetes/postgres.yaml deploys the
# official image, or use your managed or operator-backed instance).
#
# kubectl create namespace secreview
# kubectl -n secreview create secret generic secreview-db \
# --from-literal=SECREVIEW_DATABASE_URL='postgresql+psycopg2://secreview:<password>@<host>:5432/secreview'
# kubectl -n secreview create secret generic secreview-controller \
# --from-literal=SECREVIEW_SECRET_KEY="$(openssl rand -hex 32)"
# kubectl -n secreview apply -f examples/kubernetes/security-review.yaml
# kubectl -n secreview rollout status deploy/secreview-controller
# kubectl -n secreview exec deploy/secreview-controller -c controller -- \
# security-review-controller user reset-password admin # initial admin password
#
# The published image contains the PostgreSQL driver; pin a version (X.Y.Z) in production.
#
# The controller migrates the schema in an init container (Recreate strategy: the old controller
# pods stop before the new schema is applied; concurrent migrations wait for each other on a
# PostgreSQL lock). The MCP pods only check the schema when they start: new ones restart until
# the controller has migrated it. Expose each Service through its own Ingress with TLS: the MCP
# server to the agents, the controller to the people (web interface, personal MCP tokens) and
# to the CI (security-review-ci calls the REST API of the controller).
#
# The secreview-controller Secret gives SECREVIEW_SECRET_KEY: without it, the controller draws
# a random session secret at every start (sessions end at restart and are not shared by the
# replicas). Put the proxy credentials there too. The Secrets are not in this file, so that
# applying it never replaces them; as manifests (keep them out of git, or encrypted):
#
# apiVersion: v1
# kind: Secret
# metadata:
# name: secreview-controller
# type: Opaque
# stringData:
# SECREVIEW_SECRET_KEY: <openssl rand -hex 32>
---
apiVersion: v1
kind: ConfigMap
metadata:
name: secreview-config
data:
SECREVIEW_LOG_LEVEL: INFO
# Only the init container of the controller migrates the schema
SECREVIEW_DB_AUTO_UPGRADE: "false"
# Address range of the ingress controller pods: their X-Forwarded-For gives the client
# address (login throttling, enrollment limits, logs). Adapt it to your cluster.
# SECREVIEW_FORWARDED_ALLOW_IPS: "10.0.0.0/8"
# Public URL of the web interface, given by the MCP server to the clients without a valid
# token (enrollment, 401) so that the user knows where to create a personal MCP token
# SECREVIEW_WEB_URL: https://secreview.example.com/ui
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: secreview-controller
labels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: controller
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: controller
template:
metadata:
labels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: controller
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
initContainers:
- name: migrate
image: ghcr.io/cyr-ius/security-review:latest
command: ["security-review-controller", "db", "upgrade"]
envFrom: &env
- configMapRef:
name: secreview-config
- secretRef:
name: secreview-db
securityContext: &container-security
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts: &mounts
- name: tmp
mountPath: /tmp
containers:
- name: controller
image: ghcr.io/cyr-ius/security-review:latest
# The image listens on every interface of the pod, port 8000
command: ["security-review-controller", "serve"]
envFrom:
- configMapRef:
name: secreview-config
- secretRef:
name: secreview-db
# SECREVIEW_SECRET_KEY (and the proxy variables when they hold credentials)
- secretRef:
name: secreview-controller
env:
# Swagger documentation of the REST API on /api/docs (disabled by default)
# - name: SECREVIEW_SWAGGER_ENABLED
# value: "true"
# Daily download of the OSV advisories (needs egress to
# osv-vulnerabilities.storage.googleapis.com or a mirror);
# put credentials of the proxy in the secreview-controller Secret rather than here
# - name: SECREVIEW_OSV_AUTO_UPDATE
# value: "true"
# - name: SECREVIEW_OSV_ECOSYSTEMS
# value: PyPI,npm,Maven
# Proxy of the OSV download and of the OIDC provider (standard variables)
# - name: HTTPS_PROXY
# value: http://proxy.example.com:3128
# - name: HTTP_PROXY # fallback when HTTPS_PROXY is unset
# value: http://proxy.example.com:3128
# - name: NO_PROXY
# value: .svc,.cluster.local
ports:
- name: http
containerPort: 8000
readinessProbe: &probe
httpGet:
path: /healthz
port: http
periodSeconds: 10
livenessProbe: *probe
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
securityContext: *container-security
volumeMounts: *mounts
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: secreview-controller
spec:
selector:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: controller
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: secreview-mcp
labels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: mcp
spec:
# MCP sessions live in the memory of a pod: before adding replicas, make the Ingress route
# each session to the same pod (affinity on the Mcp-Session-Id header)
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: mcp
template:
metadata:
labels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: mcp
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: mcp
image: ghcr.io/cyr-ius/security-review:latest
# The image runs `serve`: streamable HTTP on every interface of the pod, port 8000
envFrom:
- configMapRef:
name: secreview-config
- secretRef:
name: secreview-db
ports:
- name: http
containerPort: 8000
readinessProbe: &probe
httpGet:
path: /healthz
port: http
periodSeconds: 10
livenessProbe: *probe
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: secreview-mcp
spec:
selector:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: mcp
ports:
- name: http
port: 80
targetPort: http
---
# Only the pods of the namespace and the ingress controller (adjust its label) reach the
# controller: people and the CI go through its Ingress. The MCP server stays reachable for the
# agents through its own Ingress.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: secreview-controller
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: security-review-mcp
app.kubernetes.io/component: controller
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector: {}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 8000
Secrets¶
Les Secrets (secreview-postgres, secreview-db, secreview-controller) ne sont pas dans les
exemples, pour que kubectl apply ne remplace jamais leurs
valeurs. Le mot de passe de secreview-postgres et celui de l'URL de secreview-db doivent être
identiques. Pour les déclarer en manifestes plutôt qu'avec kubectl create secret (GitOps),
utilisez kind: Secret :
apiVersion: v1
kind: Secret
metadata:
name: secreview-postgres
namespace: secreview
type: Opaque
stringData:
POSTGRES_PASSWORD: <password>
---
apiVersion: v1
kind: Secret
metadata:
name: secreview-db
namespace: secreview
type: Opaque
stringData:
SECREVIEW_DATABASE_URL: postgresql+psycopg2://secreview:<password>@secreview-postgres:5432/secreview
---
apiVersion: v1
kind: Secret
metadata:
name: secreview-controller
namespace: secreview
type: Opaque
stringData:
# openssl rand -hex 32 (32 caractères au moins, inchangé d'un déploiement à l'autre)
SECREVIEW_SECRET_KEY: <64 caractères hexadécimaux>
# Proxy avec identifiants (téléchargement OSV, OIDC), facultatif
# HTTPS_PROXY: http://user:<password>@proxy.example.com:3128
stringData reçoit les valeurs en clair ; Kubernetes les stocke encodées en base64 dans data.
Ne commitez jamais ce fichier avec de vraies valeurs : chiffrez-le (Sealed Secrets, SOPS) ou
laissez un opérateur comme External Secrets le créer depuis votre coffre. Les pods lisent les
Secrets au démarrage : après un changement, redémarrez-les
(kubectl -n secreview rollout restart deploy/secreview-controller). Changer
SECREVIEW_SECRET_KEY termine toutes les sessions web.
Image, exposition et administration¶
Le manifeste utilise l'image publiée ghcr.io/cyr-ius/security-review:latest, qui
contient le pilote PostgreSQL : épinglez une version (X.Y.Z) en production. Les manifestes
ne contiennent pas d'Ingress : exposez chaque Service par son propre Ingress avec TLS.
| Service | Atteint par | Ingress |
|---|---|---|
secreview-mcp |
les agents (MCP sur /mcp, enrôlement) |
vers tous les postes des développeurs |
secreview-controller |
les personnes (interface web /ui, jetons MCP et CI personnels) et la CI (security-review-ci appelle /api/v1/ci) |
vers les développeurs et les runners de CI ; kubectl port-forward suffit pour un premier essai |
Ensuite, dans la ConfigMap :
- réglez
SECREVIEW_WEB_URLsur l'URL publique de l'interface web (https://…/ui) : le serveur MCP la donne aux clients sans jeton valide, pour que l'utilisateur sache où créer un jeton MCP personnel ; - réglez
SECREVIEW_FORWARDED_ALLOW_IPSsur la plage d'adresses des pods de l'ingress controller : sinon tous les clients partagent leur adresse pour la limitation des connexions, la limite de débit et les limites d'enrôlement.
La NetworkPolicy du contrôleur laisse entrer l'ingress controller du namespace ingress-nginx :
adaptez son label au vôtre. Les commandes d'administration s'exécutent dans le pod du
contrôleur :
kubectl -n secreview exec deploy/secreview-controller -c controller -- \
security-review-controller token list
Mise à jour OSV¶
Pour la mise à jour quotidienne des avis OSV,
décommentez SECREVIEW_OSV_AUTO_UPDATE (et au besoin SECREVIEW_OSV_ECOSYSTEMS, HTTPS_PROXY
et NO_PROXY, dans le Secret secreview-controller si le proxy contient des identifiants) sur
le conteneur controller, et autorisez sa sortie vers osv-vulnerabilities.storage.googleapis.com (SECREVIEW_OSV_URL),
votre miroir ou le proxy si une NetworkPolicy d'egress s'applique. Le serveur MCP
n'a besoin d'aucune sortie. La documentation Swagger de l'API REST (/api/docs) reste
désactivée tant que vous ne décommentez pas SECREVIEW_SWAGGER_ENABLED.
Plusieurs réplicas¶
Plusieurs réplicas du serveur MCP
Les sessions MCP (transport streamable HTTP) sont gardées en mémoire par le pod qui les a
ouvertes. Avant d'augmenter replicas, configurez l'Ingress pour envoyer toutes les
requêtes d'une session au même pod (affinité sur l'en-tête Mcp-Session-Id, ou à défaut
affinité par cookie ou par adresse). Le contrôleur peut, lui, avoir plusieurs réplicas :
leurs migrations s'attendent (verrou consultatif PostgreSQL), et SECREVIEW_SECRET_KEY leur
permet de partager les sessions web.