· 16 min read · Governance
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 governed | Type | How / where | Default |
|---|---|---|---|
Which model actions run (predict, deploy, tune) | Org policy | vertexai.allowedModels · org / folder / project · gcloud or Terraform | All models and actions allowed if unset |
Partner features (web_search, structured_outputs) | Org policy | vertexai.allowedPartnerModelFeatures · org / folder / project · gcloud or Terraform | Denied by default for org-owned projects |
| Claude web search at the network boundary | Network | A perimeter over aiplatform.googleapis.com · org access policy · gcloud or Terraform | No perimeter, no block |
| Request-response logging of prompts and completions | Per-model | RequestResponseLoggingConfig to BigQuery · project + model · REST or Python SDK | Off by default |
| The search data path (retention) | Grounding | Choose the grounding provider · project, per config · grounding API request | Grounding 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:
# 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_IDSwap --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:
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedPartnerModelFeaturesspec: 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 modelgcloud org-policies set-policy allow-anthropic-web-search.yamlThe 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:
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedPartnerModelFeaturesspec: rules: - values: allowedValues: # structured outputs on ONE model; web_search stays denied by default - publishers/anthropic/models/claude-opus-4-5:structured_outputsApplied 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:
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedModelsspec: rules: - values: deniedValues: - publishers/meta/models/llama3:deploy - publishers/google/models/gemini-3-flash:tuneFlip 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:
name: organizations/ORGANIZATION_ID/policies/vertexai.allowedModelsspec: 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 VPCBoth files apply exactly like the partner-features one—set-policy overwrites
the constraint on the named resource with the file’s contents:
gcloud org-policies set-policy deny-models.yaml# ...or the allow-list form:gcloud org-policies set-policy allow-models.yamlFor 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:
# 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_IDreset 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:
# 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:
gcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --organization=ORGANIZATION_IDgcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --folder=FOLDER_IDgcloud org-policies describe vertexai.allowedPartnerModelFeatures --effective --project=PROJECT_IDOne 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:
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_NAMEPerimeter 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:
import vertexaifrom 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.
- Audit the effective policy. For each production project (or the folder
above it), run
gcloud org-policies describe … --effectivefor bothvertexai.allowedPartnerModelFeaturesandvertexai.allowedModels. Confirm the default-deny on partner features is actually in force and nothing above you loosened it. - Set the decisions you’ve actually made, at the tier they belong. Encode
every deliberate yes as a
set-policyYAML or agoogle_org_policy_policyresource, scoped as narrowly as the decision was, at the organization or folder tier so it inherits. Usedry_run_specbefore enforcing anything estate-wide. - 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. - Turn on logging where you need the audit trail. Configure
RequestResponseLoggingConfigto a BigQuery table for the models whose prompts and completions you’re required to retain and inspect. - Pick the grounding provider deliberately for any team with a zero-retention obligation, rather than inheriting the short-lived debug-log store by default.
- Put it all in version control. The
gcloudcommands 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.