Skip to content

Commit

Permalink
feat: support auth without GCP Service Account
Browse files Browse the repository at this point in the history
  • Loading branch information
nazarewk committed Nov 13, 2024
1 parent e93f3bb commit 1f4a990
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 128 deletions.
56 changes: 30 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ Note: GKE or Anthos natively support injecting workload identity for pods. This
# The value must be one of 'gcloud'(default) or 'direct'.
# Refer to the next section for 'direct' injection mode
cloud.google.com/injection-mode: "gcloud"

# optional: Defaults to value inside `service-account-email`
#
cloud.google.com/project: "12345"
```
4. All new pods launched using the Kubernetes `ServiceAccount` will be mutated so that they can impersonate the GCP service account. Below is an example pod spec with the environment variables and volume fields mutated by the webhook.
Expand All @@ -62,13 +66,13 @@ Note: GKE or Anthos natively support injecting workload identity for pods. This
metadata:
name: app-x-pod
namespace: service-a
annotations:
# optional: A comma-separated list of initContainers and container names
# to skip adding volumeMounts and environment variables
cloud.google.com/skip-containers: "init-first,sidecar"
# optional: Defaults to 86400, or value specified in ServiceAccount
# annotation as shown in previous step, for expirationSeconds if not set
cloud.google.com/token-expiration: "86400"
annotations:
# optional: A comma-separated list of initContainers and container names
# to skip adding volumeMounts and environment variables
cloud.google.com/skip-containers: "init-first,sidecar"
# optional: Defaults to 86400, or value specified in ServiceAccount
# annotation as shown in previous step, for expirationSeconds if not set
cloud.google.com/token-expiration: "86400"
spec:
serviceAccountName: app-x
initContainers:
Expand Down Expand Up @@ -167,27 +171,27 @@ To use direct injection mode:
metadata:
name: app-x-pod
namespace: service-a
annotations:
# optional: A comma-separated list of initContainers and container names
# to skip adding volumeMounts and environment variables
cloud.google.com/skip-containers: "init-first,sidecar"
#
# The Generated External Credentials Json is added as an annotation, and mounted into the container filesystem via the DownwardAPI Volume
#
cloud.google.com/external-credentials-json: |-
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/on-prem-kubernetes/providers/this-cluster",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken",
"credential_source": {
"file": "/var/run/secrets/sts.googleapis.com/serviceaccount/token",
"format": {
"type": "text"
annotations:
# optional: A comma-separated list of initContainers and container names
# to skip adding volumeMounts and environment variables
cloud.google.com/skip-containers: "init-first,sidecar"
#
# The Generated External Credentials Json is added as an annotation, and mounted into the container filesystem via the DownwardAPI Volume
#
cloud.google.com/external-credentials-json: |-
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/on-prem-kubernetes/providers/this-cluster",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken",
"credential_source": {
"file": "/var/run/secrets/sts.googleapis.com/serviceaccount/token",
"format": {
"type": "text"
}
}
}
}
spec:
serviceAccountName: app-x
initContainers:
Expand Down
6 changes: 6 additions & 0 deletions webhooks/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ const (
//
// Set to 'direct' or 'gcloud' to determine credential injection mode. Defaults to 'gcloud'.
InjectionModeAnnotation = "injection-mode"

//
// Annotations for ServiceAccount
//
// Override GCP Project normally parsed from ServiceAccount.
ProjectAnnotation = "project"
)
7 changes: 4 additions & 3 deletions webhooks/external_account_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type ExternalAccountCredentials struct {
TokenInfoURL string `json:"token_info_url,omitempty"`
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url,omitempty"`
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
// token will be valid for.
ServiceAccountImpersonationLifetimeSeconds int `json:"service_account_impersonation_lifetime_seconds,omitempty"`
Expand Down Expand Up @@ -63,9 +63,10 @@ func NewExternalAccountCredentials(aud, gsaEmail string) *ExternalAccountCredent
File: filepath.Join(K8sSATokenMountPath, K8sSATokenName),
Format: CredentialFormat{Type: "text"},
},
ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", gsaEmail),
}

