Skip to content

feat(provider/google)!: allow to use only zone-scoped permission on single zone - #6408

Open
knoblichd wants to merge 1 commit into
kubernetes-sigs:masterfrom
knoblichd:plt-4357-skip-list-when-single-zone-id
Open

knoblichd wants to merge 1 commit into
kubernetes-sigs:masterfrom
knoblichd:plt-4357-skip-list-when-single-zone-id

Conversation

@knoblichd

Copy link
Copy Markdown

When exactly one zone ID filter is configured, Zones() unconditionally calls dns.ManagedZones.List even though the zone is already known. This requires dns.managedZones.list — a project-level IAM permission that cannot be scoped to a single zone — exposing all zone names in the GCP project to any entity with that binding.

What does it do?

Adds a fast-path in Zones(): when zoneIDFilter contains exactly one ID, call dns.ManagedZones.Get directly instead of dns.ManagedZones.List. This eliminates the need for the dns.managedZones.list permission entirely, fully scoping the service account to a single zone.

Motivation

In multi-tenant GCP environments where multiple external-dns instances share a project, the list permission allows a compromised SA to enumerate all zone names project-wide, enabling cross-environment reconnaissance. With this change, each SA only needs dns.managedZones.get scoped to its own zone — it has no visibility into any other zone in the project.

More

  • Yes, this PR title follows Conventional Commits
  • Yes, I added unit tests
  • Yes, I updated end user documentation accordingly

@k8s-ci-robot
k8s-ci-robot requested review from szuecs and vflaux May 1, 2026 19:09
@k8s-ci-robot k8s-ci-robot added the provider Issues or PRs related to a provider label May 1, 2026
@linux-foundation-easycla

linux-foundation-easycla Bot commented May 1, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@k8s-ci-robot

Copy link
Copy Markdown
Contributor

Welcome @knoblichd!

It looks like this is your first PR to kubernetes-sigs/external-dns 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/external-dns has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@k8s-ci-robot k8s-ci-robot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label May 1, 2026
@k8s-ci-robot

Copy link
Copy Markdown
Contributor

Hi @knoblichd. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@k8s-ci-robot k8s-ci-robot added cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels May 1, 2026
@ivankatliarchuk

Copy link
Copy Markdown
Member

CLA no signed

@ivankatliarchuk

Copy link
Copy Markdown
Member

Not too shure about implementation. Why only for single id, but not for all ids, otherwise the solution is very specific for a single use case. Project may have multiple zones, while we only would like to provide access only to specific zones to external-dns, not specifically one

@knoblichd

Copy link
Copy Markdown
Author

Not too shure about implementation. Why only for single id, but not for all ids, otherwise the solution is very specific for a single use case. Project may have multiple zones, while we only would like to provide access only to specific zones to external-dns, not specifically one

Good point — updated to use Get for all configured zone IDs, not just when a single ID is set. When --zone-id-filter is configured with any number of IDs, we now iterate through them calling Get on each and aggregate the results.

When no zone ID filter is configured, we fall back to List as before — in that case the operator has intentionally chosen to let external-dns discover and manage all zones in the project, so the project-level permission is unavoidable and expected.

This way the permission footprint is proportional to what the operator configured: scoped deployments no longer require project-level list visibility, while unfiltered deployments continue to work unchanged.

@knoblichd

Copy link
Copy Markdown
Author

Working on CLA. Running down who at our Org will handle that aspect.

@ivankatliarchuk

Copy link
Copy Markdown
Member

I looked at the proposed solution. Looks like there is a problem

Example. The added concern - split-horizon zones: If you have a public and private zone with the same DNS name (e.g., "example.com."), they have distinct resource names in GCP (e.g., "my-zone-public", "my-zone-private"). In the old code, List() returns both, then the visibility filter keeps only the private one. In the new code, you'd need to have the exact private zone name in your filter - a suffix like "my-zone" that previously matched both and let the visibility filter do the disambiguation now just 404s on Get.

Basically the problem, there is no way to find out where we passed a zone suffix or full zone id.

@knoblichd

knoblichd commented May 4, 2026 •

Copy link
Copy Markdown
Author

I looked at the proposed solution. Looks like there is a problem

Example. The added concern - split-horizon zones: If you have a public and private zone with the same DNS name (e.g., "example.com."), they have distinct resource names in GCP (e.g., "my-zone-public", "my-zone-private"). In the old code, List() returns both, then the visibility filter keeps only the private one. In the new code, you'd need to have the exact private zone name in your filter - a suffix like "my-zone" that previously matched both and let the visibility filter do the disambiguation now just 404s on Get.

