Skip to content

HTTP server, authentication and TLS

A shared server (team, CI/CD) is made of two processes sharing the same database (see Architecture and deployment):

  • the MCP server (security-review-mcp serve --transport streamable-http): /mcp endpoint, public enrollment (/api/v1/enroll) and /healthz probe; it is the one exposed to the agents and to the CI;
  • the controller (security-review-controller serve): /api/v1 REST API, /ui/ web interface and /healthz probe; it can stay on an internal network.

MCP authentication and TLS are optional and independent; the controller API always requires a token. With stdio, the host starts the process itself and the controller is not needed.

security-review-controller db upgrade                        # creates or migrates the schema
security-review-controller serve --host 127.0.0.1 --port 8080
# web interface: http://127.0.0.1:8080/ui/
security-review-mcp serve --transport streamable-http --host 127.0.0.1 --port 8000
# MCP endpoint: http://127.0.0.1:8000/mcp

With SQLite, both processes must run on the same machine (same file); use PostgreSQL as soon as they are on different machines or pods.

Bearer tokens

Tokens are stored in the database (apitoken table): only their SHA-256 digest is kept, the secret is shown only once, at creation. Manage them with the token command (see the CLI):

security-review-controller token create --label ci                      # mcp role by default
security-review-controller token create --label jenkins --role ci --project shop-api
security-review-controller token create --label ops-script --role admin
# {"id": "6f1c…", "label": "ci", "role": "mcp", "project": null, "created_at": "…", "token": "UJB7…"}
security-review-controller token list [--project shop-api]             # never the secret nor the digest
security-review-controller token revoke ci                              # by label or by id

Developers can also create a personal MCP token in the web interface, after enrolling their project: see enrollment.

  • Give token to the client; the unique label appears in the logs.
  • --role is mcp by default (MCP server); admin reaches the MCP server and the whole REST API of the controller, never the web interface; ci (always with --project) only reaches the pipeline routes used by security-review-ci: see the roles; metrics (never with --project) only reaches the Prometheus metrics of the controller. Tokens are for machines: there is no reader or triager token, a person reads and triages with a user account and its groups. The api tokens of the REST API and its Swagger, which hold the rights of a user or of a role, are created in the web interface (API tokens).
  • --expires-in-days sets the validity of the token: see expiry and rotation.
  • --project limits the token to one or more projects (repeat the option), for the MCP tools and the pipeline routes (mcp and ci roles; refused for admin and metrics). See tokens restricted to a project. Administrators can also create and revoke restricted mcp and ci tokens from the Tokens screen.
  • The server reads the database at every request: a creation or a revocation applies immediately, including to the other instances sharing the same PostgreSQL database.

The MCP endpoint and the controller API require Authorization: Bearer <token>. Still public: /healthz on both processes, /api/v1/enroll on the MCP server, and on the controller the static files of /ui/, the /api/favicon.svg icon, the /api/health liveness of the API and, with SECREVIEW_SWAGGER_ENABLED=true (disabled by default), the Swagger documentation (/api/docs, /api/openapi.json schema, /api/swagger/ files). While no token exists, the controller API answers 503 and the MCP server stays closed: a client without a token only reaches enrollment, any other request gets 401, whatever the bearer value (a warning is logged at start-up). /mcp is only open in that case when the server listens on localhost (127.0.0.1, ::1) or when SECREVIEW_ALLOW_UNAUTHENTICATED=true, to keep for tests on an isolated network: as soon as a token exists, it is required in every case. token revoke warns on stderr when it deletes the last token.

Every creation, rotation and revocation of a token, and every enrollment, is written to the audit log, with who made it.

Expiry and rotation of tokens

A token may expire: after its expiry date it authenticates nothing (401, error="invalid_token", description The bearer token has expired, followed on /mcp by how to get a new one), but it stays listed so that an administrator rotates or revokes it. SECREVIEW_TOKEN_MAX_DAYS (default 0) sets the longest validity of a token created or rotated, and its default validity; with 0, tokens do not expire unless a validity is asked for.

security-review-controller token create --label shop-ci --project shop-api --expires-in-days 90
security-review-controller token rotate shop-ci --grace-hours 24     # new secret, shown once
# {"id": "6f1c…", "label": "shop-ci", …, "expires_at": "…", "rotated_at": "…", "grace_until": "…", "token": "pX4q…"}

A rotation replaces the secret of a token: it keeps its id, label, role and projects, and gets a new validity (--expires-in-days, default as at creation). During --grace-hours (0 by default, at most 168) the previous secret keeps working, the time to update the clients (grace_until in token list); after that, only the new one authenticates. The previous secret of an expired token never comes back. After a leak, rotate without a grace period (or revoke): the leaked secret then stops at once. A personal token cannot be rotated: its user creates a new one. In the web interface, the Tokens screen rotates the project tokens (button Rotate, grace period chosen above the list).

