Skip to content

· 16 min read · Governance

Governed Growth, Part 3: Default-Deny for Model Capabilities

Governed Growth, Part 3: Default-Deny for Model Capabilities

Part 2 ended on a tension I left open on purpose: you can read every clause and draw the boundary exactly where you want it, and a good-faith developer on a team you’ve never met can still trip the capability with one line in a tools list. Reading the terms tells you which posture you’re in. It does nothing, by itself, to keep that developer out of the wrong one.

This post closes that gap with the exact toggles that turn “we decided” into “the platform enforces what we decided, at the org boundary, defaulted to no,” at the right scope, in the two forms you’d actually ship them: gcloud for the one-off and the audit, and Terraform for the version-controlled cloud estate.

The One Thing to Get Right: There Is No Single Off Switch

Before the commands, the mistake they exist to prevent. There’s no master toggle that turns off “every server-side tool.” The control surface is a small set of distinct mechanisms, each governing a different capability, at a different layer, with its own automation. Reading them as one switch is how estates end up with a policy that looks complete and isn’t.

Here’s the whole surface on one screen. Read the Type column first: it groups the five controls into four kinds, so you can see at a glance that the top two are the same kind of thing—Organization Policy constraints you set once and inherit everywhere—and the rest are the seams org-policy doesn’t reach, named here so the inventory is honest.

  • Org policy—a list-rule constraint set at org, folder, or project and inherited down.
  • Network—a VPC Service Controls perimeter, a second gate at the network layer.
  • Per-model—a per-model, per-project toggle set through the API or SDK, not gcloud.
  • Grounding—a provider choice made in the request, not a policy at all.
Capability governedTypeHow / whereDefault
Which model actions run (predict, deploy, tune)Org policyvertexai.allowedModels · org / folder / project · gcloud or TerraformAll models and actions allowed if unset
Partner features (web_search, structured_outputs)Org policyvertexai.allowedPartnerModelFeatures · org / folder / project · gcloud or TerraformDenied by default for org-owned projects
Claude web search at the network boundaryNetworkA perimeter over aiplatform.googleapis.com · org access policy · gcloud or TerraformNo perimeter, no block
Request-response logging of prompts and completionsPer-modelRequestResponseLoggingConfig to BigQuery · project + model · REST or Python SDKOff by default
The search data path (retention)GroundingChoose the grounding provider · project, per config · grounding API requestGrounding with Google Search stores ~3-day debug logs

Two of these share a Type for a reason—both org-policy list-rule constraints, one automation surface—and they carry most of the leverage. Start there.

The Default You Should Verify First

For any project that belongs to an organization, the partner features governed by vertexai.allowedPartnerModelFeatures—Claude’s web_search and structured_outputs—are denied by default. Web search for a partner model is off until a policy explicitly turns it on. That’s least privilege applied to a model’s server-side capabilities: the capability arrives switched off, and a human has to make a deliberate decision to switch it on. web_search is the capability worth the worked examples—it reaches past the trust boundary in a way you can measure—but structured_outputs rides the same constraint and inherits the same default, so every allow-list decision here governs both, whether or not you were thinking about the quieter one.

So the first command you run isn’t a set. It’s a describe, to confirm the default is actually in force and nobody quietly loosened it. The --effective flag is the one that matters—it shows the policy as evaluated at this resource, with inheritance from parents merged in, which is what actually applies:

Verify the effective partner-feature policy
# What is DIRECTLY set on this project (NOT_FOUND if nothing is set here):
gcloud org-policies describe \
vertexai.allowedPartnerModelFeatures --project=PROJECT_ID
# What ACTUALLY applies here, with org/folder inheritance merged in:
gcloud org-policies describe \
vertexai.allowedPartnerModelFeatures --effective --project=PROJECT_ID

Swap --project=PROJECT_ID for --folder=FOLDER_ID or --organization=ORGANIZATION_ID to read any tier. Run the same two commands with vertexai.allowedModels to check which model actions are permitted. If the effective policy at a production project shows partner web search allowed and you didn’t put it there, you’ve found the gap Part 1 told you to look for—a data path that arrived instead of one you chose.

Default-deny isn’t only an org-policy story; the same posture pays off a layer down, at MCP tool auth. Ship a connector reachable by every caller by default and you’ve built the tool-layer version of the data path nobody chose; start it locked and grant access one deliberate caller at a time, and the tool boundary becomes a decision on record instead of an accident. Same discipline, different control surface.

Turning It On, On Purpose: The Allow

Default-deny is only useful if the deliberate yes is just as easy to record. When a team makes the case for Claude web search and compliance signs off, you encode that decision as policy. set-policy overwrites the constraint on the named resource with the contents of a YAML file—which is exactly what you want, because the file is source-controllable and reviewable. Here’s enabling web_search for all Anthropic models at the organization tier:

