Skip to content

Commit 8d7ed79

Browse files
git-johnsonldez
andauthored
gcloud: add service account impersonation (#2544)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent a528e28 commit 8d7ed79

File tree

6 files changed

+130
-35
lines changed

6 files changed

+130
-35
lines changed

cmd/zz_gen_cmd_dnshelp.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/dns/zz_gen_gcloud.md

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
914914
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
915915
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
916916
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
917+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
918+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
917919
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
918920
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
919921
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=

providers/dns/gcloud/gcloud.toml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,29 @@ Code = "gcloud"
55
Since = "v0.3.0"
66

77
Example = '''
8+
# Using a service account file
89
GCE_PROJECT="gc-project-id" \
910
GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \
10-
lego --email [email protected] --dns gcloud -d '*.example.com' -d example.com run
11+
lego --email [email protected] --dns gcloud -d '*.example.com' -d example.com run
12+
13+
# Using default credentials with impersonation
14+
GCE_PROJECT="gc-project-id" \
15+
GCE_IMPERSONATE_SERVICE_ACCOUNT="[email protected]" \
16+
lego --email [email protected] --dns gcloud -d '*.example.com' -d example.com run
17+
18+
# Using service account key with impersonation
19+
GCE_PROJECT="gc-project-id" \
20+
GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \
21+
GCE_IMPERSONATE_SERVICE_ACCOUNT="[email protected]" \
22+
lego --email [email protected] --dns gcloud -d '*.example.com' -d example.com run
23+
'''
24+
25+
Additional = '''
26+
Supports service account impersonation to access Google Cloud DNS resources across different projects or with restricted permissions.
27+
28+
When using impersonation, the source service account must have:
29+
1. The "Service Account Token Creator" role on the source service account
30+
2. The "https://www.googleapis.com/auth/cloud-platform" scope
1131
'''
1232

1333
[Configuration]
@@ -19,6 +39,7 @@ lego --email [email protected] --dns gcloud -d '*.example.com' -d example.com run
1939
[Configuration.Additional]
2040
GCE_ALLOW_PRIVATE_ZONE = "Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)"
2141
GCE_ZONE_ID = "Allows to skip the automatic detection of the zone"
42+
GCE_IMPERSONATE_SERVICE_ACCOUNT = "Service account email to impersonate"
2243
GCE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)"
2344
GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 180)"
2445
GCE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"

providers/dns/gcloud/googlecloud.go

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,24 @@ import (
1717
"github.com/go-acme/lego/v4/platform/config/env"
1818
"github.com/go-acme/lego/v4/platform/wait"
1919
"golang.org/x/net/context"
20+
"golang.org/x/oauth2"
2021
"golang.org/x/oauth2/google"
2122
"google.golang.org/api/dns/v1"
2223
"google.golang.org/api/googleapi"
24+
"google.golang.org/api/impersonate"
2325
"google.golang.org/api/option"
2426
)
2527

2628
// Environment variables names.
2729
const (
2830
envNamespace = "GCE_"
2931

30-
EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT"
31-
EnvProject = envNamespace + "PROJECT"
32-
EnvZoneID = envNamespace + "ZONE_ID"
33-
EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE"
34-
EnvDebug = envNamespace + "DEBUG"
32+
EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT"
33+
EnvProject = envNamespace + "PROJECT"
34+
EnvZoneID = envNamespace + "ZONE_ID"
35+
EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE"
36+
EnvDebug = envNamespace + "DEBUG"
37+
EnvImpersonateServiceAccount = envNamespace + "IMPERSONATE_SERVICE_ACCOUNT"
3538

3639
EnvTTL = envNamespace + "TTL"
3740
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
@@ -44,25 +47,27 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
4447

4548
// Config is used to configure the creation of the DNSProvider.
4649
type Config struct {
47-
Debug bool
48-
Project string
49-
ZoneID string
50-
AllowPrivateZone bool
51-
PropagationTimeout time.Duration
52-
PollingInterval time.Duration
53-
TTL int
54-
HTTPClient *http.Client
50+
Debug bool
51+
Project string
52+
ZoneID string
53+
AllowPrivateZone bool
54+
ImpersonateServiceAccount string
55+
PropagationTimeout time.Duration
56+
PollingInterval time.Duration
57+
TTL int
58+
HTTPClient *http.Client
5559
}
5660

5761
// NewDefaultConfig returns a default configuration for the DNSProvider.
5862
func NewDefaultConfig() *Config {
5963
return &Config{
60-
Debug: env.GetOrDefaultBool(EnvDebug, false),
61-
ZoneID: env.GetOrDefaultString(EnvZoneID, ""),
62-
AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false),
63-
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
64-
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),
65-
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
64+
Debug: env.GetOrDefaultBool(EnvDebug, false),
65+
ZoneID: env.GetOrDefaultString(EnvZoneID, ""),
66+
AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false),
67+
ImpersonateServiceAccount: env.GetOrDefaultString(EnvImpersonateServiceAccount, ""),
68+
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
69+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),
70+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
6671
}
6772
}
6873

