diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 263c4466..24245e02 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -1108,6 +1108,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "GCE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GCE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GCE_TTL": The TTL of the TXT record used for the DNS challenge`) + ew.writeln(` - "GCE_ZONE_ID": Allows to skip the automatic detection of the zone`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gcloud`) diff --git a/docs/content/dns/zz_gen_gcloud.md b/docs/content/dns/zz_gen_gcloud.md index adde36d7..71494528 100644 --- a/docs/content/dns/zz_gen_gcloud.md +++ b/docs/content/dns/zz_gen_gcloud.md @@ -58,6 +58,7 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}). | `GCE_POLLING_INTERVAL` | Time between DNS propagation check | | `GCE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GCE_TTL` | The TTL of the TXT record used for the DNS challenge | +| `GCE_ZONE_ID` | Allows to skip the automatic detection of the zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/providers/dns/gcloud/gcloud.toml b/providers/dns/gcloud/gcloud.toml index c08824b9..261e35b9 100644 --- a/providers/dns/gcloud/gcloud.toml +++ b/providers/dns/gcloud/gcloud.toml @@ -21,6 +21,7 @@ GCE_PROJECT="gc-project-id" GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file. GCE_SERVICE_ACCOUNT = "Account" [Configuration.Additional] GCE_ALLOW_PRIVATE_ZONE = "Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)" + GCE_ZONE_ID = "Allows to skip the automatic detection of the zone" GCE_POLLING_INTERVAL = "Time between DNS propagation check" GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GCE_TTL = "The TTL of the TXT record used for the DNS challenge" diff --git a/providers/dns/gcloud/googlecloud.go b/providers/dns/gcloud/googlecloud.go index 34a7d1e0..ff9fb6f0 100644 --- a/providers/dns/gcloud/googlecloud.go +++ b/providers/dns/gcloud/googlecloud.go @@ -32,6 +32,7 @@ const ( EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT" EnvProject = envNamespace + "PROJECT" + EnvZoneID = envNamespace + "ZONE_ID" EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE" EnvDebug = envNamespace + "DEBUG" @@ -44,6 +45,7 @@ const ( type Config struct { Debug bool Project string + ZoneID string AllowPrivateZone bool PropagationTimeout time.Duration PollingInterval time.Duration @@ -55,6 +57,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ Debug: env.GetOrDefaultBool(EnvDebug, false), + ZoneID: env.GetOrDefaultString(EnvZoneID, ""), AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second), @@ -310,24 +313,16 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // getHostedZone returns the managed-zone. func (d *DNSProvider) getHostedZone(domain string) (string, error) { - authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + authZone, zones, err := d.lookupHostedZoneID(domain) if err != nil { - return "", fmt.Errorf("designate: could not find zone for FQDN %q: %w", domain, err) + return "", err } - zones, err := d.client.ManagedZones. - List(d.config.Project). - DnsName(authZone). - Do() - if err != nil { - return "", fmt.Errorf("API call failed: %w", err) - } - - if len(zones.ManagedZones) == 0 { + if len(zones) == 0 { return "", fmt.Errorf("no matching domain found for domain %s", authZone) } - for _, z := range zones.ManagedZones { + for _, z := range zones { if z.Visibility == "public" || z.Visibility == "" || (z.Visibility == "private" && d.config.AllowPrivateZone) { return z.Name, nil } @@ -340,6 +335,45 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) { return "", fmt.Errorf("no public zone found for domain %s", authZone) } +// lookupHostedZoneID finds the managed zone ID in Google. +// +// Be careful here. +// An automated system might run in a GCloud Service Account, with access to edit the zone +// +// (gcloud dns managed-zones get-iam-policy $zone_id) (role roles/dns.admin) +// +// but not with project-wide access to list all zones +// +// (gcloud projects get-iam-policy $project_id) (a role with permission dns.managedZones.list) +// +// If we force a zone list to succeed, we demand more permissions than needed. +func (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*dns.ManagedZone, error) { + // GCE_ZONE_ID override for service accounts to avoid needing zones-list permission + if d.config.ZoneID != "" { + zone, err := d.client.ManagedZones.Get(d.config.Project, d.config.ZoneID).Do() + if err != nil { + return "", nil, fmt.Errorf("API call ManagedZones.Get for explicit zone ID %q in project %q failed: %w", d.config.ZoneID, d.config.Project, err) + } + + return zone.DnsName, []*dns.ManagedZone{zone}, nil + } + + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + if err != nil { + return "", nil, fmt.Errorf("could not find zone for FQDN %q: %w", domain, err) + } + + zones, err := d.client.ManagedZones. + List(d.config.Project). + DnsName(authZone). + Do() + if err != nil { + return "", nil, fmt.Errorf("API call ManagedZones.List failed: %w", err) + } + + return authZone, zones.ManagedZones, nil +} + func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) { recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do() if err != nil {