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 !secret for them. A literal in a file ends up in git.
  • Client-supplied credentials. A target with allow_auth_override: true lets request-level credentials take precedence over the gateway’s (passthrough docs).
  • Unpriced, uncapped targets. input_cost and output_cost are 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 example truthy.
  • severity: error, warn, info or hint. Findings at or above --fail-severity (default error) 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

AreaExamples
MCP serversauthentication required, tools denied by default, no tool arguments in logs, audit logging, upstream TLS, ACL attribute and claim settings, allowed protocol versions
Authenticationauth strategy on models and agents, OIDC issuer, audience and TLS verification
Secretsno literal provider, Redis or cloud secrets; credential expiry
Data protectionprompts and responses not logged, guardrails fail closed
Costmax_tokens and prices on every target, token-based rate limits
Resiliencebounded retries, circuit breaker, cache never blocks inference
Ownershipnamespace 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)
  • git and make
  • 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 json to count findings per rule_id over 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 !ref across files.
  • You use _templates, !file or !env heavily. 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 lint runs 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 with truthy, pattern, defined and schema.
  • 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 ruleset in the output. A wrong extends path is logged and skipped, with exit code 0. Run from the directory the paths assume.
  • A rule never fires. Check the given path 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 lint plus one ruleset catches them in the pull request with no Konnect access.
  • Start with errors, add -F warn later, 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