Expired tokens still count as configured tokens: a server whose tokens have all expired stays closed, it never becomes open.

Tokens restricted to a project

On a server shared by several teams, give each pipeline a token limited to its project:

security-review-controller token create --label shop-ci --project shop-api            # mcp role
security-review-controller token create --label shop-gate --role ci --project shop-api

A token without --project reaches every project. In the web interface, the Tokens screen creates a token for some projects or for All projects, and changes the projects of an existing token (except admin and metrics; a ci token always keeps at least one project). A restricted token:

  • only saves reports (save_security_report) for its project;
  • only reads, lists, exports and proposes a status for (propose_finding_status) the reports and findings of its project; list_security_reports without a filter only returns those;
  • gets, for any other project, report or finding, the same "not found" error as for a missing object, so as not to reveal the other projects;
  • cannot call reload_rules.

The restriction also applies to the report:// resource and to the triage-report prompt. With stdio, there is no token, hence no restriction.

Initial administrator password

At its first start, the controller creates the admin account of the web interface (SECREVIEW_ADMIN_USERNAME), with a random password that nobody knows: set it with the CLI, which prints it once (the old one is invalidated at once), see the first administrator. This account only opens the web interface and the REST API, never /mcp: for an agent, create a token (token create).

security-review-controller user reset-password admin

Enrollment: request the project

The enrollment never creates a token: it requests the project, which an administrator approves, and the developer creates a personal MCP token in the web interface. It is enabled by default (SECREVIEW_ENROLLMENT=true), even before the first token is created (except on an open server, which needs no token).

  1. Request. The client gives the name of its project (usually the repository name). A new project is pending: it is not a project yet and gives no access. A project that already exists, or is already pending, is unchanged, and the answer is the same in every case, so that it does not tell which projects exist. Three ways to enroll:

    • From the agent, at the first review. An MCP client that connects without an Authorization header is not rejected: it reaches a restricted MCP server (same security-review name) that only exposes the request_enrollment tool, and prompts named like the reviews (security-review, codebase-review…) that explain the steps. The agent calls request_enrollment with the project name.
    • From the command line, from the developer workstation or the CI:

      security-review-mcp enroll --url https://secreview.example.internal:8443 \
          --project shop-api --requested-by "alice (shop team)"
      # {"project": "shop-api", "web_url": "https://secreview.example.internal/ui", "next_steps": "…"}
      
    • Through the MCP server API: POST /api/v1/enroll (without a token) with {"project": "shop-api"}, answered with 202. The controller does not serve this endpoint: it exposes no unauthenticated write route.

    • Approval. A user holding projects.create (the admin role among others) approves or rejects the request in the Pending projects section of the Projects screen, or from the CLI:
    security-review-controller project list          # requester, address, expiry
    security-review-controller project approve 3     # id of the list
    security-review-controller project reject 4
    

    The approval creates the project, with its group project:<project> without any member: nobody gains any access, the approver included. A request nobody approved within SECREVIEW_PROJECT_ENROLLMENT_TTL_DAYS (7 days by default) is deleted. 3. Personal token. The answer of the enrollment gives the URL of the web interface (web_url, from SECREVIEW_WEB_URL) and the next steps: the user signs in, creates a personal MCP token in My account (see personal MCP tokens) and puts it in the host configuration. The token reaches the projects the user may read, at each request. 4. Access. An administrator gives the user access to the project by adding them to the group project:<project> (Groups screen, see access control); the personal token reaches the project from the next request, without reconfiguration.

An agent that already has a token may also call request_enrollment (for instance when save_security_report refuses a project its token does not reach): the project is requested the same way, and the token gains no project. When a user with projects.create creates the project directly, its pending request is settled.

Rejected token: sign in again

A client whose token is unknown, revoked or expired gets 401 on /mcp (error="invalid_token"): the description tells the user to sign in to the web interface (with its URL when SECREVIEW_WEB_URL is set) and to create a new personal MCP token, then to replace it in the host configuration. Such a client never reaches the review tools nor the enrollment server. The rejection is logged as a warning by the MCP server (Rejected unauthenticated request / Rejected expired token). security-review-mcp scan-deps gives the same advice.

Safeguards: anybody who reaches the server may enroll, so at most 20 projects are pending at a time, a client address enrolls at most 5 times per hour (429 beyond; behind a reverse proxy, set SECREVIEW_FORWARDED_ALLOW_IPS), and a pending project expires after SECREVIEW_PROJECT_ENROLLMENT_TTL_DAYS. Every request, approval, rejection and expiry is written to the audit log (enrollment.requested, enrollment.approved, enrollment.rejected, with the requester and the client address). To disable the enrollment: SECREVIEW_ENROLLMENT=false (clients without a token then get 401, with the same advice).