if gsaEmail != "" {
creds.ServiceAccountImpersonationURL = fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", gsaEmail)
}
return creds
}

Expand Down
14 changes: 7 additions & 7 deletions webhooks/identityconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ var (
)

type GCPWorkloadIdentityConfig struct {
Project *string
WorkloadIdentityProvider *string
ServiceAccountEmail *string
ServiceAccountEmail string
RunAsUser *int64
InjectionMode InjectionMode

Expand All @@ -44,12 +45,15 @@ func NewGCPWorkloadIdentityConfig(
}

if v, ok := sa.Annotations[filepath.Join(annotationDomain, ServiceAccountEmailAnnotation)]; ok {
cfg.ServiceAccountEmail = &v
cfg.ServiceAccountEmail = v
}

if v, ok := sa.Annotations[filepath.Join(annotationDomain, AudienceAnnotation)]; ok {
cfg.Audience = &v
}
if v, ok := sa.Annotations[filepath.Join(annotationDomain, ProjectAnnotation)]; ok {
cfg.Project = &v
}

if v, ok := sa.Annotations[filepath.Join(annotationDomain, TokenExpirationAnnotation)]; ok {
seconds, err := strconv.ParseInt(v, 10, 64)
Expand Down Expand Up @@ -80,14 +84,10 @@ func NewGCPWorkloadIdentityConfig(
cfg.InjectionMode = UndefinedMode
}

if cfg.WorkloadIdentityProvider == nil && cfg.ServiceAccountEmail == nil {
if cfg.WorkloadIdentityProvider == nil {
return nil, nil
}

if cfg.WorkloadIdentityProvider == nil || cfg.ServiceAccountEmail == nil {
return nil, fmt.Errorf("%s, %s must set at a time", filepath.Join(annotationDomain, WorkloadIdentityProviderAnnotation), filepath.Join(annotationDomain, TokenExpirationAnnotation))
}

if !workloadIdentityProviderRegex.Match([]byte(*cfg.WorkloadIdentityProvider)) {
return nil, fmt.Errorf("%s must be form of %s", filepath.Join(annotationDomain, WorkloadIdentityProviderAnnotation), workloadIdentityProviderFmt)
}
Expand Down
42 changes: 15 additions & 27 deletions webhooks/identityconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
Annotations: map[string]string{},
},
}
idConfig, err := NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err := NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(err).NotTo(HaveOccurred())
Expect(idConfig).To(BeNil())
})
Expand All @@ -39,11 +39,11 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err := NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err := NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(err).NotTo(HaveOccurred())
Expect(idConfig).To(BeEquivalentTo(&GCPWorkloadIdentityConfig{
WorkloadIdentityProvider: &workloadProvider,
ServiceAccountEmail: &saEmail,
ServiceAccountEmail: saEmail,
Audience: nil,
TokenExpirationSeconds: nil,
}))
Expand All @@ -61,11 +61,11 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err := NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err := NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(err).NotTo(HaveOccurred())
Expect(idConfig).To(BeEquivalentTo(&GCPWorkloadIdentityConfig{
WorkloadIdentityProvider: &workloadProvider,
ServiceAccountEmail: &saEmail,
ServiceAccountEmail: saEmail,
Audience: &audience,
TokenExpirationSeconds: &tokenExpiration,
}))
Expand All @@ -84,11 +84,11 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err := NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err := NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(err).NotTo(HaveOccurred())
Expect(idConfig).To(BeEquivalentTo(&GCPWorkloadIdentityConfig{
WorkloadIdentityProvider: &workloadProvider,
ServiceAccountEmail: &saEmail,
ServiceAccountEmail: saEmail,
Audience: &audience,
TokenExpirationSeconds: &tokenExpiration,
InjectionMode: DirectMode,
Expand All @@ -108,11 +108,11 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err := NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err := NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(err).NotTo(HaveOccurred())
Expect(idConfig).To(BeEquivalentTo(&GCPWorkloadIdentityConfig{
WorkloadIdentityProvider: &workloadProvider,
ServiceAccountEmail: &saEmail,
ServiceAccountEmail: saEmail,
Audience: &audience,
TokenExpirationSeconds: &tokenExpiration,
InjectionMode: GCloudMode,
Expand All @@ -134,21 +134,9 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err = NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
Expect(idConfig).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("must set at a time")))

By("without workload-identity-provider annotation")
sa = corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
saEmailAnnotation: saEmail,
},
},
}
idConfig, err = NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
Expect(idConfig).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("must set at a time")))
idConfig, err = NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(idConfig).NotTo(BeNil())
Expect(err).To(BeNil())
})
})
When("ServiceAccount with malformed workload-identity-provider annotation", func() {
Expand All @@ -161,7 +149,7 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err = NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err = NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(idConfig).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("must be form of")))
})
Expand All @@ -177,7 +165,7 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err = NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err = NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(idConfig).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("must be positive integer string")))
})
Expand All @@ -193,7 +181,7 @@ var _ = Describe("NewGCPWorkloadIdentityConfig", func() {
},
},
}
idConfig, err = NewGCPWorkloadIdentityConfig(annotaitonDomain, sa)
idConfig, err = NewGCPWorkloadIdentityConfig(annotationDomain, sa)
Expect(idConfig).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("mode must be")))
})
Expand Down
13 changes: 7 additions & 6 deletions webhooks/mutatepod.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ func (m *GCPWorkloadIdentityMutator) mutatePod(pod *corev1.Pod, idConfig GCPWork
pod.Annotations = map[string]string{}
}
pod.Annotations[filepath.Join(m.AnnotationDomain, WorkloadIdentityProviderAnnotation)] = *idConfig.WorkloadIdentityProvider
pod.Annotations[filepath.Join(m.AnnotationDomain, ServiceAccountEmailAnnotation)] = *idConfig.ServiceAccountEmail
pod.Annotations[filepath.Join(m.AnnotationDomain, ServiceAccountEmailAnnotation)] = idConfig.ServiceAccountEmail
pod.Annotations[filepath.Join(m.AnnotationDomain, AudienceAnnotation)] = audience
pod.Annotations[filepath.Join(m.AnnotationDomain, TokenExpirationAnnotation)] = fmt.Sprint(expirationSeconds)
if idConfig.InjectionMode == DirectMode {
// Add annotation
credBody, err := buildExternalCredentialsJson(*idConfig.WorkloadIdentityProvider, *idConfig.ServiceAccountEmail)
credBody, err := buildExternalCredentialsJson(*idConfig.WorkloadIdentityProvider, idConfig.ServiceAccountEmail)
if err != nil {
return err
}
Expand All @@ -57,10 +57,11 @@ func (m *GCPWorkloadIdentityMutator) mutatePod(pod *corev1.Pod, idConfig GCPWork
//
// calculate project from service account
//
matches := projectRegex.FindStringSubmatch(*idConfig.ServiceAccountEmail)
project := ""
if len(matches) >= 2 {
project = matches[1] // the group 0 is thw whole match
if idConfig.Project != nil {
project = *idConfig.Project
} else if matches := projectRegex.FindStringSubmatch(idConfig.ServiceAccountEmail); len(matches) >= 2 {
project = matches[1] // the group 0 is the whole match
}

//
Expand All @@ -75,7 +76,7 @@ func (m *GCPWorkloadIdentityMutator) mutatePod(pod *corev1.Pod, idConfig GCPWork
//
if idConfig.InjectionMode == GCloudMode || idConfig.InjectionMode == UndefinedMode {
pod.Spec.InitContainers = prependOrReplaceContainer(pod.Spec.InitContainers, gcloudSetupContainer(
*idConfig.WorkloadIdentityProvider, *idConfig.ServiceAccountEmail, project, m.GcloudImage, idConfig.RunAsUser, m.SetupContainerResources,
*idConfig.WorkloadIdentityProvider, idConfig.ServiceAccountEmail, project, m.GcloudImage, idConfig.RunAsUser, m.SetupContainerResources,
))
}

Expand Down
Loading

0 comments on commit 1f4a990

Please sign in to comment.