Let the VM list DNS zones so Caddy can present DNS-01 challenges

With DNS resolution fixed, issuance failed at the challenge:

  presenting for challenge: adding temporary record for zone
  "gitea.jasonmross.dev.": googleapi: Error 403: Forbidden

Testing with the VM service account's own token: managedZones/main and
its rrsets return 200, but managedZones (list) returns 403. The
googleclouddns plugin resolves the domain to a zone by listing the
project's managed zones, and listing is a project-level permission that
the zone-scoped dns.admin binding cannot grant.

Grant roles/dns.reader on the project. It adds read access only, in a
project that holds this single zone; every write stays zone-scoped. A
custom role with just dns.managedZones.list was the alternative, but
managing it would need iam.roleAdmin on the cb-infra Pulumi runner,
which widens a far more powerful identity to narrow a read-only one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 04:04:18 -05:00
committed by Jason Ross
co-authored by Claude Opus 5.5
parent 25dda00eaf
commit 69d586bfcc
2 changed files with 17 additions and 1 deletions
+15
View File
@@ -8,6 +8,7 @@ package dns
import (
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/dns"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/projects"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"gitea-infra/pkg/config"
@@ -60,5 +61,19 @@ func New(
return nil, err
}
// The zone-scoped grant is not enough on its own: the googleclouddns plugin
// maps the domain to a zone by LISTING the project's managed zones, and a
// list is a project-level permission that no zone binding can confer. Without
// this, presenting the challenge fails with a bare 403. dns.reader adds
// read-only access and nothing else, and every write stays scoped to the zone
// above.
if _, err := projects.NewIAMMember(ctx, "gitea-vm-dns-reader", &projects.IAMMemberArgs{
Project: pulumi.String(cfg.Project),
Role: pulumi.String("roles/dns.reader"),
Member: pulumi.Sprintf("serviceAccount:%s", vmServiceAccountEmail),
}, pulumi.DependsOn(deps)); err != nil {
return nil, err
}
return &DNS{Zone: zone, Record: rec}, nil
}
+2 -1
View File
@@ -17,7 +17,8 @@ ${DOMAIN} {
tls {
dns googleclouddns {
# Application Default Credentials come from the GCE metadata server.
# The VM service account holds roles/dns.admin scoped to this zone only.
# The VM service account holds roles/dns.admin scoped to this zone, plus
# project-level dns.reader so the plugin can list zones to find it.
gcp_project {env.GCP_PROJECT}
}
# Only used for propagation checks. If issuance stalls waiting for