CJIS compliance at the API gateway: governing access to criminal justice data
A CJIS compliance API gateway has to prove who touched criminal justice data, from where, with what auth. Map CJIS v6.0 controls to gateway config you own.
- cjis
- government
- compliance
- api-gateway
- audit
- ai-agents
State and county agencies are exposing more criminal justice information (CJI) over APIs than ever. A records management system answers warrant lookups for municipal police departments. A state repository feeds the court case system. A dispatch vendor pulls incident data, and now an analyst wants an AI assistant to summarise case histories. Each of those connections is in scope for the FBI CJIS Security Policy, and when the CJIS Systems Agency auditor arrives, the question is no longer "is the database locked down?" It is "show me every system that reached CJI through your APIs, how it authenticated, and what it did." A CJIS compliance API gateway is the one place where that question has a single answer.
The pressure has a specific shape. CJIS Security Policy v6.0 restructured the policy around NIST SP 800-53 control families, so auditors now ask about access control (AC), identification and authentication (IA), audit and accountability (AU) and system and communications protection (SC) in the same language a federal assessor would. Multi-factor authentication for CJI access is no longer something to plan for; it is an audit finding when it is missing. And the evidence has to come from the agency's own systems, not a vendor's support portal.
Most agencies we talk to have the controls in pieces. MFA lives in the identity provider, firewall rules sit on a network team's spreadsheet, logs sit in three different backends, and nobody can produce a single record showing that a given request from a given agency was authenticated, allowed and logged. This post maps the CJIS control families to concrete gateway configuration you can run inside your own boundary.
Why existing approaches leave gaps in CJIS audits
The common pattern is point-to-point integration: each consuming agency gets a VPN tunnel, a service account on the backend, and a firewall rule. That works until the auditor asks for a list of every account with access to CJI and when each was last reviewed. Service accounts on five backends, each with its own credential store and log format, cannot answer that without a week of manual reconciliation. Revoking one partner means touching every system it was granted.
Managed cloud gateways close part of the gap but introduce a different one. Apigee, Azure API Management and AWS API Gateway can be placed in government regions, but the control plane, the analytics pipeline and the people who operate them remain the vendor's. For CJIS, that pulls the vendor into your scope: their personnel, their logging retention and their incident process now have to be covered by the CJIS Security Addendum and your agency's agreements. Kong and Tyk can be self-hosted, but the audit record of who changed a route or a credential is often a separate enterprise add-on or a plugin you have to assemble and keep consistent across nodes.
AI tooling makes the gap wider. An analyst's AI assistant typically connects through its own MCP server with its own long-lived credentials and its own logs, completely outside the gateway that the CJIS audit already covers. That is a second, ungoverned path to CJI, and it is the one the auditor will find last.
How a CJIS compliance API gateway maps to the policy
Zerq runs as a single Go binary with a Next.js management UI and developer portal, backed by a MongoDB instance you operate. Configuration, request logs and the administrative audit trail all live in that database. No traffic, config or log data leaves your environment during normal operation, and the platform runs fully offline in an air-gapped enclave if your CJIS boundary requires it. The architecture page shows the full component layout.
The request path, end to end
Here is the path a CJI request takes, described so you can draw it on the whiteboard for your auditor:
- A consuming system (a municipal RMS, a court case system, or an AI agent) obtains an access token from the agency identity provider. The IdP enforces MFA for human users and issues client credentials for machine clients.
- The request hits your ingress or load balancer inside the CJIS boundary. TLS terminates here using your agency's validated cryptographic modules. For mTLS partners, the ingress verifies the client certificate.
- The Zerq gateway reads
X-Client-IDandX-Profile-ID, checks that the client and profile are active and belong together, validates the OIDC token against the configured issuer and audience, then applies the profile's IP allowlist and HTTP method restrictions. - The gateway checks that the client has access to the collection containing the requested endpoint, applies its rate limit and quota policy, and forwards the request to the backend using a credential the consumer never sees.
- The request log middleware records the full request and response, including denials, to the agency's MongoDB.
Every step runs on infrastructure the agency controls. The only external dependency is the one you already have: your own IdP.
Identification and authentication (IA)
The CJIS requirement for advanced authentication is met at the identity provider, and the gateway's job is to refuse anything that did not come through it. Zerq validates tokens against your IdP for both management sign-in and API traffic. The relevant backend configuration looks like this:
# Management UI and Management MCP sign-in (agency staff)
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://idp.agency.example/realms/cjis # agency IdP; MFA enforced here
OIDC_AUDIENCE=api-gateway-management # audience for admin tokens
OIDC_ROLE_CLAIM_PATH=realm_access.roles # where roles live in the token
# Gateway traffic (consuming agencies and agents)
GATEWAY_OIDC_ENABLED=true
GATEWAY_OIDC_ISSUER_URL=https://idp.agency.example/realms/cjis
GATEWAY_OIDC_AUDIENCE=api-gateway # tokens for other audiences are rejected
GATEWAY_OIDC_ROLE_CLAIM_PATH=realm_access.roles
GATEWAY_OIDC_CACHE_TTL=1h # JWKS / discovery cache lifetime
# Only allow strong profile auth types for this deployment
AUTH_METHODS_ENABLED=oidc,mtls
AUDIT_ENABLED=true # record every management change
AUTH_METHODS_ENABLED matters more than it looks. The profile service rejects any attempt to configure a profile with an auth type that is not in that list, so nobody can quietly issue a static API token to a partner in a deployment that only allows oidc and mtls. For high-assurance machine clients, an OIDC profile can also set requireCertificateBoundToken: true, which requires RFC 8705 certificate-bound tokens: a stolen bearer token is rejected unless the caller also holds the matching client certificate. That flag is set through the management API or the update_profile Management MCP tool. The security page covers the full set of supported auth methods.
Access control (AC): one client per consuming agency
Least privilege in Zerq is expressed as clients, collections and profiles. A client represents a consuming organisation or system. Collections group the APIs it may reach. A profile is the runtime access contract: auth type, allowed methods, allowed source IPs and active state. To onboard a municipal police department for read-only warrant lookups:
- In the management UI, create a collection named
warrants-readand add only the lookup endpoints, then publish it. - Open Clients, create a client for the department, and assign it the
warrants-readcollection and a rate limit policy (for example 300 requests per1mand a30dquota sized to their dispatch volume). - On the client's profiles page, click New Profile. Enter a profile name such as
rms-prod, select onlyGETunder Allowed HTTP Methods, and add the department's egress range under IP Address Restrictions. Click Create Profile. - Click Manage Authentication on the new profile and set the Authentication Type to OIDC, so the profile only accepts tokens from your agency IdP.
- Share the client ID and profile ID with the department. Their RMS sends both headers plus its bearer token on every call.
The result is enforced, not documented: a POST returns 405 HTTP method not allowed, a call from outside the allowlisted range returns 403 Access denied from current IP address, and a call to an endpoint outside warrants-read returns 403 Access to this collection is not allowed for this client. When the department's agreement lapses, you toggle the profile to inactive and every call fails with 401 Profile is inactive at once, without touching the backend.
Audit and accountability (AU)
CJIS audit requirements cover two different things, and Zerq keeps them in two different records. Request logs answer "who accessed CJI". The gateway's logging middleware runs before authentication, so denied requests are recorded alongside successful ones. A denied write attempt from the department above looks like this in the request log collection:
{
"id": "6718f3c2a91e4b0012ab77d1",
"method": "POST",
"path": "/warrants/v1/records",
"status": 405,
"latency": 3,
"created_at": "2026-10-26T14:02:11Z",
"client_ip": "10.44.8.21",
"client_id": "66f1c09e2b7d3a0019e4c210",
"profile_id": "66f1c0b72b7d3a0019e4c215",
"error": "HTTP method not allowed",
"request_id": "b3e1a7c4-5f20-4c8e-9d61-0a2f7e88c901",
"request_body": "{\"subject_id\":\"...\"}"
}
For an auditor, client_id and profile_id identify the consuming agency and the access contract it used, client_ip shows where the call came from, status and error show the enforcement decision, and request_id ties the entry to backend and SIEM records. Successful calls also carry target_url and the collection that matched.
Be deliberate about one consequence: request logs capture full headers and bodies, which means the log store holds bearer tokens and CJI payloads. Treat the MongoDB that holds them as a CJI system. Keep it inside the boundary, encrypt its storage, and restrict who can read logs through OIDC_VIEWER_ROLES. See observability for the dashboards and filters built on this data.
The second record is the administrative audit trail, which answers "who changed access to CJI". Every create, update and delete through the management API or Management MCP is recorded:
{
"timestamp": "2026-10-26T09:41:07Z",
"actor_id": "f2c9a1d0-7b3e-4e21-a8f4-3d5c6b7e8a90",
"actor_type": "user",
"resource_type": "profile",
"action": "UPDATE",
"ip_address": "10.20.1.15",
"user_agent": "Mozilla/5.0 ...",
"request_id": "9a7d2e10-3c4b-4f8a-b1e2-6c5d4e3f2a10",
"metadata": {
"url": "/api/v1/clients/66f1c09e2b7d3a0019e4c210/profiles/66f1c0b72b7d3a0019e4c215",
"request_body": "{\"allowedIPs\":[\"10.44.8.0/24\"],\"allowedMethods\":[\"GET\"]}",
"status_code": 200
}
}
actor_id is the subject from the admin's OIDC token, so it maps directly to a named person in your IdP. metadata.request_body shows exactly what was changed. This covers the account management events CJIS expects you to audit automatically: creating, modifying, disabling and removing access for a consuming system.
Separation of duties
The people who grant access should not be the people who review it. Zerq maps IdP roles to four permission levels through environment variables: OIDC_VIEWER_ROLES, OIDC_MODIFIER_ROLES, OIDC_ADMIN_ROLES and OIDC_AUDITOR_ROLES. The audit log API only answers to auditor roles, so you can give your CJIS technical audit team a cjis-auditor role that reads the full change history without any ability to modify a client, profile or credential. Deleting a request log entry requires the admin role, and that deletion itself lands in the audit trail.
Protecting backend credentials (SC)
Consuming agencies never receive backend credentials. Zerq holds them as encrypted credentials (AES-256-GCM, keyed by ENCRYPTION_KEY) and applies them on the outbound call. For agencies that keep secrets in Vault or a similar manager, every sensitive credential field supports Use Env Var, so the database stores only an environment variable name such as RMS_BACKEND_API_KEY, which your secrets manager injects into the gateway container at runtime.
AI agents and CJI
If an analyst's AI assistant will touch CJI, route it through Gateway MCP rather than a standalone MCP server. The agent gets its own client and profile, exactly like a consuming agency: its own collections, method restrictions and rate limits. It discovers tools with list_collections, list_endpoints and endpoint_details, and calls APIs with execute_endpoint, which passes through the same gateway checks and lands in the same request log, tagged with an X-Gateway-Source: mcp header. A read-only GET profile on a narrow collection keeps an agent from ever issuing a write, regardless of what the model is prompted to do.
A CJIS readiness checklist for your API layer
Run these questions against your current setup before the next audit:
- Can you list every system with API access to CJI from one screen, with its allowed endpoints and methods?
- Does every consuming client authenticate with a token from your MFA-enforcing IdP or a verified certificate, with no static shared secrets?
- Is
AUTH_METHODS_ENABLEDlimited to the methods your policy allows, and have you confirmed no profile on a CJI collection uses auth typenone? - Is every profile on a CJI collection restricted to known source ranges?
- Are denied requests logged with the same detail as successful ones?
- Does your administrative audit trail identify a named person for every access change?
- Can your audit team read the change history without being able to change anything?
- Do request logs, which contain CJI, sit inside your CJIS boundary with restricted read access?
- Would revoking one agency take one action, or a ticket to every backend team?
- Do AI agents reach CJI through the same gateway and logs as everything else?
What this looks like in practice
Consider a state agency that runs the central criminal history repository and serves forty local departments, the state court system and two vendors. Before, each consumer had a VPN tunnel and a service account on the repository's legacy SOAP layer. The last audit produced findings on shared service accounts, inconsistent MFA and the time it took to produce an access list. A request for an AI-assisted case summary tool was on hold because nobody could say how its access would be logged.
The agency deployed Zerq on its existing Kubernetes cluster inside the CJIS enclave, pointing at its own MongoDB and its existing IdP. Each consumer became a client with a dedicated read-only or read-write collection, an OIDC profile and an IP allowlist. Court integrations that required stronger assurance moved to OIDC profiles with certificate-bound tokens. The audit team received the auditor role, and the AI summary tool was onboarded through Gateway MCP with a GET-only profile on a single collection.
At the next audit, the access list was the clients page, the account management evidence was a filtered view of the audit log, and the answer to "what did the AI tool access" was a request log filter on its client ID. Revoking a vendor whose contract ended became a single profile toggle. The government and public sector use case describes the broader deployment pattern.
Summary
A CJIS compliance API gateway turns scattered controls into one enforceable record. Zerq validates every caller against your own MFA-enforcing identity provider. It limits each consuming agency to named endpoints, methods and source networks. It logs every request, allowed or denied, and every access change, attributed to a named person. It separates the people who grant access from the people who audit it. It keeps backend credentials away from consumers and AI agents alike. And it runs entirely inside your CJIS boundary, so the gateway vendor never sits in the path of your CJI. The capabilities overview lists everything included in a single deployment.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.