allow-anthropic-web-search.yaml
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedPartnerModelFeatures
spec:
rules:
- values:
allowedValues:
- publishers/anthropic # all features, all Anthropic models
# Narrow it instead, if the decision was narrower:
# - publishers/anthropic/models/MODEL_NAME # all features of one model
# - publishers/anthropic/models/MODEL_NAME:web_search # just web_search on one model
Apply the decision
gcloud org-policies set-policy allow-anthropic-web-search.yaml

The value grammar is worth reading closely, because it’s where you tune the blast radius of a yes. publishers/anthropic grants every partner feature on every Anthropic model. Adding /models/MODEL_NAME scopes it to one model; appending :web_search scopes it to one feature on one model. A decision that was really “let the fraud team try Claude web search on one model” should be written as one line, not as a blanket grant nobody meant to make. Anthropic is the example here because Claude is the partner model most estates reach for first, but the publishers/PUBLISHER grammar is the same for any partner catalog you enable—swap the publisher and the same rules apply.

The quieter feature rides the exact same grammar. structured_outputs is denied by the same default and enabled by the same allow-list; you just change the suffix. To let one model return structured JSON while leaving its web search denied, the feature suffix is :structured_outputs instead of :web_search:

allow-structured-outputs.yaml
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedPartnerModelFeatures
spec:
rules:
- values:
allowedValues:
# structured outputs on ONE model; web_search stays denied by default
- publishers/anthropic/models/claude-opus-4-5:structured_outputs

Applied with the same gcloud org-policies set-policy command, that grant lets that model emit schema-constrained output and nothing else—the data-path capability (web_search) is still off, because you never listed it. Two features, one constraint, one file grammar: every allow-list decision here is really a decision about both features, and writing the suffix out is how you make sure the one you didn’t mention stays denied on purpose rather than by neglect.

The companion constraint, vertexai.allowedModels, governs which model actions are allowed at all—calling the managed API (predict), deploying an endpoint (deploy), or tuning (tune). It’s the same kind of artifact as the partner-features file above: an Organization Policy definition, a YAML file that names a list-rule constraint and the resource it binds to. You write it, then apply it with the same set-policy command. Here it takes the deny form, with deniedValues to block a set and allow the rest:

deny-models.yaml
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedModels
spec:
rules:
- values:
deniedValues:
- publishers/meta/models/llama3:deploy
- publishers/google/models/gemini-3-flash:tune

Flip deniedValues to allowedValues and you get allow-list semantics: permit exactly the listed models, implicitly deny the rest. Which way you reach for depends on your posture—deny-list to block a few known exceptions, allow-list when the estate should run only a vetted catalog.

The allow-list is also where the estate quietly records which tier each model runs on, because the action suffix says it: :predict is the managed API you gate but don’t run, :deploy is an endpoint you stand up and own. Written out, one model per tier, the policy file becomes the estate’s memory of every deployment decision. The model IDs below are illustrative and current as of this writing—the catalog moves; the tier structure and the suffix grammar don’t:

allow-models.yaml — the allow-list records the tier
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedModels
spec:
rules:
- values:
allowedValues:
- publishers/google/models/gemini-3-pro:predict # first-party, managed API
- publishers/anthropic/models/claude-opus-4-5:predict # partner, managed API only
- publishers/google/models/gemma-3-27b:deploy # open model, self-deployed into your VPC

Both files apply exactly like the partner-features one—set-policy overwrites the constraint on the named resource with the file’s contents:

Apply either model policy
gcloud org-policies set-policy deny-models.yaml
# ...or the allow-list form:
gcloud org-policies set-policy allow-models.yaml

For the version-controlled form of the same decision—a merged pull request with a name on it instead of a one-off file—see the google_org_policy_policy Terraform resource under “Ship It as Code” below; it takes the same constraint name and the same value grammar.

Two honest limits to keep in your head, because they constrain how you write these: the constraint applies only to Model Garden models, not Model-Registry ones; and a single policy can carry at most 500 allowed-plus-denied values. You list each model individually—there’s no way to allow or deny a group in one entry—so an allow-list estate is a real inventory to maintain, not a wildcard.

Undoing It

Two verbs, two different meanings, and the difference bites if you confuse them:

Reset vs. delete
# Restore the constraint's DEFAULT behavior on this resource:
gcloud org-policies reset vertexai.allowedModels --project=PROJECT_ID
# Remove the policy entirely, so the resource inherits its PARENT's policy:
gcloud org-policies delete vertexai.allowedModels --project=PROJECT_ID

