Kong AI Gateway 2.x is configured with declarative YAML, applied to Konnect (Kong’s SaaS control plane) with kongctl. That makes the configuration reviewable like code, and lintable like code.
Take a pull request that changes a Kong AI Gateway MCP (Model Context Protocol) server definition. It looks fine in review: a URL, an auth strategy, a log setting. What nobody sees is what is missing. No tool access list, so every tool the upstream exposes is callable by every authenticated consumer.
That kind of mistake is cheap to catch before deploy and expensive afterwards. This post shows how to lint Kong AI Gateway 2.x declarative files with kongctl lint and one reusable ruleset, with MCP servers as the main example. You will end up with a pipeline step that fails the pull request, no Konnect access needed.
Run on every change, it becomes one repeatable governance check on the way to go-live, alongside the reviews and tests you already have.
Versions used: Kong AI Gateway 2.x on Konnect · kongctl 1.18 or later
Why this matters
AI Gateway configuration holds decisions that cost money or leak data, and most of them are plain fields in a file:
- MCP tool exposure. If an MCP server defines no ACL rules, requests are allowed. The AI MCP Server docs list “no ACL rules configured” as proxied with HTTP 200. Unlisted tools are not hidden, they are open.
- Payload logging. Models, agents and MCP servers can log request and response bodies. Prompts and tool arguments can carry personal data.
- Literal secrets. Provider credentials are write-only fields. The kongctl docs describe
!secretfor them. A literal in a file ends up in git. - Client-supplied credentials. A target with
allow_auth_override: truelets request-level credentials take precedence over the gateway’s (passthrough docs). - Unpriced, uncapped targets.
input_costandoutput_costare prices per one million tokens. Leave them out and the gateway has no price to compute cost from, so any budget or chargeback built on it has nothing to work with.
Code review misses these because they are absences or one-word flips. A linter does not get tired.
How it works
kongctl lint checks declarative files against a ruleset. It is powered by vacuum and uses the Spectral-compatible format, the same as deck file lint (docs). A rule has three parts:
given: a JSONPath selector that picks objects, for example$.ai_gateway_mcp_servers[*].then: a built-in function applied to a field, for exampletruthy.severity:error,warn,infoorhint. Findings at or above--fail-severity(defaulterror) give a non-zero exit code.
Here is the rule from the ruleset that closes the open-tools gap (shortened; the shipped rule also matches servers nested under ai_gateways):
# ai-gateway/ruleset.yaml
aigw-mcp-default-deny:
message: "MCP server has no access.default_tool_acls (unlisted tools stay open)."
severity: error
given: ["$.ai_gateway_mcp_servers[?(@.type!='upstream-server')]"]
then: {field: access.default_tool_acls, function: truthy}
Lint never contacts Konnect. It reads each file as written, so it needs no credentials and runs in seconds.
What the ruleset covers
| Area | Examples |
|---|---|
| MCP servers | authentication required, tools denied by default, no tool arguments in logs, audit logging, upstream TLS, ACL attribute and claim settings, allowed protocol versions |
| Authentication | auth strategy on models and agents, OIDC issuer, audience and TLS verification |
| Secrets | no literal provider, Redis or cloud secrets; credential expiry |
| Data protection | prompts and responses not logged, guardrails fail closed |
| Cost | max_tokens and prices on every target, token-based rate limits |
| Resilience | bounded retries, circuit breaker, cache never blocks inference |
| Ownership | namespace on every file, owner and status labels |
There are 80 rules in total. Read the descriptions in the file; each one says why it exists.
Prerequisites
- kongctl 1.18 or later (get started)
gitandmake- Declarative AI Gateway files, or the examples in the repository
Step 1: See the findings on a deliberately bad file
git clone https://github.com/bhatikuldeep/kongctl-lint-governance.git
cd kongctl-lint-governance
make demo
The wrong.yaml example breaks many rules at once. These are the MCP and credential findings from it:
ai-gateway/examples/wrong.yaml:45:1: [error] MCP server has no access.default_tool_acls (unlisted tools stay open).
ai-gateway/examples/wrong.yaml:45:1: [error] MCP server has no access.auth_strategies.
ai-gateway/examples/wrong.yaml:45:1: [warn] MCP server does not enable config.logging.audits.
ai-gateway/examples/wrong.yaml:48:53: [error] MCP payload logging is enabled.
ai-gateway/examples/wrong.yaml:13:53: [error] Target allows client-supplied credentials (allow_auth_override: true).
ai-gateway/examples/wrong.yaml:24:73: [error] Provider auth header value is a literal: use a {vault://...} reference or !secret.
Each finding gives file, line, column, severity and a plain message. The full run reports 95 findings, 27 of them errors.
Step 2: See what a compliant MCP server looks like
This is the MCP server from ai-gateway/examples/clean.yaml:
ai_gateway_mcp_servers:
- ref: tools
name: tools
type: listener
access:
auth_strategies: [key-auth-strategy]
acl_attribute_type: consumer
default_tool_acls: {allow: [tool-admins]}
config:
url: "https://mcp.example.invalid"
logging: {audits: true, payloads: false}
Authentication is required, tools are denied unless the caller matches the tool-admins entry (an AI Consumer or Consumer Group), audit events are on, and payloads are off. Per the ACL docs, a tool’s own ACL fully replaces the default for that tool, so anything you open up is a deliberate, reviewable exception.
Lint it with warnings treated as failures:
kongctl lint -f ai-gateway/examples/clean.yaml -r ai-gateway/ruleset.yaml -F warn; echo $?
ai-gateway/examples/clean.yaml:11:1: [hint] Model has no access.acls.
ai-gateway/examples/clean.yaml:39:7: [hint] MCP listener does not set config.allowed_versions.
0
Hints are prompts for a decision and do not fail the run. These two come from the model and from allowed_versions, which the example leaves for you to decide.
Step 3: Run it on your own configuration
Copy ai-gateway/ruleset.yaml into your repository and run:
kongctl lint -f path/to/config -R -r ai-gateway/ruleset.yaml # fail on errors
kongctl lint -f path/to/config -R -r ai-gateway/ruleset.yaml -F warn # fail on warnings too
-R walks a directory. Add -o json for machine-readable output with rule_id, severity, file and line per finding.
Roll out in two stages. Enforce errors first and fix them. Review the warnings with their owners, then add -F warn when the team is ready.
Step 4: Put it in the pull request
# .github/workflows/lint.yaml
name: lint
on: pull_request
jobs:
kongctl-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: curl -fsSL https://get.konghq.com/kongctl | sh # Linux runners, per the kongctl install docs
- run: kongctl lint -f config/ -R -r ai-gateway/ruleset.yaml
Because lint needs no Konnect credentials, the job needs no secrets.
Step 5: Change a rule without editing the shared file
Extend the ruleset and override only what you need. The extends path resolves from the directory you run kongctl in, so run it from the repository root, where it matches the copy in Step 3:
# my-ruleset.yaml
extends:
- ai-gateway/ruleset.yaml
rules:
aigw-model-owner-label: off # or: warn / error
A wrong extends path does not fail the run. kongctl logs cannot open external ruleset, applies no rules and exits with code 0, so a typo can turn the whole gate off. Keep a known-bad file in CI and assert that lint fails on it.
Keep the base file unmodified so you can pull updates. Every rule has a stable aigw- id, so a review comment or a rule override can cite it.
kongctl lint has no inline ignore option, so an exception for one resource is not possible. The way to grant one is a separate ruleset that sets the rule to warn or off, used only for the folder that needs it (for example a sandbox directory). Record who approved it and when it expires.
Where it fits before go-live
Lint is one check on the way to production, not the whole checklist. A workable split:
- Errors must be fixed before go-live. They cover open MCP tools, literal secrets, logged payloads and missing authentication.
- Warnings need an owner and an expiry. Cost caps and retry settings are often a deliberate trade-off at launch. Record the decision instead of ignoring it.
- Hints are for the design review. Treat them as questions, not defects.
Run the same ruleset on every pull request after go-live, so the standard you launched with is the one you keep.
In production
- Security. Pair lint with
kongctl plan --require-namespace=<ns>, which restricts planning to the namespaces you name (docs). Lint tells the developer early; the system account’s permissions are what actually limit the pipeline. - Scale. Rules run per file. In a large repository, pass only the changed paths to
-f(the flag can repeat) and run the full directory on a schedule. - Observability. Use
-o jsonto count findings perrule_idover time. A rule that fires on most pull requests is either too strict for your standards or pointing at a habit worth fixing at the source. - Failure modes. A rule that is too noisy gets ignored. Start with errors only. A waiver without an owner and an expiry becomes permanent.
- Upgrades. Pin the ruleset to a release tag (the repository publishes
v1.0.0) and the kongctl version in CI. New AI Gateway releases change fields; review rule changes in a pull request like any dependency.
When not to use this
- You need cross-file or cross-resource checks, such as “this policy is attached to a model”. Lint sees one file at a time and does not follow
!refacross files. - You use
_templates,!fileor!envheavily. They are not expanded before linting, so a value supplied that way is invisible to the rules. Keep governed fields written out in the files you lint. - You need custom logic.
kongctl lintruns vacuum’s built-in functions only. The lint code builds its rule execution without any custom function hook (internal/lint/lint.go), so a rule that names one fails with an unknown-function error. Most checks can be written withtruthy,pattern,definedandschema. - You want enforcement. Anyone who can call kongctl directly can skip the pull request. Lint is feedback, not a gate.
Troubleshooting
- “Unknown function” error. The rule uses a custom function (for example
Unknown function 'myCustomFn' in rule 'r1'). Rewrite it with built-in functions. - Lint passes but reports nothing. Look for
cannot open external rulesetin the output. A wrongextendspath is logged and skipped, with exit code 0. Run from the directory the paths assume. - A rule never fires. Check the
givenpath against your file’s actual keys. Resources can be root keys (ai_gateway_models) or nested (ai_gateways[].models); the shipped rules match both. - Findings on templates, none on the file that uses them. Rules skip targets that use
_extends. Lint the templates file too. - Exit code 0 with warnings printed. Warnings fail only with
-F warn.
Bonus: team onboarding
If several teams share one Konnect organisation, the same mechanism answers a second question: whose configuration is this, and what may they define? The repository’s team-governance/ folder checks each team’s onboarding file, requires the owning team’s labels on its resources, and keeps it inside its namespace and approved resource types.
The two layers fit together because each team ruleset extends the AI Gateway ruleset. One command covers both:
kongctl lint -f . -R -r team-governance/teams/<team>/ruleset.yaml -F warn
A sound AI Gateway file with another team’s namespace passes the AI Gateway rules and fails the team rules. One rule to remember: choose the ruleset from the pipeline, never from the namespace written in the file, because a file can copy another team’s namespace and labels and pass that team’s rules.
Takeaways
- Many costly AI Gateway mistakes are visible in the YAML: absent ACLs, logged payloads, literal secrets, uncapped targets.
kongctl lintplus one ruleset catches them in the pull request with no Konnect access.- Start with errors, add
-F warnlater, and override rules by id in your own ruleset. - Lint is early feedback. Namespace flags and system-account permissions do the enforcing.
- Built-in functions cover most needs. Plan around the cases they do not: cross-file checks and unexpanded templates.
References
- kongctl lint
- Declarative configuration with kongctl (namespaces,
!secret) - AI MCP Servers (ACL tool control)
- Model cost management
- Passthrough format
- vacuum rules
- Ruleset and examples
- Related post: Your CI is green, Kong says no: lint route paths before you deploy