@@ -95,14 +100,14 @@ func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
95100
return nil, errors.New("googlecloud: project name missing")
96101
}
97102

98-
client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
99-
if err != nil {
100-
return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %w", err)
101-
}
102-
103103
config := NewDefaultConfig()
104104
config.Project = project
105-
config.HTTPClient = client
105+
106+
var err error
107+
config.HTTPClient, err = newClientFromCredentials(context.Background(), config)
108+
if err != nil {
109+
return nil, fmt.Errorf("googlecloud: %w", err)
110+
}
106111

107112
return NewDNSProviderConfig(config)
108113
}
@@ -129,15 +134,14 @@ func NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) {
129134
project = datJSON.ProjectID
130135
}
131136

132-
conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope)
133-
if err != nil {
134-
return nil, fmt.Errorf("googlecloud: unable to acquire config: %w", err)
135-
}
136-
client := conf.Client(context.Background())
137-
138137
config := NewDefaultConfig()
139138
config.Project = project
140-
config.HTTPClient = client
139+
140+
var err error
141+
config.HTTPClient, err = newClientFromServiceAccountKey(context.Background(), config, saKey)
142+
if err != nil {
143+
return nil, fmt.Errorf("googlecloud: %w", err)
144+
}
141145

142146
return NewDNSProviderConfig(config)
143147
}
@@ -384,6 +388,54 @@ func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSe
384388
return recs.Rrsets, nil
385389
}
386390

391+
func newClientFromCredentials(ctx context.Context, config *Config) (*http.Client, error) {
392+
if config.ImpersonateServiceAccount != "" {
393+
ts, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
394+
if err != nil {
395+
return nil, fmt.Errorf("unable to get default token source: %w", err)
396+
}
397+
398+
return newImpersonateClient(ctx, config.ImpersonateServiceAccount, ts)
399+
}
400+
401+
client, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope)
402+
if err != nil {
403+
return nil, fmt.Errorf("unable to get Google Cloud client: %w", err)
404+
}
405+
406+
return client, nil
407+
}
408+
409+
func newClientFromServiceAccountKey(ctx context.Context, config *Config, saKey []byte) (*http.Client, error) {
410+
if config.ImpersonateServiceAccount != "" {
411+
conf, err := google.JWTConfigFromJSON(saKey, "https://www.googleapis.com/auth/cloud-platform")
412+
if err != nil {
413+
return nil, fmt.Errorf("unable to acquire config: %w", err)
414+
}
415+
416+
return newImpersonateClient(ctx, config.ImpersonateServiceAccount, conf.TokenSource(ctx))
417+
}
418+
419+
conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope)
420+
if err != nil {
421+
return nil, fmt.Errorf("unable to acquire config: %w", err)
422+
}
423+
424+
return conf.Client(ctx), nil
425+
}
426+
427+
func newImpersonateClient(ctx context.Context, impersonateServiceAccount string, ts oauth2.TokenSource) (*http.Client, error) {
428+
impersonatedTS, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
429+
TargetPrincipal: impersonateServiceAccount,
430+
Scopes: []string{dns.NdevClouddnsReadwriteScope},
431+
}, option.WithTokenSource(ts))
432+
if err != nil {
433+
return nil, fmt.Errorf("unable to create impersonated credentials: %w", err)
434+
}
435+
436+
return oauth2.NewClient(ctx, impersonatedTS), nil
437+
}
438+
387439
func mustUnquote(raw string) string {
388440
clean, err := strconv.Unquote(raw)
389441
if err != nil {

providers/dns/gcloud/googlecloud_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ var envTest = tester.NewEnvTest(
3030
envServiceAccountFile,
3131
envGoogleApplicationCredentials,
3232
envMetadataHost,
33-
EnvServiceAccount).
33+
EnvServiceAccount,
34+
EnvImpersonateServiceAccount).
3435
WithDomain(envDomain).
3536
WithLiveTestExtra(func() bool {
3637
_, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)

0 commit comments

Comments
 (0)