reset on allowedModels returns to “allow all.” delete on allowedPartnerModelFeatures returns to the constraint default—which, for an org-owned project, is deny-by-default. Deleting a partner-feature policy isn’t “turn it off”; it’s “fall back to the safe default,” and that’s usually what you want.

Ship It as Code, Not as a Console Click

Everything above is gcloud, which is right for the audit and the one-off. But an estate’s policy should live in version control, reviewed like any other infrastructure change, so the answer to “who allowed partner web search in prod?” is a merged pull request with a name on it. The Terraform resource is google_org_policy_policy, and it encodes both the tier and the constraint in its name:

org-policy.tf
# Enable web_search for all Anthropic partner models, at the ORGANIZATION tier.
resource "google_org_policy_policy" "allow_anthropic_partner_features" {
name = "organizations/123456789/policies/vertexai.allowedPartnerModelFeatures"
parent = "organizations/123456789"
spec {
rules {
values {
allowed_values = ["publishers/anthropic"]
}
}
}
}
# Restrict callable models at the PROJECT tier (deny a set, allow the rest).
resource "google_org_policy_policy" "restrict_models" {
name = "projects/my-project/policies/vertexai.allowedModels"
parent = "projects/my-project"
spec {
rules {
values {
denied_values = ["publishers/meta/models/llama3:deploy"]
}
}
}
}

The tier lives entirely in the name and parent prefix—swap organizations/… for folders/FOLDER_ID or projects/PROJECT_ID and the same module templates a policy across every tier. Three spec features earn their keep in a real rollout: inherit_from_parent controls whether a child merges with or overrides its parent (more on that next), reset restores the default, and dry_run_spec audits how a policy would apply without enforcing it—the infrastructure-as-code equivalent of a rollout guardrail. Reach for dry_run_spec before you enforce anything estate-wide; it turns a scary change into an observable one.

Scope: Govern High, and Deny Wins

The reason all of this is set once rather than chased per project is the scope model, and it has two rules you have to hold together. Both org-policy constraints—allowedModels and allowedPartnerModelFeatures—can be set at the organization, folder, or project tier, and all the policies that contribute to a resource are merged, then evaluated.

The first rule is deny-wins. Any explicit deny beats any explicit allow. A folder-level deny overrides a project-level allow—unless the project policy is set to override its parent entirely. So a stray project that tries to loosen prod can’t, as long as the restrictive policy lives above it and nobody granted the project override.

The second rule is inherit versus override. A child with inheritFromParent: true (inherit_from_parent = true in Terraform) merges with the parent’s rules; without it, the child’s rules stand alone and replace the parent’s. This is how a dev project can add to an org allow-list without breaking away from it, or how a sandbox can deliberately break away. Who’s allowed to set that override is itself an IAM decision—you can forbid the break-away by controlling who holds org-policy admin—so the two rules compose into a real guarantee rather than a hope.

graph TD
  org["Organization policy<br/>deny partner web_search"]
  prodFolder["Prod folder<br/>inherits: deny stands"]
  devFolder["Dev folder<br/>allow web_search for experimentation"]
  prodProj["Prod project<br/>tries to allow: DENIED, deny-wins"]
  devProj["Dev project<br/>inherits the dev allow"]

  subgraph estate["Set the decision high, inherit it everywhere"]
    org --> prodFolder
    org --> devFolder
    prodFolder --> prodProj
    devFolder --> devProj
  end

Read the diagram top-down and the operating pattern falls out. Put prod projects under a folder with a restrictive partner-feature policy and dev projects under a folder that allows experimentation. Set the estate-wide decision at the organization node so every current and future project inherits it, and let deny-wins guarantee that a stray project-level allow can’t loosen prod.

This is “govern outcomes, not steps” made literal: the outcome—no partner web search in prod—is set once as infrastructure, not enforced by asking developers to remember.

The same command reads the effective policy at any tier, which is how you prove inheritance is doing what you think:

Verify the effective policy at each tier
gcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --organization=ORGANIZATION_ID
gcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --folder=FOLDER_ID
gcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --project=PROJECT_ID

One operational reality to plan around: org-policy changes take up to 15 minutes to fully enforce. Don’t set a policy and immediately conclude from a still-working request that it failed. Give it the window.

The Seams Org-Policy Doesn’t Reach

Two org-policy constraints don’t cover the whole control surface. Three more capabilities matter, and none of them is an org-policy toggle. Naming them is the difference between a governance plan and a false sense of one.

Claude web search at the network boundary. The partner-feature constraint governs whether the web_search feature is allowed; a VPC Service Controls perimeter is a second, network-layer gate. Google’s own documentation is blunt about it: if VPC-SC is configured, requests that use the web_search_tool are blocked. The mechanism is a perimeter restricting aiplatform.googleapis.com, and because server-side search needs public egress, the perimeter’s egress restriction is what stops it. Create it dry-run first—it logs what would be blocked without blocking—so you can confirm you’re only catching search egress before you enforce:

Create a perimeter, dry-run first
gcloud access-context-manager perimeters dry-run create prod-ai-perimeter \
--perimeter-title="Prod AI perimeter" \
--perimeter-type=regular \
--perimeter-resources=projects/PROJECT_NUMBER \
--perimeter-restricted-services=aiplatform.googleapis.com \
--policy=POLICY_NAME

Perimeter changes take up to 30 minutes to propagate—twice the org-policy window—and during that time requests may fail with Error 403: Request is prohibited by organization's policy. The Terraform resource for the same job is google_access_context_manager_service_perimeter, with status.restricted_services = ["aiplatform.googleapis.com"].

Request-response logging. Logging prompts and completions to a BigQuery table is a separate per-model, per-project toggle, off by default. Its automation surface is the REST API or Python SDK—there is no gcloud subcommand. Here it is on a Gemini base model, through the Python SDK:

Enable logging on a Gemini base model
import vertexai
from vertexai.preview.generative_models import GenerativeModel
vertexai.init(project="PROJECT_ID", location="LOCATION")
publisher_model = GenerativeModel("gemini-3-flash")
publisher_model.set_request_response_logging_config(
enabled=True,
sampling_rate=1.0,
bigquery_destination="bq://PROJECT_ID.DATASET_NAME.TABLE_NAME",
enable_otel_logging=True,
)

Call the same method with enabled=False to turn it off, and read the current config back with fetchPublisherModelConfig so you can confirm whether logging is on and where it lands. The toggle covers Anthropic Claude too, with one wrinkle: for Anthropic models only the REST API is supported, and only against a regional endpoint. This is the toggle that turns “we approved Claude with web search” into “we approved Claude with web search and request logging to our own BigQuery”—a third, independently-recorded trust decision, and the one that gives your auditors something to read.

The search data path. The lever here is not a policy at all; it’s provider selection. As Part 2 covered in full—the retention terms that just shifted under everyone—the default Gemini search path keeps short-lived debug logs (queries derived from your prompts, retained up to three days for reliability, with no opt-out)—not your prompts and output. When you need zero retention on the Gemini search path, the operational move is to choose a different provider: Web Grounding for Enterprise, set in the grounding API request rather than an org policy. It belongs in this inventory because it’s a deliberate capability choice an architect makes for the data path—the same kind of decision as the org-policy toggles, made at a different layer.

The Whole Runbook, in Order

Here’s the sequence you’d actually run—the afternoon that turns Parts 1 and 2 into enforced policy.

  1. Audit the effective policy. For each production project (or the folder above it), run gcloud org-policies describe … --effective for both vertexai.allowedPartnerModelFeatures and vertexai.allowedModels. Confirm the default-deny on partner features is actually in force and nothing above you loosened it.
  2. Set the decisions you’ve actually made, at the tier they belong. Encode every deliberate yes as a set-policy YAML or a google_org_policy_policy resource, scoped as narrowly as the decision was, at the organization or folder tier so it inherits. Use dry_run_spec before enforcing anything estate-wide.
  3. Add the network gate where the stakes justify it. For the environments that must block Claude web search at the boundary regardless of the feature policy, stand up a VPC-SC perimeter over aiplatform.googleapis.com—dry-run first, then enforce, and budget the 30-minute window.
  4. Turn on logging where you need the audit trail. Configure RequestResponseLoggingConfig to a BigQuery table for the models whose prompts and completions you’re required to retain and inspect.
  5. Pick the grounding provider deliberately for any team with a zero-retention obligation, rather than inheriting the short-lived debug-log store by default.
  6. Put it all in version control. The gcloud commands are how you audit and patch; the Terraform is how the estate’s answer to “who allowed this?” becomes a reviewable, named, dated change instead of a memory.

Do that, and the tension Part 2 left open is closed—not by asking developers to be careful, but by making the safe answer the default and the deliberate answer a decision on record.

What Part 4 Does With This

Part 3 was the mechanism. Part 4, Multi-Model, Governed, and Boring is the end state these toggles buy you: the same default-deny discipline applied across every model you add, so the estate stops depending on anyone staying vigilant. Nothing trips in the night, because the switches all start in the safe position and every exception has a name attached.


This is Part 3 of the Governed Growth series. Every constraint name, command flag, and Terraform resource above is drawn from Google Cloud’s own documentation and the HashiCorp provider reference; the org-policy behavior, default-deny, and deny-wins precedence are from Google Cloud’s Organization Policy and Gemini Enterprise Agent Platform docs.