Architecture and deployment¶
A shared server is made of two independent processes sharing the same database. They can run in two containers, two pods or on two machines.
agents, CI ──▶ ┌──────────────────────┐ ┌──────────────────────────┐ ◀── browser,
│ MCP server │ │ controller │ admins
│ security-review-mcp │ │ security-review- │
│ /mcp, /api/v1/enroll │ │ controller serve │
│ /healthz │ │ /api/v1, /ui, /healthz │
└──────────┬───────────┘ └────────────┬─────────────┘
└──────────── PostgreSQL ─────────────┘
| MCP server | Controller | |
|---|---|---|
| Command | security-review-mcp serve --transport streamable-http |
security-review-controller serve |
| Serves | /mcp, public enrollment POST /api/v1/enroll, /healthz |
REST API /api/v1, interface /ui, /healthz |
| Accepted credentials | mcp and admin tokens |
web sessions (users), admin and api tokens; ci tokens on the pipeline routes /api/v1/ci/ only (gate, SARIF, trend, scans) |
| Routes without a token | enrollment, /healthz |
/ui files, API documentation, /healthz (read only) |
| Database schema | only checks it | migrates it (unless SECREVIEW_DB_AUTO_UPGRADE=false) |
| Advised exposure | agents and CI, with TLS | internal network or access restricted to administrators |
The two processes never call each other: everything goes through the database. A token created or revoked on the controller applies to the next request of the MCP server. A changed rule is reloaded within a few seconds. A project requested on the MCP server waits in the Projects screen of the controller, for an approval.
The enrollment is served by the MCP server because the client only knows its URL. The controller thus exposes no unauthenticated write route.
Shared database
With SQLite, both processes must run on the same machine and share the file. As soon as
they are in different pods or on different machines, use PostgreSQL (postgres extra,
image built with --build-arg EXTRAS=postgres).
With stdio (developer workstation), only security-review-mcp is needed: it creates and
migrates its local database itself.
Schema migrations¶
The schema is managed by Alembic migrations, embedded in the
package (no alembic.ini file to provide):
security-review-controller db upgrade # creates or migrates the schema, then seeds the default rules
security-review-controller db status # {"current": "0001", "head": "0001", "up_to_date": true}
db statusreturns code1if the schema is not up to date: handy for a script or a probe.- The HTTP MCP server never migrates: it refuses to start (code
2, messagerun security-review-controller db upgrade first) while the database is not at the expected revision. Under Kubernetes, the pod restarts until the migration is done. - The controller, the stdio MCP server and the CLI commands migrate at start-up, unless
SECREVIEW_DB_AUTO_UPGRADE=false. They then only check the schema. Disable the automatic migration when a dedicated task handles it (migrateservice of Docker Compose, Kubernetes init container): one migration at a time.
Docker Compose¶
docker-compose.yml starts PostgreSQL, the migration, the controller and the MCP server: see
Docker.
Kubernetes¶
The
examples/kubernetes/security-review.yaml
example deploys both processes on an existing PostgreSQL database:
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 exec deploy/secreview-controller -c controller -- security-review-controller user reset-password admin # initial admin password
It contains:
- a
secreview-controllerDeployment (one replica,Recreatestrategy) whose init container runssecurity-review-controller db upgradeat every deployment; - a
secreview-mcpDeployment, which only checks the schema; - the
secreview-controllerSecret, created by hand (see below), withSECREVIEW_SECRET_KEY: without it, the controller draws a random session secret at every start, so the web sessions would end at every restart and would not be shared by the replicas (see Login and sessions); - one Service per process,
readinessandlivenessprobes on/healthz, non-root containers with a read-only file system, without any Linux capability; - a NetworkPolicy that limits access to the controller to the pods of the namespace and to the ingress controller (adapt its label).
The two Secrets are not in the example, so that kubectl apply never replaces their values. To
declare them as manifests rather than with kubectl create secret (GitOps), use kind: Secret:
apiVersion: v1
kind: Secret
metadata:
name: secreview-db
namespace: secreview
type: Opaque
stringData:
SECREVIEW_DATABASE_URL: postgresql+psycopg2://secreview:<password>@<host>:5432/secreview
---
apiVersion: v1
kind: Secret
metadata:
name: secreview-controller
namespace: secreview
type: Opaque
stringData:
# openssl rand -hex 32 (at least 32 characters, never changed between deployments)
SECREVIEW_SECRET_KEY: <64 hexadecimal characters>
# Proxy holding credentials (OSV download, OIDC), optional
# HTTPS_PROXY: http://user:<password>@proxy.example.com:3128
stringData takes the values in clear; Kubernetes stores them base64-encoded in data. Never
commit this file with real values: encrypt it (Sealed Secrets, SOPS) or let an operator such as
External Secrets create it from your vault. The pods read the Secrets at start-up: after a
change, restart them (kubectl -n secreview rollout restart deploy/secreview-controller).
Changing SECREVIEW_SECRET_KEY ends every web session.
Replace the image (registry.example.com/security-review-mcp:pg) with yours, built with the
postgres extra. Expose the secreview-mcp Service to the agents and to the CI through an
Ingress with TLS. Keep the controller internal (kubectl port-forward, VPN or Ingress
restricted to administrators). Behind the Ingress, set SECREVIEW_FORWARDED_ALLOW_IPS (commented
in the ConfigMap) to the address range of the ingress controller pods: otherwise every client
shares their address for the login throttling and the enrollment limits. Administration commands run in its pod:
kubectl -n secreview exec deploy/secreview-controller -c controller -- \
security-review-controller token list
For the daily update of the OSV advisories,
uncomment SECREVIEW_OSV_AUTO_UPDATE (and if needed SECREVIEW_OSV_ECOSYSTEMS, HTTPS_PROXY
and NO_PROXY, in the secreview-controller Secret if the proxy holds credentials) on the
controller container, and allow its egress to osv.dev or your mirror (or the proxy) if an
egress NetworkPolicy applies. The MCP server needs no egress. The Swagger documentation of the
REST API (/api/docs) stays disabled unless you uncomment SECREVIEW_SWAGGER_ENABLED.
Several replicas of the MCP server
MCP sessions (streamable HTTP transport) are kept in memory by the pod that opened them.
Before raising replicas, configure the Ingress to send every request of a session to the
same pod (affinity on the Mcp-Session-Id header, or failing that cookie or address
affinity). The controller can have several replicas, provided the migration is done by a
single task (SECREVIEW_DB_AUTO_UPGRADE=false on the replicas).