--url is the URL of the MCP server (with or without /mcp): it receives the requests.

Tip

The token commands run on the controller side (or in its container), with the same SECREVIEW_DATABASE_URL as the controller. Tokens are never passed as command-line arguments.

TLS

Uvicorn serves HTTPS directly when a certificate is given, for the MCP server as for the controller (same options, same variables):

security-review-mcp serve --transport streamable-http --host 0.0.0.0 --port 8443 \
    --tls-certfile /etc/secreview/tls/cert.pem --tls-keyfile /etc/secreview/tls/key.pem
security-review-controller serve --host 0.0.0.0 --port 9443 \
    --tls-certfile /etc/secreview/tls/cert.pem --tls-keyfile /etc/secreview/tls/key.pem
# or SECREVIEW_TLS_CERTFILE / SECREVIEW_TLS_KEYFILE (+ SECREVIEW_TLS_KEYFILE_PASSWORD)

SECREVIEW_TLS_CLIENT_CA also enables mutual TLS: only the clients presenting a certificate signed by this CA can connect.

Certificate authority

The server never generates a certificate: provide one signed by a certificate authority trusted by the clients, a public CA (for example Let's Encrypt, or cert-manager in Kubernetes) or the internal CA of your organization. Its subjectAltName must contain the names and addresses used by the clients (for example DNS:secreview.lan,IP:192.168.1.1); otherwise the clients refuse the connection even when they trust the CA. The key file (key.pem) never leaves the server: keep it readable by the server account only (chmod 600).

With an internal CA, the clients must trust its certificate: see Host configuration.

HTTPS server with an internal CA

When the server certificate is signed by an internal CA, the host refuses the connection (unable to verify the first certificate, unable to get local issuer certificate). Neither VS Code nor Claude has a per-server TLS option in its MCP configuration: the certificate is added to the trusted authorities of the host process. Claude Code and VS Code run on Node.js, which reads the NODE_EXTRA_CA_CERTS environment variable (PEM file) for this, when the process starts.

  1. Get the certificate of the internal CA (PEM) from its administrators, save it as ~/.config/secreview/ca.pem, and check that the name or address of the MCP url is in the server certificate:

    openssl s_client -connect 192.168.1.1:8000 -CAfile ~/.config/secreview/ca.pem </dev/null \
        2>/dev/null | openssl x509 -noout -ext subjectAltName
    
  2. Declare it to the host, then restart it:

In the shell profile (~/.bashrc, ~/.zshrc), before starting claude:

export NODE_EXTRA_CA_CERTS="$HOME/.config/secreview/ca.pem"

The Claude Code documentation also gives the env key of settings.json ("env": { "NODE_EXTRA_CA_CERTS": "/home/me/.config/secreview/ca.pem" }). The variable applies to every connection of Claude Code, not only to the MCP server.

Start VS Code from a shell where the variable is set (code .), or set it at system level:

export NODE_EXTRA_CA_CERTS="$HOME/.config/secreview/ca.pem"
code .

Another option: add the certificate to the certificate store of the system (Windows: Trusted Root Certification Authorities; macOS: Keychain, Always Trust; Linux: /usr/local/share/ca-certificates/ then sudo update-ca-certificates). VS Code loads the system certificates (http.systemCertificates setting, enabled by default).

In a dev container, the connection leaves from the container: mount the file and set the variable in devcontainer.json ("remoteEnv": { "NODE_EXTRA_CA_CERTS": "..." }), then rebuild or reload the window.

Warning

Do not use NODE_TLS_REJECT_UNAUTHORIZED=0: it disables the verification of every connection of the process (API, extensions, updates), not only the one to the MCP server, and lets an interception through.

The remote connectors added from the Claude web interface (claude.ai, Claude Desktop) are contacted from Anthropic's infrastructure, not from the workstation: they require a server reachable from the Internet with a publicly trusted certificate, which rules out an internal CA.

For the enrollment from the CLI, security-review-mcp enroll accepts --cafile directly (see below).

Client configuration

VS Code, with a token asked to the user (.vscode/mcp.json, see Host configuration):

{
  "servers": {
    "security-review": {
      "type": "http",
      "url": "https://secreview.example.internal:8443/mcp",
      "headers": { "Authorization": "Bearer ${input:secreview-token}" }
    }
  },
  "inputs": [
    { "id": "secreview-token", "type": "promptString", "description": "Token", "password": true }
  ]
}

For Claude and to get a token from the host, see Host configuration.

Behind a reverse proxy

A reverse proxy (TLS, OIDC, rate limiting) remains a valid option: in that case, leave these variables empty.

Warning

If the server listens elsewhere than on localhost without authentication or without TLS, a warning is logged. Any token with the mcp or admin role gives access to every tool.