Basically the problem, there is no way to find out where we passed a zone suffix or full zone id.

Good catch on split-horizon. Updated to handle it: we attempt Get for each zone ID, but if we get a 404 (indicating the ID may be a suffix pattern rather than an exact name), we fall back to List so the existing suffix-match + visibility-filter behavior is fully preserved. Exact zone IDs still benefit from the reduced IAM permissions — suffix patterns degrade gracefully to the previous behavior with no change for existing deployments.

EDIT: Why

To add context on the motivation: in multi-tenant environments where multiple external-dns instances share a single GCP project, each instance's service account currently requires dns.managedZones.list — a project-level permission. This means any compromised SA can enumerate all zone names across every tenant in the project, enabling cross-environment reconnaissance. By using Get when exact zone IDs are configured, each SA only needs access to its own zone with zero project-level visibility into other tenants.

@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from fb3a4b9 to ff778b8 Compare May 4, 2026 16:28
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 4, 2026
@ivankatliarchuk

Copy link
Copy Markdown
Member

/ok-to-test

@k8s-ci-robot k8s-ci-robot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels May 5, 2026
@coveralls

coveralls commented May 5, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 43293585328

Coverage decreased (-0.003%) to 83.468%

Details

  • Coverage decreased (-0.003%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 41 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

41 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
google/google.go 41 77.14%

Coverage Stats

Coverage Status
Relevant Lines: 19870
Covered Lines: 16585
Line Coverage: 83.47%
Coverage Strength: 1463.82 hits per line

💛 - Coveralls

@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels May 5, 2026
@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from f74b90f to 1453d0d Compare May 11, 2026 15:46
@k8s-ci-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign vflaux for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from 1453d0d to a0b844c Compare June 8, 2026 14:20
@knoblichd

Copy link
Copy Markdown
Author

Rebased onto current master (23 commits). All prior reviewer feedback from @ivankatliarchuk has been addressed — the implementation now tries Get per configured zone ID and falls back to List on 404 to preserve backward compatibility with suffix-pattern zone IDs.

@szuecs @vflaux — would appreciate a review when you get a chance. Happy to make any further changes.

@knoblichd

Copy link
Copy Markdown
Author

/assign @vflaux

/cc @mloiseleur
/cc @Raffo

If anyone has any concerns or improvements please let me know.

@mloiseleur mloiseleur changed the title provider/google: use Get instead of List when single zone ID is confi… feat(provider/google): allow to use only zone-scoped permission on single zone Jun 15, 2026
Comment thread provider/google/google_test.go Outdated
return nil, m.zonesErr
}
// Try lookup by zone name first (GCP API accepts both name and numeric ID)
if zone, ok := testZones[m.project+"/"+m.managedZone]; ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if zone, ok := testZones[m.project+"/"+m.managedZone]; ok {
zoneKey := zoneKey(m.project, m.managedZone.Name)
if zone, ok := testZones[zoneKey]; ok {

@mloiseleur

mloiseleur commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Would you please also add documentation on this feature? And also about the change on zone match?

Note: I updated PR title to reflect that it's a breaking change. The suffix semantic was not really documented nor supported, but still, we may have users relying on it, so we need to document clearly this change.

@mloiseleur mloiseleur changed the title feat(provider/google): allow to use only zone-scoped permission on single zone feat(provider/google)!: allow to use only zone-scoped permission on single zone Jul 31, 2026
@kubernetes-prow kubernetes-prow Bot added docs cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. and removed cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Aug 3, 2026
@knoblichd

Copy link
Copy Markdown
Author

@mloiseleur Thank you most kindly for the review! I believe I have addressed what you have noted. Please let me know if I have not.

Thank you again!

@kubernetes-prow kubernetes-prow Bot added the apis Issues or PRs related to API change label Aug 3, 2026
@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch 2 times, most recently from 6f77723 to 009d300 Compare August 3, 2026 19:32
@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels Aug 3, 2026
@vflaux

vflaux commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@knoblichd Did you accidentally remove your changes in GoogleProvider.Zones()?
Given the latest commit, this PR no longer makes sense. There is no behavioral changes and managedZonesServiceInterface.Get() is never called.

Also, I don't think provider-specific behavior should be documented directly in generic flags. This belongs in the provider tutorial.

@knoblichd

Copy link
Copy Markdown
Author

@vflaux You're right, that last commit reverted the fix by mistake. I've restored the Get-with-List-fallback and applied @mloiseleur's suggestions. The docs are now in the GKE tutorial instead of the generic flag description, including the change to zone ID matching.

@vflaux

vflaux commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

I don't see any new commits in the PR. Could you push the changes?

@kubernetes-prow kubernetes-prow Bot added cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. and removed cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Oct 5, 2026
@knoblichd

Copy link
Copy Markdown
Author

I don't see any new commits in the PR. Could you push the changes?

I do very much apologize. The push failed and I didn't realize it until today. Updates should be there now.

@mloiseleur

Copy link
Copy Markdown
Collaborator

@knoblichd You need to fix commits as described in easycla comment (#6408 (comment)) before we can continue on this PR.

@knoblichd

Copy link
Copy Markdown
Author

/easycla

@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from bb3830b to 0bd84aa Compare November 12, 2026 16:57
@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels Nov 12, 2026
@knoblichd

Copy link
Copy Markdown
Author

I am very sorry for the delay. I thought I had addressed the commits, but apparently it had failed. Should be good to go now.

@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from 0bd84aa to ee645bf Compare November 12, 2026 17:13
…ingle zone

Resolve each configured zone ID with managedZones.get, falling back to
managedZones.list on 404, so external-dns can run with permissions scoped to
a single zone. An exact zone name now resolves via Get alone and no longer
matches suffix siblings. Document zone-scoped IAM in the GKE tutorial.
@knoblichd
knoblichd force-pushed the plt-4357-skip-list-when-single-zone-id branch from ee645bf to fb2b884 Compare November 12, 2026 19:46

@mloiseleur mloiseleur left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for restoring the change.
Design is fine to me: Get per ID, List fallback on 404, breaking change documented.
Remaining asks are about size: extract the Get path into a helper, shorten comments and docs, and assert listCallCount in the two other tests.
Could you also update the PR description? It still says "exactly one zone ID", does not mention the breaking change, and the docs checkbox is unchecked.

Comment thread provider/google/google.go
Comment on lines +175 to +222
// When zone ID filters are configured, attempt Get for each ID to avoid requiring
// dns.managedZones.list — a project-level permission that exposes all zone names in
// the project, enabling cross-environment enumeration in multi-tenant deployments.
//
// If Get returns 404 for a zone ID, it may be a suffix pattern (e.g. "my-zone" matching
// both "public-my-zone" and "private-my-zone" in split-horizon setups). In that case we
// fall back to List so the existing suffix-match + visibility-filter behavior is preserved.
// A zone ID that is an exact zone name or numeric ID resolves via Get alone, so it no
// longer also matches other zones whose names end with it.
if p.zoneIDFilter.IsConfigured() {
log.Debugf("Zone ID filters configured %v, attempting Get instead of List", p.zoneIDFilter.ZoneIDs)

needsList := false
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
if zoneID == "" {
continue
}

zone, err := p.managedZonesClient.Get(p.project, zoneID).Do()
if err != nil {
var apiErr *googleapi.Error
if errors.As(err, &apiErr) && apiErr.Code == http.StatusNotFound {
log.Warnf("Zone %s not found via Get (may be a suffix pattern), falling back to List", zoneID)
needsList = true
break
}
return nil, provider.NewSoftErrorf("failed to get zone %s: %w", zoneID, err)
}

if zone.PeeringConfig == nil && p.domainFilter.Match(zone.DnsName) && p.zoneTypeFilter.Match(zone.Visibility) {
zones[zone.Name] = zone
log.Debugf("Matched %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
} else {
log.Debugf("Filtered %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
}
}

if !needsList {
if len(zones) == 0 {
log.Warnf("No zones in project %s matched zone ID filters %v and domain filters %v", p.project, p.zoneIDFilter.ZoneIDs, p.domainFilter)
}
for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", zone.Name, zone.DnsName)
}
return zones, nil
}

zones = make(map[string]*dns.ManagedZone)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this block into a helper?
The needsList flag, the break, the map reset and the copied "No zones" / "Considering" logs all go away:

Suggested change
// When zone ID filters are configured, attempt Get for each ID to avoid requiring
// dns.managedZones.list — a project-level permission that exposes all zone names in
// the project, enabling cross-environment enumeration in multi-tenant deployments.
//
// If Get returns 404 for a zone ID, it may be a suffix pattern (e.g. "my-zone" matching
// both "public-my-zone" and "private-my-zone" in split-horizon setups). In that case we
// fall back to List so the existing suffix-match + visibility-filter behavior is preserved.
// A zone ID that is an exact zone name or numeric ID resolves via Get alone, so it no
// longer also matches other zones whose names end with it.
if p.zoneIDFilter.IsConfigured() {
log.Debugf("Zone ID filters configured %v, attempting Get instead of List", p.zoneIDFilter.ZoneIDs)
needsList := false
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
if zoneID == "" {
continue
}
zone, err := p.managedZonesClient.Get(p.project, zoneID).Do()
if err != nil {
var apiErr *googleapi.Error
if errors.As(err, &apiErr) && apiErr.Code == http.StatusNotFound {
log.Warnf("Zone %s not found via Get (may be a suffix pattern), falling back to List", zoneID)
needsList = true
break
}
return nil, provider.NewSoftErrorf("failed to get zone %s: %w", zoneID, err)
}
if zone.PeeringConfig == nil && p.domainFilter.Match(zone.DnsName) && p.zoneTypeFilter.Match(zone.Visibility) {
zones[zone.Name] = zone
log.Debugf("Matched %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
} else {
log.Debugf("Filtered %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
}
}
if !needsList {
if len(zones) == 0 {
log.Warnf("No zones in project %s matched zone ID filters %v and domain filters %v", p.project, p.zoneIDFilter.ZoneIDs, p.domainFilter)
}
for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", zone.Name, zone.DnsName)
}
return zones, nil
}
zones = make(map[string]*dns.ManagedZone)
if p.zoneIDFilter.IsConfigured() {
if byID, ok, err := p.zonesByID(); err != nil || ok {
return byID, err
}
}
// zonesByID resolves zone IDs with Get, so only dns.managedZones.get is needed.
// ok is false when an ID is not an exact zone name or ID; the caller then lists.
func (p *GoogleProvider) zonesByID() (map[string]*dns.ManagedZone, bool, error) {
	zones := make(map[string]*dns.ManagedZone)
	for _, zoneID := range p.zoneIDFilter.ZoneIDs {
		if zoneID == "" {
			continue
		}
		zone, err := p.managedZonesClient.Get(p.project, zoneID).Do()
		var apiErr *googleapi.Error
		if errors.As(err, &apiErr) && apiErr.Code == http.StatusNotFound {
			log.Warnf("Zone %s not found via Get (may be a suffix pattern), falling back to List", zoneID)
			return nil, false, nil
		}
		if err != nil {
			return nil, false, provider.NewSoftErrorf("failed to get zone %s: %w", zoneID, err)
		}
		if zone.PeeringConfig == nil && p.domainFilter.Match(zone.DnsName) && p.zoneTypeFilter.Match(zone.Visibility) {
			zones[zone.Name] = zone
			log.Debugf("Matched %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
		} else {
			log.Debugf("Filtered %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility)
		}
	}
	if len(zones) == 0 {
		log.Warnf("No zones in project %s matched zone ID filters %v and domain filters %v", p.project, p.zoneIDFilter.ZoneIDs, p.domainFilter)
	}
	return zones, true, nil
}

The two-line doc comment replaces the 9-line block comment; the split-horizon and matching details are already in the GKE tutorial.

Comment on lines +99 to +107
// Try lookup by zone name first (GCP API accepts both name and numeric ID)
if zone, ok := testZones[zoneKey(m.project, m.managedZone)]; ok {
return zone, nil
}
// Fall back to lookup by numeric ID string (GCP API also accepts numeric IDs)
for key, zone := range testZones {
if strings.HasPrefix(key, m.project+"/") && fmt.Sprintf("%v", zone.Id) == m.managedZone {
return zone, nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these comments restate the code.

Suggested change
// Try lookup by zone name first (GCP API accepts both name and numeric ID)
if zone, ok := testZones[zoneKey(m.project, m.managedZone)]; ok {
return zone, nil
}
// Fall back to lookup by numeric ID string (GCP API also accepts numeric IDs)
for key, zone := range testZones {
if strings.HasPrefix(key, m.project+"/") && fmt.Sprintf("%v", zone.Id) == m.managedZone {
return zone, nil
}
if zone, ok := testZones[zoneKey(m.project, m.managedZone)]; ok {
return zone, nil
}
for key, zone := range testZones {
if strings.HasPrefix(key, m.project+"/") && fmt.Sprintf("%v", zone.Id) == m.managedZone {
return zone, nil
}
}

validateZones(t, zones, map[string]*dns.ManagedZone{
"internal-2": {Name: "internal-2", DnsName: "cluster.local.", Id: 10002, Visibility: "private"},
"internal-3": {Name: "internal-3", DnsName: "cluster.local.", Id: 10003, Visibility: "private"},
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is the main goal of the PR, so it should also assert that List is not called:

Suggested change
})
})
assert.Equal(t, 0, provider.managedZonesClient.(*mockManagedZonesClient).listCallCount)


validateZones(t, zones, map[string]*dns.ManagedZone{
"internal-1": {Name: "internal-1", DnsName: "cluster.local.", Id: 10001, Visibility: "private"},
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
})
})
assert.Equal(t, 1, provider.managedZonesClient.(*mockManagedZonesClient).listCallCount)

Comment on lines +307 to +310
// TestGoogleZonesIDFilterGetFallbackToList verifies that a zone ID suffix pattern that
// does not resolve via Get (404) falls back to List for backward compatibility.
// "ernal-1" is a suffix of "internal-1" but not an exact zone name, so Get 404s
// and we fall back to List which applies the suffix filter as before.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the test name already says most of it; one line is enough here (same for L293-294 and L332-334).

Suggested change
// TestGoogleZonesIDFilterGetFallbackToList verifies that a zone ID suffix pattern that
// does not resolve via Get (404) falls back to List for backward compatibility.
// "ernal-1" is a suffix of "internal-1" but not an exact zone name, so Get 404s
// and we fall back to List which applies the suffix filter as before.
// "ernal-1" is not an exact zone name, so Get returns 404 and List applies the suffix match.

Comment thread docs/tutorials/gke.md
Comment on lines +248 to +258
### Restricting access to specific zones

By default, ExternalDNS lists every managed zone in the project. That requires `dns.managedZones.list`, which can only be granted at project level (for example through `roles/dns.reader` above) and exposes the names of all zones in the project.

To limit ExternalDNS to specific zones, set `--zone-id-filter` to each zone's exact name or numeric ID, and grant only a zone-level role such as `roles/dns.admin` on those zones (`google_dns_managed_zone_iam_member` above), without the project-level `roles/dns.reader`.
ExternalDNS then looks up each zone with `dns.managedZones.get` instead of listing.

!!! warning "Change in zone ID matching"
`--zone-id-filter` values are matched as a suffix of the zone name or numeric ID, so `public` used to match both `public` and `prod-public`.
A value that is an exact zone name or ID now matches only that zone.
A value that matches no zone exactly (a suffix pattern such as `ernal-1`) still falls back to listing, keeps the old suffix behavior, and still needs `dns.managedZones.list`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: paragraph 1 and the warning explain List/Get twice. Shorter version:

Suggested change
### Restricting access to specific zones
By default, ExternalDNS lists every managed zone in the project. That requires `dns.managedZones.list`, which can only be granted at project level (for example through `roles/dns.reader` above) and exposes the names of all zones in the project.
To limit ExternalDNS to specific zones, set `--zone-id-filter` to each zone's exact name or numeric ID, and grant only a zone-level role such as `roles/dns.admin` on those zones (`google_dns_managed_zone_iam_member` above), without the project-level `roles/dns.reader`.
ExternalDNS then looks up each zone with `dns.managedZones.get` instead of listing.
!!! warning "Change in zone ID matching"
`--zone-id-filter` values are matched as a suffix of the zone name or numeric ID, so `public` used to match both `public` and `prod-public`.
A value that is an exact zone name or ID now matches only that zone.
A value that matches no zone exactly (a suffix pattern such as `ernal-1`) still falls back to listing, keeps the old suffix behavior, and still needs `dns.managedZones.list`.
### Restricting access to specific zones
Set `--zone-id-filter` to each zone's exact name or numeric ID and grant a zone-level role (`google_dns_managed_zone_iam_member` above) instead of project-level `roles/dns.reader`.
ExternalDNS then uses `dns.managedZones.get` and does not need `dns.managedZones.list`, which exposes every zone name in the project.
!!! warning "Change in zone ID matching"
An exact zone name or ID now matches only that zone (`public` no longer also matches `prod-public`).
A suffix pattern that matches no zone exactly still lists all zones and needs `dns.managedZones.list`.
With only zone-level roles, Cloud DNS returns `403 Forbidden` (not `404`) for a value that is not an exact zone name or ID, so a typo or a suffix pattern shows up as a permission error.

The last line matters for support: without project-level permissions, Cloud DNS answers 403 for a zone that does not exist, so the 404 fallback never runs and the error looks like a missing IAM binding.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apis Issues or PRs related to API change cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. docs ok-to-test Indicates a non-member PR verified by an org member that is safe to test. provider Issues or PRs related to a provider size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants